@@ -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