Skip to content

Commit cde346c

Browse files
Document the new card group export tests with Given/When/Then comments
Co-authored-by: Yauheni Horbach <ZhenjaHorbach@users.noreply.github.com>
1 parent 7481f93 commit cde346c

1 file changed

Lines changed: 31 additions & 7 deletions

File tree

‎tests/unit/hooks/useSearchBulkActionsExportTest.ts‎

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,44 +1290,59 @@ describe('useSearchBulkActions - export options', () => {
12901290
}
12911291

12921292
it('offers the Reconciliation template, and only that template, for a card group selection', async () => {
1293+
// Given a user who qualifies for the Reconciliation template, offered alongside the templates that stay hidden for a group selection
12931294
mockTemplatesIncludingReconciliation();
1295+
// Given a ticked card group row, because selecting the card group rather than the individual expenses is how an admin reconciles a statement
12941296
selectCardGroup();
12951297

1298+
// When the export menu is built for a search grouped by card
12961299
const {result} = renderHook(() => useSearchBulkActions({queryJSON: cardGroupedExpenseQueryJSON}), {wrapper: OnyxListItemProvider});
12971300

1301+
// Then Reconciliation is the one template still offered, because the selected card groups can be expressed as a `cardID:` filter while the other templates have no way to describe a group
12981302
await waitFor(() => {
12991303
expect(getExportOptionTexts(result.current.headerButtonsOptions)).toEqual(['export.currentView', 'export.reconciliationAllExpenses']);
13001304
});
13011305
});
13021306

13031307
it('offers the Reconciliation template for a card group whose children were never loaded', async () => {
1308+
// Given a user who qualifies for the Reconciliation template
13041309
mockTemplatesIncludingReconciliation();
1310+
// Given a card group ticked before any of its children were loaded, so the selection holds the group key on its own instead of stamped children
13051311
selectEmptyCardGroup();
13061312

1313+
// When the export menu is built for a search grouped by card
13071314
const {result} = renderHook(() => useSearchBulkActions({queryJSON: cardGroupedExpenseQueryJSON}), {wrapper: OnyxListItemProvider});
13081315

1316+
// Then the template is offered just the same, because both shapes a card group selection can take have to reach the carve-out or the option would come and go with how far the list happened to be scrolled
13091317
await waitFor(() => {
13101318
expect(getExportOptionTexts(result.current.headerButtonsOptions)).toEqual(['export.currentView', 'export.reconciliationAllExpenses']);
13111319
});
13121320
});
13131321

13141322
it('keeps every template hidden when the group selection is grouped by something other than card', async () => {
1323+
// Given a user who qualifies for the Reconciliation template
13151324
mockTemplatesIncludingReconciliation();
1325+
// Given a ticked group row
13161326
selectCardGroup();
13171327

1328+
// When the export menu is built for a search grouped by category instead of by card
13181329
const {result} = renderHook(() => useSearchBulkActions({queryJSON: groupedExpenseQueryJSON}), {wrapper: OnyxListItemProvider});
13191330

1331+
// Then no template is offered at all, because the carve-out is limited to card groups and every other grouping keeps the existing rule that a group cannot be scoped
13201332
await waitFor(() => {
13211333
expect(getExportOptionTexts(result.current.headerButtonsOptions)).toEqual(['export.currentView']);
13221334
});
13231335
});
13241336

13251337
it('hides the Reconciliation template for a card group selection when the user is not a card-enabled admin', async () => {
1326-
// getExportTemplates leaves the template out entirely for a user who does not qualify for it.
1338+
// Given a user who does not qualify for the template, so getExportTemplates leaves it out entirely
1339+
// Given a ticked card group row
13271340
selectCardGroup();
13281341

1342+
// When the export menu is built for a search grouped by card
13291343
const {result} = renderHook(() => useSearchBulkActions({queryJSON: cardGroupedExpenseQueryJSON}), {wrapper: OnyxListItemProvider});
13301344

1345+
// Then only Current view is offered, because the carve-out narrows the templates the user was already entitled to and must never hand out one they were not
13311346
await waitFor(() => {
13321347
expect(getExportOptionTexts(result.current.headerButtonsOptions)).toEqual(['export.currentView']);
13331348
});
@@ -1337,12 +1352,14 @@ describe('useSearchBulkActions - export options', () => {
13371352
// children selected, looking for the group prefix alone misses the group and exports the loaded IDs instead
13381353
// of the `cardID:` filter, so a group with paginated children would export incompletely.
13391354
it('scopes the Reconciliation export to the selected card groups instead of a transaction ID list', async () => {
1355+
// Given a user who qualifies for the Reconciliation template
13401356
mockTemplatesIncludingReconciliation();
1357+
// Given a ticked card group
13411358
selectCardGroup();
1342-
// An expense ticked on its own, alongside the card group, so it is not covered by the group's filter.
1359+
// Given one expense ticked on its own alongside it, because that expense belongs to another card and so is not covered by the group's filter
13431360
mockSelectedTransactions.tx2 = makeSelectedTransaction();
13441361
const searchResults = makeSearchResults([]);
1345-
// The card group row as it arrives in the search snapshot, which the group's filter entry is derived from.
1362+
// Given the card group row as it arrives in the search snapshot, which the group's filter entry is derived from
13461363
Object.assign(searchResults.data, {[CARD_GROUP_KEY]: {cardID: 1234}});
13471364
mockCurrentSearchResults = searchResults;
13481365
jest.mocked(getSelectedGroupFilterEntry).mockReturnValue({key: CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID, value: 1234});
@@ -1353,16 +1370,19 @@ describe('useSearchBulkActions - export options', () => {
13531370
expect(getExportOptionByText(result.current.headerButtonsOptions, 'export.reconciliationAllExpenses')).toBeDefined();
13541371
});
13551372

1373+
// When the user runs the Reconciliation export on that selection
13561374
getExportOptionByText(result.current.headerButtonsOptions, 'export.reconciliationAllExpenses')?.onSelected?.();
13571375

13581376
await waitFor(() => {
13591377
expect(queueExportSearchWithTemplate).toHaveBeenCalled();
13601378
});
13611379

13621380
const [parameters] = jest.mocked(queueExportSearchWithTemplate).mock.calls.at(-1) ?? [];
1381+
// Then the chosen template is the one that runs
13631382
expect(parameters?.templateName).toBe(CONST.REPORT.EXPORT_OPTIONS.RECONCILIATION_ALL_EXPENSES);
1364-
// The card groups travel as a `cardID:` filter on the query, so neither the group row nor the children it
1365-
// selected are sent as IDs. The expense ticked on its own is kept, as "Current view" keeps it.
1383+
// Then the card groups travel as a `cardID:` filter on the query rather than as IDs, so the export covers
1384+
// every expense on the card and not only the rows that happened to be loaded, while the expense ticked on
1385+
// its own is still sent as an ID so it is not dropped, matching what "Current view" does.
13661386
expect(parameters?.reportIDList).toEqual([]);
13671387
expect(parameters?.transactionIDList).toEqual(['tx2']);
13681388
expect(parameters?.jsonQuery).toContain(CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID);
@@ -1372,9 +1392,10 @@ describe('useSearchBulkActions - export options', () => {
13721392
// Current view to the same expectation is what stops the two paths drifting apart again: the gate that shows
13731393
// the Reconciliation option and the gate that scopes it have to agree, or a card group exports incompletely.
13741394
it('scopes the Current view export of a card group the same way as the Reconciliation template', async () => {
1395+
// Given the same selection the Reconciliation export was checked against, so the two paths are compared on identical input
13751396
mockTemplatesIncludingReconciliation();
13761397
selectCardGroup();
1377-
// An expense ticked on its own, alongside the card group, so it is not covered by the group's filter.
1398+
// Given one expense ticked on its own alongside the group, because that expense belongs to another card and so is not covered by the group's filter
13781399
mockSelectedTransactions.tx2 = makeSelectedTransaction();
13791400
const searchResults = makeSearchResults([]);
13801401
Object.assign(searchResults.data, {[CARD_GROUP_KEY]: {cardID: 1234}});
@@ -1387,17 +1408,20 @@ describe('useSearchBulkActions - export options', () => {
13871408
expect(getExportOptionByText(result.current.headerButtonsOptions, 'export.currentView')).toBeDefined();
13881409
});
13891410

1411+
// When the user runs the Current view export instead of the template
13901412
getExportOptionByText(result.current.headerButtonsOptions, 'export.currentView')?.onSelected?.();
13911413

13921414
await waitFor(() => {
13931415
expect(exportSearchItemsToCSV).toHaveBeenCalled();
13941416
});
13951417

13961418
const {isGroupExport, reportIDList, transactionIDList, query} = getLastCSVExportParameters();
1419+
// Then it covers exactly the same rows, because an admin who exports the same selection twice must get the
1420+
// same scope back, and the two paths drifting apart is what made a card group export incompletely.
13971421
expect(isGroupExport).toBe(true);
13981422
expect(reportIDList).toEqual([]);
13991423
expect(transactionIDList).toEqual(['tx2']);
1400-
// The selected card groups reach the backend as a `cardID:` filter on the query rather than as IDs.
1424+
// Then the selected card groups reach the backend as a `cardID:` filter on the query rather than as IDs
14011425
expect(JSON.stringify(query)).toContain(CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID);
14021426
});
14031427
});

0 commit comments

Comments
 (0)