diff --git a/tests/ui/IOURequestStepScanTest.tsx b/tests/ui/IOURequestStepScanTest.tsx index 8dc4d8775991..d9d8790a5e07 100644 --- a/tests/ui/IOURequestStepScanTest.tsx +++ b/tests/ui/IOURequestStepScanTest.tsx @@ -1,176 +1,230 @@ +import {NavigationContainer} from '@react-navigation/native'; +import {act, render} from '@testing-library/react-native'; +import React from 'react'; import Onyx from 'react-native-onyx'; -import {shouldReuseInitialTransaction} from '@libs/TransactionUtils'; +import type {OnyxEntry} from 'react-native-onyx'; +import {LocaleContextProvider} from '@components/LocaleContextProvider'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; +import IOURequestStepScan from '@pages/iou/request/step/IOURequestStepScan'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Transaction} from '@src/types/onyx'; +import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; +import type {Report, Transaction} from '@src/types/onyx'; +import type {FileObject} from '@src/types/utils/Attachment'; import createRandomTransaction from '../utils/collections/transaction'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +let triggerFileSelection: ((files: FileObject[]) => void) | null = null; + +jest.mock('react-native-permissions', () => ({ + RESULTS: {GRANTED: 'granted', DENIED: 'denied', UNAVAILABLE: 'unavailable', BLOCKED: 'blocked', LIMITED: 'limited'}, + PERMISSIONS: {IOS: {CAMERA: 'ios.permission.CAMERA'}, ANDROID: {CAMERA: 'android.permission.CAMERA'}}, + check: jest.fn(() => Promise.resolve('granted')), + request: jest.fn(() => Promise.resolve('granted')), + checkLocationAccuracy: jest.fn(() => 'full'), + requestLocationAccuracy: jest.fn(() => 'full'), + checkMultiple: jest.fn(() => Promise.resolve({})), + requestMultiple: jest.fn(() => Promise.resolve({})), + checkNotifications: jest.fn(() => Promise.resolve({status: 'granted', settings: {}})), + requestNotifications: jest.fn(() => Promise.resolve({status: 'granted', settings: {}})), + openLimitedPhotoLibraryPicker: jest.fn(), + openSettings: jest.fn(), +})); + +jest.mock('@hooks/useFilesValidation', () => { + const ReactLib = require('react') as { + createElement: (type: unknown, props?: unknown, ...children: unknown[]) => unknown; + Fragment: unknown; + }; + return (callback: (files: FileObject[]) => void) => { + triggerFileSelection = callback; + return { + validateFiles: (files: FileObject[]) => callback(files), + PDFValidationComponent: ReactLib.createElement(ReactLib.Fragment), + ErrorModal: ReactLib.createElement(ReactLib.Fragment), + }; + }; +}); + +jest.mock('react-native-vision-camera', () => ({ + useCameraDevice: jest.fn(() => null), +})); + +function createMinimalReport(reportID: string, policyID: string): Report { + return { + reportID, + policyID, + ownerAccountID: 1, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + isPinned: false, + lastVisibleActionCreated: '', + lastReadTime: '', + }; +} + +function getTransactionsDraftOnyx(transactionID: string): Promise> { + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, + callback: (val) => { + resolve(val); + Onyx.disconnect(connection); + }, + }); + }); +} describe('IOURequestStepScan', () => { beforeAll(() => { - Onyx.init({ - keys: ONYXKEYS, - }); + Onyx.init({keys: ONYXKEYS}); }); - beforeEach(async () => { - jest.clearAllMocks(); - await Onyx.clear(); - await waitForBatchedUpdates(); + beforeEach(() => { + triggerFileSelection = null; }); afterEach(async () => { - await Onyx.clear(); jest.clearAllMocks(); + await Onyx.clear(); }); - it('should preserve multiple draft transactions when processing new receipts', async () => { - const transaction1 = createRandomTransaction(1); - transaction1.receipt = {source: 'file://receipt1.png', state: CONST.IOU.RECEIPT_STATE.OPEN}; - - const transaction2 = createRandomTransaction(2); - transaction2.receipt = {source: 'file://receipt2.png', state: CONST.IOU.RECEIPT_STATE.OPEN}; - - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`, transaction1); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction2.transactionID}`, transaction2); - await waitForBatchedUpdates(); - - const tx1Data = await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`, - callback: (val) => { - resolve(val); - Onyx.disconnect(connection); - }, - }); - }); - - const tx2Data = await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction2.transactionID}`, - callback: (val) => { - resolve(val); - Onyx.disconnect(connection); - }, - }); - }); - - expect(tx1Data?.transactionID).toBe(transaction1.transactionID); - expect(tx1Data?.receipt?.source).toBe('file://receipt1.png'); - expect(tx2Data?.transactionID).toBe(transaction2.transactionID); - expect(tx2Data?.receipt?.source).toBe('file://receipt2.png'); + afterAll(() => { + jest.restoreAllMocks(); }); - it('should not lose draft transactions when updating one receipt', async () => { - const transaction1 = createRandomTransaction(1); - transaction1.receipt = {source: 'file://receipt1.png'}; - - const transaction2 = createRandomTransaction(2); - transaction2.receipt = {source: 'file://receipt2.png'}; - - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`, transaction1); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction2.transactionID}`, transaction2); - await waitForBatchedUpdates(); - - const updatedTransaction1 = {...transaction1, receipt: {source: 'file://receipt1-updated.png'}}; - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`, updatedTransaction1); - await waitForBatchedUpdates(); - - const tx1Data = await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`, - callback: (val) => { - resolve(val); - Onyx.disconnect(connection); - }, - }); - }); - - const tx2Data = await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction2.transactionID}`, - callback: (val) => { - resolve(val); - Onyx.disconnect(connection); - }, - }); - }); + it('replacing receipt when editing does not clear other drafts', async () => { + const REPORT_ID = '1'; + const POLICY_ID = 'policy-1'; + const TRANSACTION_ID_1 = '101'; + const TRANSACTION_ID_2 = '102'; - expect(tx2Data?.transactionID).toBe(transaction2.transactionID); - expect(tx2Data?.receipt?.source).toBe('file://receipt2.png'); - expect(tx1Data?.transactionID).toBe(transaction1.transactionID); - expect(tx1Data?.receipt?.source).toBe('file://receipt1-updated.png'); - }); - - it('replacing receipt in multi-scan does not clear other drafts', async () => { const transaction1 = createRandomTransaction(1); + transaction1.reportID = REPORT_ID; + transaction1.transactionID = TRANSACTION_ID_1; transaction1.receipt = {source: 'file://receipt1.png', state: CONST.IOU.RECEIPT_STATE.OPEN}; const transaction2 = createRandomTransaction(2); + transaction2.reportID = REPORT_ID; + transaction2.transactionID = TRANSACTION_ID_2; transaction2.receipt = {source: 'file://receipt2.png', state: CONST.IOU.RECEIPT_STATE.OPEN}; - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction1.transactionID}`, transaction1); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction2.transactionID}`, transaction2); + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, createMinimalReport(REPORT_ID, POLICY_ID)); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID_1}`, transaction1); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID_2}`, transaction2); + }); await waitForBatchedUpdates(); - const transactions = [transaction1, transaction2]; - const shouldReuse = shouldReuseInitialTransaction(transaction1, true, 0, true, transactions); - - expect(shouldReuse).toBe(false); - - const tx2Data = await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction2.transactionID}`, - callback: (val) => { - resolve(val); - Onyx.disconnect(connection); - }, - }); + render( + + + + ['route'] + } + navigation={{} as never} + /> + + + , + ); + + await waitForBatchedUpdatesWithAct(); + + expect(triggerFileSelection).not.toBeNull(); + + const replacementFile = {name: 'replacement-receipt.png', type: 'image/png', size: 100} as FileObject; + await act(async () => { + if (!triggerFileSelection) { + return; + } + triggerFileSelection([replacementFile]); }); - - expect(tx2Data?.transactionID).toBe(transaction2.transactionID); - expect(tx2Data?.receipt?.source).toBe('file://receipt2.png'); - }); - - it('multi-scan mode preserves receipts when adding new ones', async () => { - const initialTx = createRandomTransaction(0); - initialTx.receipt = {source: 'file://initial-receipt.png', state: CONST.IOU.RECEIPT_STATE.OPEN}; - - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${initialTx.transactionID}`, initialTx); await waitForBatchedUpdates(); - const secondTx = createRandomTransaction(1); - secondTx.receipt = {source: 'file://second-receipt.png', state: CONST.IOU.RECEIPT_STATE.OPEN}; - - const transactions = [initialTx]; - const shouldReuseForSecondFile = shouldReuseInitialTransaction(initialTx, true, 1, true, transactions); + const tx2After = await getTransactionsDraftOnyx(TRANSACTION_ID_2); + expect(tx2After).toBeDefined(); + expect(tx2After?.transactionID).toBe(TRANSACTION_ID_2); + expect(tx2After?.receipt?.source).toBe('file://receipt2.png'); + }); - expect(shouldReuseForSecondFile).toBe(false); + it('multi-scan mode preserves first receipt when adding second receipt', async () => { + const REPORT_ID = '1'; + const POLICY_ID = 'policy-1'; + const TRANSACTION_ID_1 = '101'; - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${secondTx.transactionID}`, secondTx); - await waitForBatchedUpdates(); + const transaction1 = createRandomTransaction(1); + transaction1.reportID = REPORT_ID; + transaction1.transactionID = TRANSACTION_ID_1; + transaction1.receipt = {source: 'file://first-receipt.png', state: CONST.IOU.RECEIPT_STATE.OPEN}; - const tx1Data = await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${initialTx.transactionID}`, - callback: (val) => { - resolve(val); - Onyx.disconnect(connection); - }, - }); + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, createMinimalReport(REPORT_ID, POLICY_ID)); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${TRANSACTION_ID_1}`, transaction1); }); + await waitForBatchedUpdates(); - const tx2Data = await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${secondTx.transactionID}`, - callback: (val) => { - resolve(val); - Onyx.disconnect(connection); - }, - }); + render( + + + + ['route'] + } + navigation={{} as never} + isMultiScanEnabled + isStartingScan + setIsMultiScanEnabled={jest.fn()} + /> + + + , + ); + + await waitForBatchedUpdatesWithAct(); + + expect(triggerFileSelection).not.toBeNull(); + + const secondFile = {name: 'second-receipt.png', type: 'image/png', size: 200} as FileObject; + await act(async () => { + if (!triggerFileSelection) { + return; + } + triggerFileSelection([secondFile]); }); + await waitForBatchedUpdates(); - expect(tx1Data?.receipt?.source).toBe('file://initial-receipt.png'); - expect(tx2Data?.receipt?.source).toBe('file://second-receipt.png'); + const tx1After = await getTransactionsDraftOnyx(TRANSACTION_ID_1); + expect(tx1After).toBeDefined(); + expect(tx1After?.receipt?.source).toBe('file://first-receipt.png'); }); });