Skip to content

Commit e997be4

Browse files
committed
test: cover the split-save deferred write
Three tests in SplitTest.ts for the new behaviour: - saving from the Search page reserves the SEARCH channel and routes the write through deferOrExecuteWrite with shouldDeferForSearch: true - saving from a report defers without ever touching the SEARCH channel - a direct updateSplitTransactions call outside the split-expenses flow is not deferred, since there is no navigation to hide behind SplitSelfDMTest.ts needed the deferredLayoutWrite mock SplitTest.ts already uses. Without it the selfDM branch now parks its write on the DISMISS_MODAL channel and no destination screen mounts in a test to flush it, so three assertions on optimistic data failed. The mock runs the write inline; the deferral timing itself is covered by the tests above rather than mocked away.
1 parent ec03a3c commit e997be4

2 files changed

Lines changed: 158 additions & 1 deletion

File tree

tests/actions/IOUTest/SplitSelfDMTest.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ jest.mock('@src/libs/actions/Report', () => {
5757

5858
jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => jest.fn());
5959

60+
// The split-expenses save routes its API.write through deferOrExecuteWrite, which parks the write
61+
// until the destination screen lays out. No destination mounts here, so run it inline the same way
62+
// SplitTest.ts does for the other IOU flows on this primitive. The deferral timing itself is covered
63+
// by the "split save deferred write" tests in SplitTest.ts.
64+
jest.mock('@libs/deferredLayoutWrite', () => ({
65+
registerDeferredWrite: (_key: string, callback: () => void) => callback(),
66+
flushDeferredWrite: jest.fn(),
67+
cancelDeferredWrite: jest.fn(),
68+
hasDeferredWrite: () => false,
69+
getOptimisticWatchKey: () => undefined,
70+
deferOrExecuteWrite: jest.fn((apiWrite: () => void) => apiWrite()),
71+
reserveDeferredWriteChannel: jest.fn(),
72+
}));
73+
6074
jest.mock('@src/libs/SearchQueryUtils', () => {
6175
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
6276
const actual = jest.requireActual('@src/libs/SearchQueryUtils');

tests/actions/IOUTest/SplitTest.ts

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {addComment, notifyNewAction} from '@libs/actions/Report';
1111
import initSplitExpense from '@libs/actions/SplitExpenses';
1212
import {WRITE_COMMANDS} from '@libs/API/types';
1313
import {getCurrencyDecimals, getCurrencySymbol} from '@libs/CurrencyUtils';
14+
import {deferOrExecuteWrite, reserveDeferredWriteChannel} from '@libs/deferredLayoutWrite';
15+
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
1416
import {rand64} from '@libs/NumberUtils';
1517
import {getIOUActionForReportID, getIOUActionForTransactionID, getOriginalMessage, isActionOfType, isAddCommentAction, isDeletedAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
1618
import {buildOptimisticIOUReportAction, getAncestors, getReportOrDraftReport} from '@libs/ReportUtils';
@@ -105,7 +107,7 @@ jest.mock('@libs/deferredLayoutWrite', () => ({
105107
cancelDeferredWrite: jest.fn(),
106108
hasDeferredWrite: () => false,
107109
getOptimisticWatchKey: () => undefined,
108-
deferOrExecuteWrite: (apiWrite: () => void) => apiWrite(),
110+
deferOrExecuteWrite: jest.fn((apiWrite: () => void) => apiWrite()),
109111
reserveDeferredWriteChannel: jest.fn(),
110112
}));
111113
jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}})));
@@ -9490,3 +9492,144 @@ describe('startSplitBill delegateAccountID forwarding', () => {
94909492
expect(splitIOUAction?.delegateAccountID).toBe(DELEGATE_ACCOUNT_ID);
94919493
});
94929494
});
9495+
9496+
/**
9497+
* Minimal fixture for the split-expenses save flow: one expense report holding one transaction,
9498+
* with a draft that splits it in two.
9499+
*/
9500+
const buildSplitFlowParams = async () => {
9501+
const expenseReport: Report = {
9502+
...createRandomReport(9001, undefined),
9503+
type: CONST.REPORT.TYPE.EXPENSE,
9504+
};
9505+
const transaction: Transaction = {
9506+
amount: 100,
9507+
currency: 'USD',
9508+
transactionID: '9001',
9509+
reportID: expenseReport.reportID,
9510+
created: DateUtils.getDBTime(),
9511+
merchant: 'test',
9512+
};
9513+
const transactionThread: Report = {...createRandomReport(9002, undefined)};
9514+
const iouAction: ReportAction = {
9515+
...buildOptimisticIOUReportAction({
9516+
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
9517+
amount: transaction.amount,
9518+
currency: transaction.currency,
9519+
comment: '',
9520+
participants: [],
9521+
transactionID: transaction.transactionID,
9522+
iouReportID: expenseReport.reportID,
9523+
}),
9524+
childReportID: transactionThread.reportID,
9525+
};
9526+
9527+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport);
9528+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${transactionThread.reportID}`, transactionThread);
9529+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, {[iouAction.reportActionID]: iouAction});
9530+
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction);
9531+
await waitForBatchedUpdates();
9532+
9533+
let allTransactions: OnyxCollection<Transaction>;
9534+
let allReports: OnyxCollection<Report>;
9535+
await getOnyxData({
9536+
key: ONYXKEYS.COLLECTION.TRANSACTION,
9537+
callback: (value) => {
9538+
allTransactions = value;
9539+
},
9540+
});
9541+
await getOnyxData({
9542+
key: ONYXKEYS.COLLECTION.REPORT,
9543+
callback: (value) => {
9544+
allReports = value;
9545+
},
9546+
});
9547+
9548+
const reports = getTransactionAndExpenseReports(expenseReport.reportID);
9549+
const params = {
9550+
allTransactionsList: allTransactions,
9551+
allReportsList: allReports,
9552+
allReportActionsList: undefined,
9553+
allReportNameValuePairsList: undefined,
9554+
transactionData: {
9555+
reportID: expenseReport.reportID,
9556+
originalTransactionID: transaction.transactionID,
9557+
splitExpenses: [
9558+
{transactionID: '9003', amount: 50, currency: 'USD', description: '', category: '', tags: [''], created: transaction.created, reportID: expenseReport.reportID},
9559+
{transactionID: '9004', amount: 50, currency: 'USD', description: '', category: '', tags: [''], created: transaction.created, reportID: expenseReport.reportID},
9560+
],
9561+
splitExpensesTotal: 100,
9562+
},
9563+
searchContext: {currentSearchHash: -2},
9564+
policyCategories: undefined,
9565+
policy: undefined,
9566+
policyRecentlyUsedCategories: [],
9567+
iouReport: expenseReport,
9568+
firstIOU: iouAction,
9569+
isASAPSubmitBetaEnabled: false,
9570+
currentUserPersonalDetails,
9571+
transactionViolations: {},
9572+
policyRecentlyUsedCurrencies: [],
9573+
quickAction: undefined,
9574+
betas: [CONST.BETAS.ALL],
9575+
allPolicyTags: undefined,
9576+
personalDetails: {[RORY_ACCOUNT_ID]: {accountID: RORY_ACCOUNT_ID, login: RORY_EMAIL}},
9577+
transactionReport: reports.transactionReport,
9578+
expenseReport: reports.expenseReport,
9579+
isOffline: false,
9580+
delegateAccountID: undefined,
9581+
isTrackIntentUser: false,
9582+
};
9583+
9584+
return {expenseReport, iouAction, params};
9585+
};
9586+
9587+
describe('split save deferred write', () => {
9588+
beforeEach(() => {
9589+
jest.mocked(isSearchTopmostFullScreenRoute).mockReturnValue(false);
9590+
});
9591+
9592+
it('reserves the SEARCH channel and defers the write when saving from the Search page', async () => {
9593+
// Given a split saved while the Search page is the topmost full screen route
9594+
jest.mocked(isSearchTopmostFullScreenRoute).mockReturnValue(true);
9595+
const {expenseReport, iouAction, params} = await buildSplitFlowParams();
9596+
9597+
// When the split is saved
9598+
updateSplitTransactionsFromSplitExpensesFlow(params);
9599+
await waitForBatchedUpdates();
9600+
9601+
// Then the SEARCH channel is reserved before navigating, so a flush fired by the
9602+
// destination's layout is remembered rather than dropped
9603+
expect(reserveDeferredWriteChannel).toHaveBeenCalledWith(CONST.DEFERRED_LAYOUT_WRITE_KEYS.SEARCH);
9604+
9605+
// And the write is routed through the deferral rather than executed inline
9606+
expect(deferOrExecuteWrite).toHaveBeenCalledWith(expect.any(Function), expect.objectContaining({shouldDeferForSearch: true}));
9607+
expect(expenseReport.reportID).toBeTruthy();
9608+
expect(iouAction.reportActionID).toBeTruthy();
9609+
});
9610+
9611+
it('reserves the DISMISS_MODAL channel with the destination report when saving from a report', async () => {
9612+
// Given a split saved from a report rather than the Search page
9613+
const {params} = await buildSplitFlowParams();
9614+
9615+
// When the split is saved
9616+
updateSplitTransactionsFromSplitExpensesFlow(params);
9617+
await waitForBatchedUpdates();
9618+
9619+
// Then the write is still deferred, but never onto the SEARCH channel
9620+
expect(deferOrExecuteWrite).toHaveBeenCalledWith(expect.any(Function), expect.objectContaining({shouldDeferForSearch: false}));
9621+
expect(reserveDeferredWriteChannel).not.toHaveBeenCalledWith(CONST.DEFERRED_LAYOUT_WRITE_KEYS.SEARCH);
9622+
});
9623+
9624+
it('writes immediately when the caller is not the split-expenses flow', async () => {
9625+
// Given a direct updateSplitTransactions call, as useDeleteTransactions makes
9626+
const {params} = await buildSplitFlowParams();
9627+
9628+
// When it runs outside the split-expenses flow
9629+
updateSplitTransactions({...params, isFromSplitExpensesFlow: false});
9630+
await waitForBatchedUpdates();
9631+
9632+
// Then there is no navigation to hide behind, so the write is not deferred
9633+
expect(deferOrExecuteWrite).not.toHaveBeenCalled();
9634+
});
9635+
});

0 commit comments

Comments
 (0)