@@ -10,11 +10,8 @@ import {createWorkspace, generatePolicyID, setWorkspaceApprovalMode} from '@libs
1010import { addComment , notifyNewAction } from '@libs/actions/Report' ;
1111import initSplitExpense from '@libs/actions/SplitExpenses' ;
1212import type * as API from '@libs/API' ;
13- import type { WriteCommand } from '@libs/API/types' ;
1413import { WRITE_COMMANDS } from '@libs/API/types' ;
15- import { createTransitionBarrier , writeWhenReady } from '@libs/API/writeWhenReady' ;
1614import { getCurrencyDecimals , getCurrencySymbol } from '@libs/CurrencyUtils' ;
17- import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute' ;
1815import { rand64 } from '@libs/NumberUtils' ;
1916import { getIOUActionForReportID , getIOUActionForTransactionID , getOriginalMessage , isActionOfType , isAddCommentAction , isDeletedAction , isMoneyRequestAction } from '@libs/ReportActionsUtils' ;
2017import { buildOptimisticIOUReportAction , getAncestors , getReportOrDraftReport } from '@libs/ReportUtils' ;
@@ -38,7 +35,6 @@ import {
3835 updateSplitExpenseAmountField ,
3936 updateSplitExpenseField ,
4037} from '@userActions/IOU/SplitExpenseItems' ;
41- import type { UpdateSplitTransactionsParams } from '@userActions/IOU/SplitTransactionUpdate' ;
4238import { updateSplitTransactions , updateSplitTransactionsFromSplitExpensesFlow } from '@userActions/IOU/SplitTransactionUpdate' ;
4339
4440import CONST from '@src/CONST' ;
@@ -125,7 +121,6 @@ jest.mock('@src/libs/actions/Report', () => {
125121
126122jest . mock ( '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute' , ( ) => jest . fn ( ) ) ;
127123jest . mock ( '@libs/Navigation/helpers/isReportTopmostSplitNavigator' , ( ) => jest . fn ( ) ) ;
128- jest . mock ( '@libs/API/writeWhenReady' ) ;
129124jest . mock ( '@libs/deferredLayoutWrite' , ( ) => ( {
130125 registerDeferredWrite : ( _key : string , callback : ( ) => void ) => callback ( ) ,
131126 flushDeferredWrite : jest . fn ( ) ,
@@ -10305,221 +10300,6 @@ describe('startSplitBill delegateAccountID forwarding', () => {
1030510300 } ) ;
1030610301} ) ;
1030710302
10308- /**
10309- * Fixture for the save tests below: an expense report with a single transaction, plus a draft that splits that
10310- * transaction into two.
10311- *
10312- * `withExistingSplitChildren` also seeds those two split transactions in Onyx. That is what makes a one-item
10313- * `splitExpenses` mean "collapse an existing split back into one expense" (a reverse split) rather than "split
10314- * this expense into one".
10315- */
10316- const buildSplitFlowParams = async ( { withExistingSplitChildren = false , asSelfDMThread = false } = { } ) => {
10317- const expenseReport : Report = {
10318- ...createRandomReport ( 9001 , undefined ) ,
10319- type : CONST . REPORT . TYPE . EXPENSE ,
10320- } ;
10321- const transaction : Transaction = {
10322- amount : 100 ,
10323- currency : 'USD' ,
10324- transactionID : '9001' ,
10325- reportID : expenseReport . reportID ,
10326- created : DateUtils . getDBTime ( ) ,
10327- merchant : 'test' ,
10328- } ;
10329- // `asSelfDMThread` makes the thread a selfDM child of the expense report, which is the only shape that
10330- // reaches the navigate-first branch in `updateSplitTransactionsFromSplitExpensesFlow`.
10331- const transactionThread : Report = {
10332- ...createRandomReport ( 9002 , undefined ) ,
10333- type : CONST . REPORT . TYPE . CHAT ,
10334- ...( asSelfDMThread ? { chatType : CONST . REPORT . CHAT_TYPE . SELF_DM , parentReportID : expenseReport . reportID } : { } ) ,
10335- } ;
10336- const iouAction : ReportAction = {
10337- ...buildOptimisticIOUReportAction ( {
10338- type : CONST . IOU . REPORT_ACTION_TYPE . CREATE ,
10339- amount : transaction . amount ,
10340- currency : transaction . currency ,
10341- comment : '' ,
10342- participants : [ ] ,
10343- transactionID : transaction . transactionID ,
10344- iouReportID : expenseReport . reportID ,
10345- getCurrencyDecimals : getCurrencyDecimalsLocal ,
10346- } ) ,
10347- childReportID : transactionThread . reportID ,
10348- } ;
10349-
10350- await Onyx . merge ( `${ ONYXKEYS . COLLECTION . REPORT } ${ expenseReport . reportID } ` , expenseReport ) ;
10351- await Onyx . merge ( `${ ONYXKEYS . COLLECTION . REPORT } ${ transactionThread . reportID } ` , transactionThread ) ;
10352- await Onyx . merge ( `${ ONYXKEYS . COLLECTION . REPORT_ACTIONS } ${ expenseReport . reportID } ` , { [ iouAction . reportActionID ] : iouAction } ) ;
10353- await Onyx . merge ( `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ transaction . transactionID } ` , transaction ) ;
10354- if ( withExistingSplitChildren ) {
10355- for ( const childTransactionID of [ '9003' , '9004' ] ) {
10356- await Onyx . merge ( `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ childTransactionID } ` , {
10357- ...transaction ,
10358- transactionID : childTransactionID ,
10359- amount : 50 ,
10360- comment : { originalTransactionID : transaction . transactionID , source : CONST . IOU . TYPE . SPLIT } ,
10361- } ) ;
10362- }
10363- }
10364- await waitForBatchedUpdates ( ) ;
10365-
10366- let allTransactions : OnyxCollection < Transaction > ;
10367- let allReports : OnyxCollection < Report > ;
10368- await getOnyxData ( {
10369- key : ONYXKEYS . COLLECTION . TRANSACTION ,
10370- callback : ( value ) => {
10371- allTransactions = value ;
10372- } ,
10373- } ) ;
10374- await getOnyxData ( {
10375- key : ONYXKEYS . COLLECTION . REPORT ,
10376- callback : ( value ) => {
10377- allReports = value ;
10378- } ,
10379- } ) ;
10380-
10381- const reports = getTransactionAndExpenseReports ( asSelfDMThread ? transactionThread . reportID : expenseReport . reportID ) ;
10382- const params : UpdateSplitTransactionsParams = {
10383- allTransactionsList : allTransactions ,
10384- allReportsList : allReports ,
10385- allReportActionsList : undefined ,
10386- allReportNameValuePairsList : undefined ,
10387- transactionData : {
10388- reportID : expenseReport . reportID ,
10389- originalTransactionID : transaction . transactionID ,
10390- splitExpenses : [
10391- { transactionID : '9003' , amount : 50 , description : '' , category : '' , tags : [ '' ] , created : transaction . created , reportID : expenseReport . reportID } ,
10392- { transactionID : '9004' , amount : 50 , description : '' , category : '' , tags : [ '' ] , created : transaction . created , reportID : expenseReport . reportID } ,
10393- ] ,
10394- splitExpensesTotal : 100 ,
10395- } ,
10396- searchContext : { currentSearchHash : - 2 } ,
10397- policyCategories : undefined ,
10398- policy : undefined ,
10399- policyRecentlyUsedCategories : [ ] ,
10400- iouReport : expenseReport ,
10401- firstIOU : iouAction ,
10402- isASAPSubmitBetaEnabled : false ,
10403- currentUserPersonalDetails,
10404- transactionViolations : { } ,
10405- policyRecentlyUsedCurrencies : [ ] ,
10406- quickAction : undefined ,
10407- betas : [ CONST . BETAS . ALL ] ,
10408- allPolicyTags : undefined ,
10409- personalDetails : { [ RORY_ACCOUNT_ID ] : { accountID : RORY_ACCOUNT_ID , login : RORY_EMAIL } } ,
10410- transactionReport : reports . transactionReport ,
10411- expenseReport : reports . expenseReport ,
10412- isOffline : false ,
10413- delegateAccountID : undefined ,
10414- isTrackIntentUser : false ,
10415- getCurrencyDecimals : getCurrencyDecimalsLocal ,
10416- formatPhoneNumber,
10417- getCurrencySymbol : getCurrencySymbolLocal ,
10418- } ;
10419-
10420- return { expenseReport, iouAction, params} ;
10421- } ;
10422-
10423- /**
10424- * `@libs/API` re-declares `writeWhenReady` rather than re-exporting it, and its wrapper always forwards the
10425- * optional 5th argument. So the arity the mock records is the wrapper's, not the call site's, and asserting
10426- * with `toHaveBeenCalledWith` breaks whenever that wrapper's signature grows. Assert the arguments this flow
10427- * actually chooses - the command and the barrier - and ignore the rest.
10428- */
10429- function expectDeferredWriteFor ( command : WriteCommand ) {
10430- const deferredCall = jest . mocked ( writeWhenReady ) . mock . calls . find ( ( [ calledCommand ] ) => calledCommand === command ) ;
10431- expect ( deferredCall ) . toBeDefined ( ) ;
10432- expect ( deferredCall ?. at ( 3 ) ) . toEqual ( expect . any ( Function ) ) ;
10433- }
10434-
10435- describe ( 'split save deferred write' , ( ) => {
10436- beforeEach ( ( ) => {
10437- jest . mocked ( isSearchTopmostFullScreenRoute ) . mockReturnValue ( false ) ;
10438- } ) ;
10439-
10440- it ( 'defers the write behind a navigation barrier when saving from the split-expenses flow' , async ( ) => {
10441- // Given a split saved through the split-expenses flow
10442- const { params} = await buildSplitFlowParams ( ) ;
10443-
10444- // When the split is saved
10445- updateSplitTransactionsFromSplitExpensesFlow ( params ) ;
10446- await waitForBatchedUpdates ( ) ;
10447-
10448- // Then the write goes through writeWhenReady gated on a screen transition specifically, so its
10449- // optimistic data does not land while the press is still trying to paint, and a stray modal or
10450- // keyboard blip cannot release it early
10451- expect ( createTransitionBarrier ) . toHaveBeenCalledWith ( 'navigation' ) ;
10452- expectDeferredWriteFor ( WRITE_COMMANDS . SPLIT_TRANSACTION ) ;
10453- } ) ;
10454-
10455- it ( 'navigates back to the selfDM before deferring the write when Search is not topmost' , async ( ) => {
10456- const Navigation = jest . requireMock ( '@src/libs/Navigation/Navigation' ) ;
10457-
10458- // Given a split whose transaction thread is a selfDM, the one shape that reaches the navigate-first branch
10459- const { params} = await buildSplitFlowParams ( { asSelfDMThread : true } ) ;
10460-
10461- // When it is saved while Search is not the topmost full screen route
10462- updateSplitTransactionsFromSplitExpensesFlow ( params ) ;
10463- await waitForBatchedUpdates ( ) ;
10464-
10465- // Then the flow navigates back to the selfDM first, so the transaction thread is off screen before the
10466- // data changes under it. The write is still deferred, it just happens after that navigation.
10467- expect ( Navigation . dismissModal ) . toHaveBeenCalled ( ) ;
10468- expectDeferredWriteFor ( WRITE_COMMANDS . SPLIT_TRANSACTION ) ;
10469- } ) ;
10470-
10471- it ( 'defers the write for the same selfDM split when the Search page is topmost' , async ( ) => {
10472- const Navigation = jest . requireMock ( '@src/libs/Navigation/Navigation' ) ;
10473-
10474- // Given the identical selfDM fixture, changed only by Search being the topmost full screen route
10475- jest . mocked ( isSearchTopmostFullScreenRoute ) . mockReturnValue ( true ) ;
10476- const { params} = await buildSplitFlowParams ( { asSelfDMThread : true } ) ;
10477-
10478- // When the split is saved
10479- updateSplitTransactionsFromSplitExpensesFlow ( params ) ;
10480- await waitForBatchedUpdates ( ) ;
10481-
10482- // Then the navigate-first branch is skipped and the write goes through the barrier instead
10483- expect ( Navigation . dismissModal ) . not . toHaveBeenCalled ( ) ;
10484- expect ( createTransitionBarrier ) . toHaveBeenCalledWith ( 'navigation' ) ;
10485- expectDeferredWriteFor ( WRITE_COMMANDS . SPLIT_TRANSACTION ) ;
10486- } ) ;
10487-
10488- it ( 'defers the reverse-split write behind the same barrier' , async ( ) => {
10489- // Given an existing two-way split being collapsed back to a single expense
10490- const { params} = await buildSplitFlowParams ( { withExistingSplitChildren : true } ) ;
10491- const remainingSplit = params . transactionData . splitExpenses . at ( 0 ) ;
10492-
10493- // When the reverse split is saved from the split-expenses flow
10494- updateSplitTransactions ( {
10495- ...params ,
10496- transactionData : { ...params . transactionData , splitExpenses : remainingSplit ? [ remainingSplit ] : [ ] } ,
10497- isFromSplitExpensesFlow : true ,
10498- } ) ;
10499- await waitForBatchedUpdates ( ) ;
10500-
10501- // Then REVERT_SPLIT_TRANSACTION goes through the same fork as the creation path
10502- expect ( createTransitionBarrier ) . toHaveBeenCalledWith ( 'navigation' ) ;
10503- expectDeferredWriteFor ( WRITE_COMMANDS . REVERT_SPLIT_TRANSACTION ) ;
10504- } ) ;
10505-
10506- it ( 'writes immediately when the caller is not the split-expenses flow' , async ( ) => {
10507- // Given a direct updateSplitTransactions call, as useDeleteTransactions makes
10508- const { params} = await buildSplitFlowParams ( ) ;
10509- const writeSpy = jest . spyOn ( APIlib , 'write' ) ;
10510-
10511- // When it runs outside the split-expenses flow
10512- updateSplitTransactions ( { ...params , isFromSplitExpensesFlow : false } ) ;
10513- await waitForBatchedUpdates ( ) ;
10514-
10515- // Then the write still happens, it is just not deferred behind a barrier
10516- expect ( writeSpy ) . toHaveBeenCalledWith ( WRITE_COMMANDS . SPLIT_TRANSACTION , expect . anything ( ) , expect . anything ( ) ) ;
10517- expect ( writeWhenReady ) . not . toHaveBeenCalled ( ) ;
10518- expect ( createTransitionBarrier ) . not . toHaveBeenCalled ( ) ;
10519- writeSpy . mockRestore ( ) ;
10520- } ) ;
10521- } ) ;
10522-
1052310303describe ( 'resolveOptimisticSplitChatReportID' , ( ) => {
1052410304 it ( 'returns the existing chat and no optimistic ID when the participants already share one' , async ( ) => {
1052510305 const reportID = 'existing-split-chat' ;
0 commit comments