@@ -13843,13 +13843,6 @@ describe('actions/IOU', () => {
1384313843
1384413844 let getCurrentSearchQueryJSONSpy: jest.SpyInstance;
1384513845
13846- // `API.write` is spied on directly here rather than going through the normal
13847- // mock infrastructure because these tests need to inspect the exact
13848- // optimisticData / successData / failureData arguments passed to the
13849- // underlying network layer before any Onyx side-effects are applied.
13850- // The eslint rule `rulesdir/no-multiple-api-calls` guards against tests
13851- // that accidentally fire two real API calls; the mock below replaces the
13852- // implementation entirely so no actual request is made.
1385313846 const mockApiWrite = () => {
1385413847 // eslint-disable-next-line rulesdir/no-multiple-api-calls
1385513848 return jest.spyOn(API, 'write').mockImplementation(jest.fn());
@@ -13873,21 +13866,27 @@ describe('actions/IOU', () => {
1387313866 });
1387413867
1387513868 it('should do nothing when file is undefined', async () => {
13869+ // Given a transaction with an existing receipt
1387613870 await setupTransactionWithSnapshot(transactionID, {receipt: {source: 'original.jpg'}});
1387713871
13872+ // When replaceReceipt is called without a file
1387813873 replaceReceipt({transactionID, file: undefined, source, transactionPolicy: undefined});
1387913874 await waitForBatchedUpdates();
1388013875
13876+ // Then the receipt source remains unchanged
1388113877 const updatedTransaction = await getUpdatedTransaction(transactionID);
1388213878 expect(updatedTransaction?.receipt?.source).toBe('original.jpg');
1388313879 });
1388413880
1388513881 it('should replace the receipt of the transaction', async () => {
13882+ // Given a transaction with an existing receipt
1388613883 await setupTransactionWithSnapshot(transactionID, {receipt: {source: 'test1'}});
1388713884
13885+ // When replaceReceipt is called with a new file
1388813886 replaceReceipt({transactionID, file: createFile(), source, transactionPolicy: undefined});
1388913887 await waitForBatchedUpdates();
1389013888
13889+ // Then both the transaction and its snapshot entry reflect the new receipt
1389113890 const updatedTransaction = await getUpdatedTransaction(transactionID);
1389213891 expect(updatedTransaction?.receipt?.source).toBe(source);
1389313892 expect(updatedTransaction?.receipt?.state).toBe(CONST.IOU.RECEIPT_STATE.OPEN);
@@ -13898,11 +13897,14 @@ describe('actions/IOU', () => {
1389813897 });
1389913898
1390013899 it('should preserve receipt state when state is provided', async () => {
13900+ // Given a transaction with a receipt in SCAN_READY state
1390113901 await setupTransactionWithSnapshot(transactionID, {receipt: {source: 'test1', state: CONST.IOU.RECEIPT_STATE.SCAN_READY}});
1390213902
13903+ // When replaceReceipt is called with the same state explicitly passed
1390313904 replaceReceipt({transactionID, file: createFile(), source, state: CONST.IOU.RECEIPT_STATE.SCAN_READY, transactionPolicy: undefined});
1390413905 await waitForBatchedUpdates();
1390513906
13907+ // Then the new receipt retains the provided state instead of falling back to OPEN
1390613908 const updatedTransaction = await getUpdatedTransaction(transactionID);
1390713909 expect(updatedTransaction?.receipt?.source).toBe(source);
1390813910 expect(updatedTransaction?.receipt?.state).toBe(CONST.IOU.RECEIPT_STATE.SCAN_READY);
@@ -13913,11 +13915,14 @@ describe('actions/IOU', () => {
1391313915 });
1391413916
1391513917 it('should add receipt if it does not exist', async () => {
13918+ // Given a transaction with no receipt
1391613919 await setupTransactionWithSnapshot(transactionID);
1391713920
13921+ // When replaceReceipt is called
1391813922 replaceReceipt({transactionID, file: createFile(), source, transactionPolicy: undefined});
1391913923 await waitForBatchedUpdates();
1392013924
13925+ // Then the receipt is created with the new source on both the transaction and snapshot
1392113926 const updatedTransaction = await getUpdatedTransaction(transactionID);
1392213927 expect(updatedTransaction?.receipt?.source).toBe(source);
1392313928
@@ -13928,13 +13933,16 @@ describe('actions/IOU', () => {
1392813933 });
1392913934
1393013935 it('should optimistically set pending field for receipt', async () => {
13936+ // Given a transaction with an existing receipt
1393113937 const writeSpy = mockApiWrite();
1393213938 await setupTransactionWithSnapshot(transactionID, {receipt: OLD_RECEIPT});
1393313939
1393413940 try {
13941+ // When replaceReceipt is called
1393513942 replaceReceipt({transactionID, file: createFile(), source, transactionPolicy: undefined});
1393613943 await waitForBatchedUpdates();
1393713944
13945+ // Then the optimisticData marks the receipt field as pending UPDATE
1393813946 const onyxData = getOnyxDataFromWriteSpy(writeSpy);
1393913947 const transactionOptimistic = onyxData?.optimisticData?.find((update) => update.key === `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
1394013948 expect(transactionOptimistic?.value).toEqual(
@@ -13949,20 +13957,24 @@ describe('actions/IOU', () => {
1394913957 });
1395013958
1395113959 it('should call API.write with REPLACE_RECEIPT command and correct params', async () => {
13960+ // Given a transaction with an existing receipt
1395213961 const writeSpy = mockApiWrite();
1395313962 await setupTransactionWithSnapshot(transactionID, {receipt: OLD_RECEIPT});
1395413963
1395513964 try {
13965+ // When replaceReceipt is called
1395613966 replaceReceipt({transactionID, file: createFile(), source, transactionPolicy: undefined});
1395713967 await waitForBatchedUpdates();
1395813968
13969+ // Then API.write is invoked with the REPLACE_RECEIPT command and the correct transactionID
1395913970 expect(writeSpy).toHaveBeenCalledWith(WRITE_COMMANDS.REPLACE_RECEIPT, expect.objectContaining({transactionID}), expect.anything());
1396013971 } finally {
1396113972 writeSpy.mockRestore();
1396213973 }
1396313974 });
1396413975
1396513976 it('should compute violations when policy is paid group', async () => {
13977+ // Given a transaction and expense report linked to a paid group policy with tag definitions
1396613978 const reportID = 'replaceReceiptReportID';
1396713979 const policy = {
1396813980 ...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM),
@@ -13988,22 +14000,27 @@ describe('actions/IOU', () => {
1398814000 await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, policyTagList);
1398914001 await waitForBatchedUpdates();
1399014002
14003+ // When replaceReceipt is called with the paid group policy
1399114004 replaceReceipt({transactionID, file: createFile(), source, transactionPolicy: policy});
1399214005 await waitForBatchedUpdates();
1399314006
14007+ // Then transaction violations are computed and stored
1399414008 const violations = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
1399514009 expect(violations).toBeDefined();
1399614010 expect(Array.isArray(violations)).toBe(true);
1399714011 });
1399814012
1399914013 it('should rollback to the previous receipt in failure data', async () => {
14014+ // Given a transaction with OLD_RECEIPT
1400014015 const writeSpy = mockApiWrite();
1400114016 await setupTransactionWithSnapshot(transactionID, {receipt: OLD_RECEIPT});
1400214017
1400314018 try {
14019+ // When replaceReceipt is called
1400414020 replaceReceipt({transactionID, file: createFile(), source, transactionPolicy: undefined});
1400514021 await waitForBatchedUpdates();
1400614022
14023+ // Then the failureData restores the original receipt, clears pendingFields, and attaches errors
1400714024 const onyxData = getOnyxDataFromWriteSpy(writeSpy);
1400814025 const transactionFailure = onyxData?.failureData?.find((update) => update.key === `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
1400914026 expect(transactionFailure?.value).toEqual(
@@ -14019,13 +14036,16 @@ describe('actions/IOU', () => {
1401914036 });
1402014037
1402114038 it('should rollback the receipt to null in failure data when there was no previous receipt', async () => {
14039+ // Given a transaction with no receipt
1402214040 const writeSpy = mockApiWrite();
1402314041 await setupTransactionWithSnapshot(transactionID);
1402414042
1402514043 try {
14044+ // When replaceReceipt is called
1402614045 replaceReceipt({transactionID, file: createFile(), source, transactionPolicy: undefined});
1402714046 await waitForBatchedUpdates();
1402814047
14048+ // Then the failureData sets receipt to null since there was nothing to restore
1402914049 const onyxData = getOnyxDataFromWriteSpy(writeSpy);
1403014050 const transactionFailure = onyxData?.failureData?.find((update) => update.key === `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
1403114051 expect(transactionFailure?.value).toEqual(
@@ -14040,13 +14060,16 @@ describe('actions/IOU', () => {
1404014060 });
1404114061
1404214062 it('should clear pending fields in success data', async () => {
14063+ // Given a transaction with an existing receipt
1404314064 const writeSpy = mockApiWrite();
1404414065 await setupTransactionWithSnapshot(transactionID, {receipt: OLD_RECEIPT});
1404514066
1404614067 try {
14068+ // When replaceReceipt is called
1404714069 replaceReceipt({transactionID, file: createFile(), source, transactionPolicy: undefined});
1404814070 await waitForBatchedUpdates();
1404914071
14072+ // Then the successData clears the pending field for the receipt
1405014073 const onyxData = getOnyxDataFromWriteSpy(writeSpy);
1405114074 const transactionSuccess = onyxData?.successData?.find((update) => update.key === `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
1405214075 expect(transactionSuccess?.value).toEqual({
@@ -14058,14 +14081,17 @@ describe('actions/IOU', () => {
1405814081 });
1405914082
1406014083 it('should not include snapshot updates when there is no current search query hash', async () => {
14084+ // Given there is no active search query hash
1406114085 getCurrentSearchQueryJSONSpy.mockReturnValueOnce(null);
1406214086 const writeSpy = mockApiWrite();
1406314087 await setupTransactionWithSnapshot(transactionID, {receipt: OLD_RECEIPT});
1406414088
1406514089 try {
14090+ // When replaceReceipt is called
1406614091 replaceReceipt({transactionID, file: createFile(), source, transactionPolicy: undefined});
1406714092 await waitForBatchedUpdates();
1406814093
14094+ // Then no snapshot updates are included in either optimisticData or failureData
1406914095 const onyxData = getOnyxDataFromWriteSpy(writeSpy);
1407014096 const hasSnapshotOptimistic = onyxData?.optimisticData?.some((update) => update.key.startsWith(ONYXKEYS.COLLECTION.SNAPSHOT));
1407114097 const hasSnapshotFailure = onyxData?.failureData?.some((update) => update.key.startsWith(ONYXKEYS.COLLECTION.SNAPSHOT));
@@ -14077,13 +14103,16 @@ describe('actions/IOU', () => {
1407714103 });
1407814104
1407914105 it('should rollback the snapshot receipt in failure data when a search query hash exists', async () => {
14106+ // Given a transaction with OLD_RECEIPT and an active search snapshot
1408014107 const writeSpy = mockApiWrite();
1408114108 await setupTransactionWithSnapshot(transactionID, {receipt: OLD_RECEIPT});
1408214109
1408314110 try {
14111+ // When replaceReceipt is called
1408414112 replaceReceipt({transactionID, file: createFile(), source, transactionPolicy: undefined});
1408514113 await waitForBatchedUpdates();
1408614114
14115+ // Then the failureData restores the original receipt inside the snapshot entry
1408714116 const onyxData = getOnyxDataFromWriteSpy(writeSpy);
1408814117 const snapshotFailure = onyxData?.failureData?.find((update) => update.key === `${ONYXKEYS.COLLECTION.SNAPSHOT}${snapshotHash}`);
1408914118 const snapshotData = snapshotFailure?.value?.data as Record<string, {receipt?: unknown}> | undefined;
@@ -14094,10 +14123,12 @@ describe('actions/IOU', () => {
1409414123 });
1409514124
1409614125 it('should forward isSameReceipt and receiptState to API parameters', async () => {
14126+ // Given a transaction with an existing receipt
1409714127 const writeSpy = mockApiWrite();
1409814128 await setupTransactionWithSnapshot(transactionID, {receipt: OLD_RECEIPT});
1409914129
1410014130 try {
14131+ // When replaceReceipt is called with isSameReceipt=true and a specific receipt state
1410114132 replaceReceipt({
1410214133 transactionID,
1410314134 file: createFile(),
@@ -14108,6 +14139,7 @@ describe('actions/IOU', () => {
1410814139 });
1410914140 await waitForBatchedUpdates();
1411014141
14142+ // Then API.write receives those parameters verbatim
1411114143 expect(writeSpy).toHaveBeenCalledWith(
1411214144 WRITE_COMMANDS.REPLACE_RECEIPT,
1411314145 expect.objectContaining({
@@ -14124,6 +14156,7 @@ describe('actions/IOU', () => {
1412414156 });
1412514157
1412614158 it('should rollback transaction violations in failure data when policy is paid group', async () => {
14159+ // Given a transaction with existing violations linked to a paid group policy
1412714160 const reportID = 'replaceReceiptViolationsRollbackReportID';
1412814161 const policy = {
1412914162 ...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM),
@@ -14149,11 +14182,13 @@ describe('actions/IOU', () => {
1414914182 await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, existingViolations);
1415014183 await waitForBatchedUpdates();
1415114184
14185+ // When replaceReceipt is called with the paid group policy
1415214186 const writeSpy = mockApiWrite();
1415314187 try {
1415414188 replaceReceipt({transactionID, file: createFile(), source, transactionPolicy: policy});
1415514189 await waitForBatchedUpdates();
1415614190
14191+ // Then the failureData restores the original violations
1415714192 const onyxData = getOnyxDataFromWriteSpy(writeSpy);
1415814193 const violationsFailure = onyxData?.failureData?.find((update) => update.key === `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
1415914194 expect(violationsFailure?.value).toEqual(existingViolations);
0 commit comments