diff --git a/tests/actions/AppTest.ts b/tests/actions/AppTest.ts index 47da82d340af..37a675885c27 100644 --- a/tests/actions/AppTest.ts +++ b/tests/actions/AppTest.ts @@ -21,6 +21,7 @@ import type {MockFetch} from '../utils/TestHelper'; import * as App from '../../src/libs/actions/App'; import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; +import createMock from '../utils/createMock'; import getOnyxValue from '../utils/getOnyxValue'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -245,42 +246,42 @@ describe('actions/App', () => { }); it('should filter out undefined policies', () => { - const policies = { + const policies = createMock>({ policy1: {id: 'policy1', name: 'Policy 1'}, policy2: undefined, policy3: {id: 'policy3', name: 'Policy 3'}, - } as unknown as OnyxCollection; + }); const result = App.getNonOptimisticPolicyIDs(policies); expect(result).toEqual(['policy1', 'policy3']); }); it('should filter out policies with pendingAction ADD', () => { - const policies = { + const policies = createMock>({ policy1: {id: 'policy1', name: 'Policy 1', pendingAction: 'add'}, policy2: {id: 'policy2', name: 'Policy 2'}, policy3: {id: 'policy3', name: 'Policy 3', pendingAction: 'update'}, - } as unknown as OnyxCollection; + }); const result = App.getNonOptimisticPolicyIDs(policies); expect(result).toEqual(['policy2', 'policy3']); }); it('should return IDs for all valid non-optimistic policies', () => { - const policies = { + const policies = createMock>({ policy1: {id: 'policy1', name: 'Policy 1'}, policy2: {id: 'policy2', name: 'Policy 2'}, policy3: {id: 'policy3', name: 'Policy 3'}, - } as unknown as OnyxCollection; + }); const result = App.getNonOptimisticPolicyIDs(policies); expect(result).toEqual(['policy1', 'policy2', 'policy3']); }); it('should include policies with other pendingAction values', () => { - const policies = { + const policies = createMock>({ policy1: {id: 'policy1', name: 'Policy 1', pendingAction: 'update'}, policy2: {id: 'policy2', name: 'Policy 2', pendingAction: 'delete'}, policy3: {id: 'policy3', name: 'Policy 3', pendingAction: null}, policy4: {id: 'policy4', name: 'Policy 4', pendingAction: undefined}, - } as unknown as OnyxCollection; + }); const result = App.getNonOptimisticPolicyIDs(policies); expect(result).toEqual(['policy1', 'policy2', 'policy3', 'policy4']); }); diff --git a/tests/actions/IOU/SearchUpdateTest.ts b/tests/actions/IOU/SearchUpdateTest.ts index c6313116f52b..ba051382a5f0 100644 --- a/tests/actions/IOU/SearchUpdateTest.ts +++ b/tests/actions/IOU/SearchUpdateTest.ts @@ -19,6 +19,7 @@ import Onyx from 'react-native-onyx'; import currencyList from '../../unit/currencyList.json'; import {createRandomReport} from '../../utils/collections/reports'; import createRandomTransaction from '../../utils/collections/transaction'; +import createMock from '../../utils/createMock'; import {getGlobalFetchMock} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; @@ -140,7 +141,7 @@ describe('actions/IOU', () => { const transaction = { ...createRandomTransaction(1), }; - const currentSearchQueryJSON = { + const currentSearchQueryJSON = createMock({ type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, @@ -181,7 +182,7 @@ describe('actions/IOU', () => { hash: 939629734, recentSearchHash: 1023339253, similarSearchHash: 1855682507, - } as SearchQueryJSON; + }); const iouReport: Report = { ...createRandomReport(2, undefined), type: CONST.REPORT.TYPE.EXPENSE, @@ -202,7 +203,7 @@ describe('actions/IOU', () => { const transaction = { ...createRandomTransaction(1), }; - const currentSearchQueryJSON = { + const currentSearchQueryJSON = createMock({ type: CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, @@ -243,7 +244,7 @@ describe('actions/IOU', () => { inputQuery: 'sortBy:date sortOrder:desc type:expense-report action:approve to:20671314', recentSearchHash: 244251677, similarSearchHash: 1539858783, - } as SearchQueryJSON; + }); const iouReport: Report = { ...createRandomReport(2, undefined), type: CONST.REPORT.TYPE.EXPENSE, @@ -265,7 +266,7 @@ describe('actions/IOU', () => { ...createRandomTransaction(1), reimbursable: true, }; - const currentSearchQueryJSON = { + const currentSearchQueryJSON = createMock({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.DESC, @@ -311,7 +312,7 @@ describe('actions/IOU', () => { inputQuery: 'sortBy:date sortOrder:desc type:expense groupBy:from status:drafts,outstanding reimbursable:yes', recentSearchHash: 1043581824, similarSearchHash: 1832274510, - } as SearchQueryJSON; + }); const iouReport: Report = { ...createRandomReport(2, undefined), @@ -334,7 +335,7 @@ describe('actions/IOU', () => { ...createRandomTransaction(1), }; const policyID = '12345'; - const currentSearchQueryJSON = { + const currentSearchQueryJSON = createMock({ type: 'expense', sortBy: 'date', sortOrder: 'desc', @@ -357,7 +358,7 @@ describe('actions/IOU', () => { isDefault: true, }, ], - } as unknown as SearchQueryJSON; + }); // When the IOU report has a matching policyID, it should return true const matchingIOUReport: Report = { diff --git a/tests/actions/IOUTest/HoldTest.ts b/tests/actions/IOUTest/HoldTest.ts index 6ee89bb9ab13..584807792444 100644 --- a/tests/actions/IOUTest/HoldTest.ts +++ b/tests/actions/IOUTest/HoldTest.ts @@ -25,7 +25,9 @@ import Onyx from 'react-native-onyx'; import type {MockFetch} from '../../utils/TestHelper'; import createRandomPolicy from '../../utils/collections/policies'; -import {getCurrencyDecimalsLocal, getGlobalFetchMock} from '../../utils/TestHelper'; +import createMock from '../../utils/createMock'; +import {createGlobalFetchMock, getCurrencyDecimalsLocal} from '../../utils/TestHelper'; +import {hasDefinedProperty, isObject} from '../../utils/typeGuards'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; const topMostReportID = '23423423'; @@ -71,6 +73,7 @@ const RORY_ACCOUNT_ID = 3; OnyxUpdateManager(); describe('actions/IOU/Hold', () => { + let mockFetch: MockFetch; beforeAll(() => { Onyx.init({ keys: ONYXKEYS, @@ -84,11 +87,10 @@ describe('actions/IOU/Hold', () => { return waitForBatchedUpdates(); }); - let mockFetch: MockFetch; beforeEach(() => { jest.clearAllTimers(); - global.fetch = getGlobalFetchMock(); - mockFetch = fetch as MockFetch; + mockFetch = createGlobalFetchMock(); + global.fetch = mockFetch; return Onyx.clear().then(waitForBatchedUpdates); }); @@ -317,7 +319,7 @@ describe('actions/IOU/Hold', () => { }); }); }) - .then(() => mockFetch?.resume?.()); + .then(() => mockFetch.resume()); }); test('should invoke navigation for each transaction when isOffline is true', () => { @@ -395,7 +397,7 @@ describe('actions/IOU/Hold', () => { .then(() => { // Navigation should be called once for each transaction (putOnHold called for each) expect(Navigation.setNavigationActionToMicrotaskQueue).toHaveBeenCalledTimes(2); - return mockFetch?.resume?.(); + return mockFetch.resume(); }); }); }); @@ -585,7 +587,7 @@ describe('actions/IOU/Hold', () => { }) .then(() => { mockFetch.fail(); - mockFetch?.resume?.(); + mockFetch.resume(); unholdRequest( transaction.transactionID, transactionThread.reportID, @@ -649,11 +651,11 @@ describe('actions/IOU/Hold', () => { reimbursableTotal: overrides.reimbursableTotal ?? overrides.total - overrides.nonReimbursableTotal, unheldReimbursableTotal: overrides.unheldReimbursableTotal ?? (overrides.unheldTotal ?? 0) - (overrides.unheldNonReimbursableTotal ?? 0), }; - const chatReport: Report = { + const chatReport = createMock({ reportID: '99', iouReportID: iouReport.reportID, lastVisibleActionCreated: '2026-01-01 00:00:00.000', - } as Report; + }); const heldAmount = overrides.heldAmount ?? 0; const heldTransaction = heldAmount > 0 ? buildHeldTransaction(iouReport.reportID, heldAmount) : undefined; @@ -737,8 +739,8 @@ describe('actions/IOU/Hold', () => { (entry) => entry.onyxMethod === Onyx.METHOD.MERGE && entry.key === `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, ); const totalsRestore = restorationEntries.find((entry) => { - const value = entry.value as Partial | undefined; - return value?.total !== undefined || value?.nonReimbursableTotal !== undefined; + const value = entry.value; + return isObject(value) && (hasDefinedProperty(value, 'total') || hasDefinedProperty(value, 'nonReimbursableTotal')); }); expect(totalsRestore?.value).toEqual({total: 300, nonReimbursableTotal: 50, reimbursableTotal: 250}); }); @@ -766,8 +768,12 @@ describe('actions/IOU/Hold', () => { getCurrencyDecimals: getCurrencyDecimalsLocal, }); const totalsUpdates = result.optimisticData.filter((entry) => { - const value = entry.value as Partial | undefined; - return entry.key === `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}` && (value?.total !== undefined || value?.nonReimbursableTotal !== undefined); + const value = entry.value; + return ( + entry.key === `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}` && + isObject(value) && + (hasDefinedProperty(value, 'total') || hasDefinedProperty(value, 'nonReimbursableTotal')) + ); }); expect(totalsUpdates).toEqual([]); }); @@ -795,8 +801,12 @@ describe('actions/IOU/Hold', () => { getCurrencyDecimals: getCurrencyDecimalsLocal, }); const totalsUpdates = result.optimisticData.filter((entry) => { - const value = entry.value as Partial | undefined; - return entry.key === `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}` && (value?.total !== undefined || value?.nonReimbursableTotal !== undefined); + const value = entry.value; + return ( + entry.key === `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}` && + isObject(value) && + (hasDefinedProperty(value, 'total') || hasDefinedProperty(value, 'nonReimbursableTotal')) + ); }); expect(totalsUpdates).toEqual([]); }); diff --git a/tests/actions/IOUTest/SendInvoiceTest.ts b/tests/actions/IOUTest/SendInvoiceTest.ts index d3d629e87569..014c00f1c4e1 100644 --- a/tests/actions/IOUTest/SendInvoiceTest.ts +++ b/tests/actions/IOUTest/SendInvoiceTest.ts @@ -12,7 +12,7 @@ import * as API from '@src/libs/API'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTagLists, RecentlyUsedCategories, RecentlyUsedTags, Report} from '@src/types/onyx'; import type {Participant as IOUParticipant} from '@src/types/onyx/IOU'; -import type {InvoiceReceiver} from '@src/types/onyx/Report'; +import type {InvoiceReceiver, Participant as ReportParticipant, Participants} from '@src/types/onyx/Report'; import type Transaction from '@src/types/onyx/Transaction'; import type {OnyxEntry} from 'react-native-onyx'; @@ -25,9 +25,10 @@ import type {MockFetch} from '../../utils/TestHelper'; import * as InvoiceData from '../../data/Invoice'; import createRandomPolicy from '../../utils/collections/policies'; import createRandomTransaction from '../../utils/collections/transaction'; +import createMock from '../../utils/createMock'; import getOnyxValue from '../../utils/getOnyxValue'; import initCurrencyListContext from '../../utils/initCurrencyListContext'; -import {formatPhoneNumber, getCurrencyDecimalsLocal, getGlobalFetchMock} from '../../utils/TestHelper'; +import {createGlobalFetchMock, formatPhoneNumber, getCurrencyDecimalsLocal} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; const topMostReportID = '23423423'; @@ -70,6 +71,7 @@ function isRecord(input: unknown): input is Record { OnyxUpdateManager(); describe('actions/SendInvoice', () => { let currencyListProvider: RenderAPI; + let mockFetch: MockFetch; beforeAll(() => { Onyx.init({keys: ONYXKEYS}); @@ -78,10 +80,9 @@ describe('actions/SendInvoice', () => { return waitForBatchedUpdates(); }); - let mockFetch: MockFetch; beforeEach(async () => { jest.clearAllTimers(); - mockFetch = getGlobalFetchMock() as unknown as MockFetch; + mockFetch = createGlobalFetchMock(); global.fetch = mockFetch; await Onyx.clear(); currencyListProvider = await initCurrencyListContext({ @@ -148,7 +149,7 @@ describe('actions/SendInvoice', () => { baseSenderPolicyTags = (await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${baseSenderPolicyID}`)) ?? {}; }); - const baseTransaction = { + const baseTransaction = createMock({ transactionID: 'transaction_base', reportID: 'report_base', amount: 100, @@ -156,29 +157,29 @@ describe('actions/SendInvoice', () => { created: '2024-02-01', merchant: 'Test Merchant', participants: baseParticipants, - }; + }); - const existingInvoiceChatReportFixture: OnyxEntry = { + const existingInvoiceChatReportFixture = createMock({ reportID: 'invoice_chat_123', chatType: CONST.REPORT.CHAT_TYPE.INVOICE, type: CONST.REPORT.TYPE.CHAT, - participants: { + participants: createMock({ // eslint-disable-next-line @typescript-eslint/naming-convention - '123': { + 123: createMock({ role: CONST.REPORT.ROLE.MEMBER, notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, - }, + }), // eslint-disable-next-line @typescript-eslint/naming-convention - '456': { + 456: createMock({ role: CONST.REPORT.ROLE.MEMBER, notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, - }, - }, - invoiceReceiver: { + }), + }), + invoiceReceiver: createMock({ type: 'individual', accountID: 456, - }, - }; + }), + }); it('should merge policyRecentlyUsedCategories when provided', () => { const currentUserAccountID = 123; @@ -186,7 +187,7 @@ describe('actions/SendInvoice', () => { // When: Call getSendInvoiceInformation with policyRecentlyUsedCategories const result = getSendInvoiceInformation({ - transaction: baseTransaction as OnyxEntry, + transaction: baseTransaction, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -214,7 +215,7 @@ describe('actions/SendInvoice', () => { const currentUserAccountID = 123; const result = getSendInvoiceInformation({ - transaction: {...baseTransaction, currency: CONST.CURRENCY.EUR} as OnyxEntry, + transaction: {...baseTransaction, currency: CONST.CURRENCY.EUR}, currentUserAccountID, policyRecentlyUsedCurrencies: initialCurrencies, invoiceChatReport: undefined, @@ -252,7 +253,7 @@ describe('actions/SendInvoice', () => { }, }; - const mockPolicyTagList = { + const mockPolicyTagList = createMock({ tagList: { name: 'tagList', orderWeight: 0, @@ -264,22 +265,22 @@ describe('actions/SendInvoice', () => { }, }, }, - }; + }); // When: Call getSendInvoiceInformation const result = getSendInvoiceInformation({ - transaction: baseTransaction as OnyxEntry, + transaction: baseTransaction, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, receiptFile: undefined, policy: mockPolicy, - policyTagList: mockPolicyTagList as OnyxEntry, + policyTagList: mockPolicyTagList, policyCategories: mockPolicyCategories, companyName: 'Test Company Inc.', companyWebsite: 'https://testcompany.com', policyRecentlyUsedCategories: ['Services', 'Consulting'], - senderPolicyTags: mockPolicyTagList as PolicyTagLists, + senderPolicyTags: mockPolicyTagList, formatPhoneNumber, delegateAccountID: undefined, getCurrencyDecimals: getCurrencyDecimalsLocal, @@ -317,7 +318,7 @@ describe('actions/SendInvoice', () => { const currentUserAccountID = 123; const result = getSendInvoiceInformation({ - transaction: baseTransaction as OnyxEntry, + transaction: baseTransaction, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -349,13 +350,13 @@ describe('actions/SendInvoice', () => { it('should not set report loading state in failure data for existing invoice chat report', () => { const currentUserAccountID = 123; - const transaction: OnyxEntry = { + const transaction = createMock({ ...baseTransaction, participants: [ {accountID: 123, isSender: true, policyID: 'workspace_456'}, {accountID: 456, isSender: false}, ], - }; + }); const result = getSendInvoiceInformation({ transaction, @@ -396,7 +397,7 @@ describe('actions/SendInvoice', () => { mockPolicy.id = 'workspace_test'; const result = getSendInvoiceInformation({ - transaction: baseTransaction as OnyxEntry, + transaction: baseTransaction, currentUserAccountID: 123, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -418,8 +419,8 @@ describe('actions/SendInvoice', () => { let delegateAccountID: number | undefined; if (isRecord(reportActionValue) && result.reportActionID in reportActionValue) { const action = reportActionValue[result.reportActionID]; - if (typeof action === 'object' && action !== null && 'delegateAccountID' in action) { - delegateAccountID = (action as {delegateAccountID?: number}).delegateAccountID; + if (isRecord(action) && typeof action.delegateAccountID === 'number') { + delegateAccountID = action.delegateAccountID; } } expect(delegateAccountID).toBe(DELEGATE_ACCOUNT_ID); @@ -429,13 +430,13 @@ describe('actions/SendInvoice', () => { it('should return correct invoice information with existing chat report', () => { const currentUserAccountID = 123; - const transaction: OnyxEntry = { + const transaction = createMock({ ...baseTransaction, participants: [ {accountID: 123, isSender: true, policyID: 'workspace_456'}, {accountID: 456, isSender: false}, ], - }; + }); // When: Call getSendInvoiceInformation with existing chat report const result = getSendInvoiceInformation({ @@ -478,7 +479,7 @@ describe('actions/SendInvoice', () => { // When: Call getSendInvoiceInformation with receipt const result = getSendInvoiceInformation({ - transaction: baseTransaction as OnyxEntry, + transaction: baseTransaction, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -507,7 +508,7 @@ describe('actions/SendInvoice', () => { it('should handle missing transaction data gracefully', () => { // Given: Minimal transaction data - const mockTransaction = { + const mockTransaction = createMock({ transactionID: 'transaction_minimal', reportID: 'report_minimal', amount: 100, @@ -524,13 +525,13 @@ describe('actions/SendInvoice', () => { isSender: false, }, ], - }; + }); const currentUserAccountID = 123; // When: Call getSendInvoiceInformation with minimal data const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: mockTransaction, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -562,7 +563,7 @@ describe('actions/SendInvoice', () => { const currentUserAccountID = 123; const result = getSendInvoiceInformation({ - transaction: baseTransaction as OnyxEntry, + transaction: baseTransaction, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -590,37 +591,35 @@ describe('actions/SendInvoice', () => { const existingReportID = 'existing_invoice_chat'; const receiverAccountID = 456; - const existingInvoiceChatReport = { + const existingInvoiceChatReport = createMock({ reportID: existingReportID, chatType: CONST.REPORT.CHAT_TYPE.INVOICE, type: CONST.REPORT.TYPE.CHAT, - participants: { + participants: createMock({ // eslint-disable-next-line @typescript-eslint/naming-convention - '123': { - accountID: 123, + 123: createMock({ role: CONST.REPORT.ROLE.MEMBER, notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, - }, + }), // eslint-disable-next-line @typescript-eslint/naming-convention - '456': { - accountID: receiverAccountID, + 456: createMock({ role: CONST.REPORT.ROLE.MEMBER, notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, - }, - }, + }), + }), invoiceReceiver: { type: CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL, accountID: receiverAccountID, }, - }; + }); const currentUserAccountID = 123; const result = getSendInvoiceInformation({ - transaction: baseTransaction as OnyxEntry, + transaction: baseTransaction, currentUserAccountID, policyRecentlyUsedCurrencies: [], - invoiceChatReport: existingInvoiceChatReport as OnyxEntry, + invoiceChatReport: existingInvoiceChatReport, invoiceChatReportID: preGeneratedReportID, receiptFile: undefined, policy: undefined, @@ -665,7 +664,7 @@ describe('actions/SendInvoice', () => { [tagListName]: ['Marketing'], }; - const mockTransaction = { + const mockTransaction = createMock({ transactionID: 'transaction_tags_test', reportID: 'report_tags_test', amount: 100, @@ -677,11 +676,11 @@ describe('actions/SendInvoice', () => { {accountID: 123, isSender: true, policyID}, {accountID: 456, isSender: false}, ], - }; + }); // When: Call getSendInvoiceInformation with senderPolicyTags read from Onyx const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: mockTransaction, currentUserAccountID: 123, policyRecentlyUsedCurrencies: [], policyRecentlyUsedTags, @@ -717,7 +716,7 @@ describe('actions/SendInvoice', () => { const senderPolicyTags = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); - const mockTransaction = { + const mockTransaction = createMock({ transactionID: 'transaction_no_tags', reportID: 'report_no_tags', amount: 100, @@ -728,11 +727,11 @@ describe('actions/SendInvoice', () => { {accountID: 123, isSender: true, policyID}, {accountID: 456, isSender: false}, ], - }; + }); // When: Call getSendInvoiceInformation without a tag on the transaction const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: mockTransaction, currentUserAccountID: 123, policyRecentlyUsedCurrencies: [], senderPolicyTags: senderPolicyTags ?? {}, @@ -796,7 +795,7 @@ describe('actions/SendInvoice', () => { const initialCurrencies: string[] = []; await Onyx.set(ONYXKEYS.RECENTLY_USED_CURRENCIES, initialCurrencies); - mockFetch?.pause?.(); + mockFetch.pause(); sendInvoice({ currentUserAccountID: 1, transaction, @@ -807,8 +806,8 @@ describe('actions/SendInvoice', () => { getCurrencyDecimals: getCurrencyDecimalsLocal, }); - mockFetch?.fail?.(); - mockFetch?.resume?.(); + mockFetch.fail(); + mockFetch.resume(); await waitForBatchedUpdates(); await new Promise((resolve) => { diff --git a/tests/actions/IOUTest/UpdateMoneyRequestTest.ts b/tests/actions/IOUTest/UpdateMoneyRequestTest.ts index b7eb17424b82..1104a4868a0b 100644 --- a/tests/actions/IOUTest/UpdateMoneyRequestTest.ts +++ b/tests/actions/IOUTest/UpdateMoneyRequestTest.ts @@ -28,7 +28,7 @@ import type {Attendee} from '@src/types/onyx/IOU'; import type {CurrentUserPersonalDetails} from '@src/types/onyx/PersonalDetails'; import type Transaction from '@src/types/onyx/Transaction'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {NullishDeep, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import {format} from 'date-fns'; @@ -41,8 +41,9 @@ import createPersonalDetails from '../../utils/collections/personalDetails'; import createRandomPolicy, {createCategoryTaxExpenseRules} from '../../utils/collections/policies'; import {createRandomReport} from '../../utils/collections/reports'; import createRandomTransaction from '../../utils/collections/transaction'; +import createMock from '../../utils/createMock'; import getOnyxValue from '../../utils/getOnyxValue'; -import {getCurrencyDecimalsLocal, getGlobalFetchMock} from '../../utils/TestHelper'; +import {createGlobalFetchMock, getCurrencyDecimalsLocal} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; const topMostReportID = '23423423'; @@ -110,6 +111,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { avatar: 'https://example.com/avatar.jpg', }; + let mockFetch: MockFetch; beforeAll(() => { Onyx.init({ keys: ONYXKEYS, @@ -126,11 +128,10 @@ describe('actions/IOU/UpdateMoneyRequest', () => { return waitForBatchedUpdates(); }); - let mockFetch: MockFetch; beforeEach(() => { jest.clearAllTimers(); - global.fetch = getGlobalFetchMock(); - mockFetch = fetch as MockFetch; + mockFetch = createGlobalFetchMock(); + global.fetch = mockFetch; return Onyx.clear().then(waitForBatchedUpdates); }); @@ -419,7 +420,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); - mockFetch?.pause?.(); + mockFetch.pause(); updateMoneyRequestAmountAndCurrency({ transactionID: fakeTransaction.transactionID, @@ -454,8 +455,8 @@ describe('actions/IOU/UpdateMoneyRequest', () => { }); await waitForBatchedUpdates(); - mockFetch?.succeed?.(); - await mockFetch?.resume?.(); + mockFetch.succeed(); + await mockFetch.resume(); const updatedTransaction = await new Promise>((resolve) => { const connection = Onyx.connect({ @@ -491,7 +492,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); - mockFetch?.pause?.(); + mockFetch.pause(); updateMoneyRequestAmountAndCurrency({ transactionID: fakeTransaction.transactionID, @@ -526,8 +527,8 @@ describe('actions/IOU/UpdateMoneyRequest', () => { }); await waitForBatchedUpdates(); - mockFetch?.fail?.(); - await mockFetch?.resume?.(); + mockFetch.fail(); + await mockFetch.resume(); const updatedTransaction = await new Promise>((resolve) => { const connection = Onyx.connect({ @@ -572,19 +573,32 @@ describe('actions/IOU/UpdateMoneyRequest', () => { const {onyxData} = getUpdateTrackExpenseParams(transactionID, transactionThreadReport.reportID, {amount: 20000}, createRandomPolicy(1), undefined, snapshotHash); const snapshotKey = `${ONYXKEYS.COLLECTION.SNAPSHOT}${snapshotHash}` as const; - const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}` as const; + const transactionKey: keyof SearchResults['data'] = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; + type SnapshotUpdate = Extract, {onyxMethod: typeof Onyx.METHOD.SET | typeof Onyx.METHOD.MERGE}>; - const optimisticSnapshot = onyxData.optimisticData?.find((update) => update.key === snapshotKey)?.value as OnyxEntry; - expect(optimisticSnapshot?.data?.[transactionKey]).toMatchObject({ + const optimisticSnapshot = onyxData.optimisticData?.find( + (update): update is SnapshotUpdate => update.key === snapshotKey && (update.onyxMethod === Onyx.METHOD.SET || update.onyxMethod === Onyx.METHOD.MERGE), + ); + const optimisticSnapshotData: NullishDeep | null | undefined = optimisticSnapshot?.value?.data; + const optimisticTransaction = optimisticSnapshotData?.[transactionKey]; + expect(optimisticTransaction).toMatchObject({ modifiedAmount: -20000, pendingFields: {amount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, }); - const successSnapshot = onyxData.successData?.find((update) => update.key === snapshotKey)?.value as OnyxEntry; - expect(successSnapshot?.data?.[transactionKey]).toEqual({pendingFields: {amount: null}}); + const successSnapshot = onyxData.successData?.find( + (update): update is SnapshotUpdate => update.key === snapshotKey && (update.onyxMethod === Onyx.METHOD.SET || update.onyxMethod === Onyx.METHOD.MERGE), + ); + const successSnapshotData: NullishDeep | null | undefined = successSnapshot?.value?.data; + const successTransaction = successSnapshotData?.[transactionKey]; + expect(successTransaction).toEqual({pendingFields: {amount: null}}); - const failureSnapshot = onyxData.failureData?.find((update) => update.key === snapshotKey)?.value as OnyxEntry; - expect(failureSnapshot?.data?.[transactionKey]).toMatchObject({ + const failureSnapshot = onyxData.failureData?.find( + (update): update is SnapshotUpdate => update.key === snapshotKey && (update.onyxMethod === Onyx.METHOD.SET || update.onyxMethod === Onyx.METHOD.MERGE), + ); + const failureSnapshotData: NullishDeep | null | undefined = failureSnapshot?.value?.data; + const failureTransaction = failureSnapshotData?.[transactionKey]; + expect(failureTransaction).toMatchObject({ transactionID, amount: 10000, pendingFields: {amount: null}, @@ -1098,14 +1112,14 @@ describe('actions/IOU/UpdateMoneyRequest', () => { }; const fakePolicy = createRandomPolicy(Number(policyID)); - const transactionThreadReport = { + const transactionThreadReport = createMock({ reportID: transactionThreadReportID, type: CONST.REPORT.TYPE.EXPENSE, - } as Report; - const parentReport = { + }); + const parentReport = createMock({ reportID: parentReportID, type: CONST.REPORT.TYPE.IOU, - } as Report; + }); const recentWaypoints: RecentWaypoint[] = []; await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, fakeTransaction); @@ -1113,7 +1127,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, transactionThreadReport); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, parentReport); - mockFetch?.pause?.(); + mockFetch.pause(); // When updating the money request with distance and waypoints updateMoneyRequestDistance({ @@ -1141,7 +1155,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { getCurrencySymbol, }); - mockFetch?.resume?.(); + mockFetch.resume(); await waitForBatchedUpdates(); @@ -1193,14 +1207,14 @@ describe('actions/IOU/UpdateMoneyRequest', () => { ]; const fakePolicy = createRandomPolicy(Number(policyID)); - const transactionThreadReport = { + const transactionThreadReport = createMock({ reportID: transactionThreadReportID, type: CONST.REPORT.TYPE.EXPENSE, - } as Report; - const parentReport = { + }); + const parentReport = createMock({ reportID: parentReportID, type: CONST.REPORT.TYPE.IOU, - } as Report; + }); await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, fakeTransaction); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); @@ -1210,7 +1224,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { await Onyx.merge(ONYXKEYS.NVP_RECENT_WAYPOINTS, recentWaypoints); // Simulate a failed request - this will cause failureData to be applied - mockFetch?.fail?.(); + mockFetch.fail(); // When updating the money request WITHOUT distance (only waypoints) updateMoneyRequestDistance({ @@ -1317,21 +1331,21 @@ describe('actions/IOU/UpdateMoneyRequest', () => { }; const fakePolicy = createRandomPolicy(Number(policyID)); - const transactionThreadReport = { + const transactionThreadReport = createMock({ reportID: transactionThreadReportID, type: CONST.REPORT.TYPE.EXPENSE, - } as Report; - const parentReport = { + }); + const parentReport = createMock({ reportID: parentReportID, type: CONST.REPORT.TYPE.IOU, - } as Report; + }); await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, fakeTransaction); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, transactionThreadReport); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, parentReport); - mockFetch?.pause?.(); + mockFetch.pause(); // First update: Add more waypoints to the expense updateMoneyRequestDistance({ @@ -1359,7 +1373,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { getCurrencySymbol, }); - mockFetch?.resume?.(); + mockFetch.resume(); await waitForBatchedUpdates(); // Verify the transaction was updated with complete route information @@ -1398,14 +1412,14 @@ describe('actions/IOU/UpdateMoneyRequest', () => { }; const fakePolicy = createRandomPolicy(Number(policyID)); - const transactionThreadReport = { + const transactionThreadReport = createMock({ reportID: transactionThreadReportID, type: CONST.REPORT.TYPE.EXPENSE, - } as Report; - const parentReport = { + }); + const parentReport = createMock({ reportID: parentReportID, type: CONST.REPORT.TYPE.IOU, - } as Report; + }); await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, fakeTransaction); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); @@ -1858,7 +1872,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { waypoints: {}, }, }; - const fakeThreadReport = {reportID: transactionThreadReportID, type: CONST.REPORT.TYPE.CHAT} as Report; + const fakeThreadReport = createMock({reportID: transactionThreadReportID, type: CONST.REPORT.TYPE.CHAT}); const fakePolicy = createRandomPolicy(Number(1)); await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, fakeTransaction); @@ -1899,11 +1913,11 @@ describe('actions/IOU/UpdateMoneyRequest', () => { waypoints: {}, }, }; - const fakeThreadReport = { + const fakeThreadReport = createMock({ reportID: transactionThreadReportID, type: CONST.REPORT.TYPE.CHAT, parentReportID: 'self-dm-report', - } as Report; + }); const fakePolicy: Policy = { ...createRandomPolicy(Number(1)), id: policyID, diff --git a/tests/actions/OnyxUpdateManagerTest.ts b/tests/actions/OnyxUpdateManagerTest.ts index 69df0fa02c7f..c4be8e8ba572 100644 --- a/tests/actions/OnyxUpdateManagerTest.ts +++ b/tests/actions/OnyxUpdateManagerTest.ts @@ -1,12 +1,10 @@ import type {AppActionsMock} from '@libs/actions/__mocks__/App'; -import * as AppImport from '@libs/actions/App'; import applyOnyxUpdatesReliably from '@libs/actions/applyOnyxUpdatesReliably'; import * as OnyxUpdateManagerExports from '@libs/actions/OnyxUpdateManager'; import type {AnyDeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager/types'; -import * as OnyxUpdateManagerUtilsImport from '@libs/actions/OnyxUpdateManager/utils'; import type {OnyxUpdateManagerUtilsMock} from '@libs/actions/OnyxUpdateManager/utils/__mocks__'; import type {ApplyUpdatesMock} from '@libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates'; -import * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; +import type * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; import {isPaused as isSequentialQueuePaused, isRunning as isSequentialQueueRunning} from '@libs/Network/SequentialQueue'; import CONST from '@src/CONST'; @@ -19,6 +17,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; +import createMock from '../utils/createMock'; import OnyxUpdateMockUtils from '../utils/OnyxUpdateMockUtils'; jest.mock('@userActions/OnyxUpdates'); @@ -46,9 +45,9 @@ const TEST_USER_ACCOUNT_ID = 1; const REPORT_ID = 'testReport1'; const ONYX_KEY = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}` as const; -const App = AppImport as AppActionsMock; -const ApplyUpdates = ApplyUpdatesImport as ApplyUpdatesMock; -const OnyxUpdateManagerUtils = OnyxUpdateManagerUtilsImport as OnyxUpdateManagerUtilsMock; +const App = jest.requireMock>('@userActions/App'); +const ApplyUpdates = jest.requireMock('@userActions/OnyxUpdateManager/utils/applyUpdates'); +const OnyxUpdateManagerUtils = jest.requireMock('@userActions/OnyxUpdateManager/utils'); const exampleReportAction = { actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, @@ -60,7 +59,7 @@ const exampleReportAction = { shouldShow: true, } satisfies Partial; -const initialData = {report1: exampleReportAction, report2: exampleReportAction, report3: exampleReportAction} as unknown as OnyxTypes.ReportActions; +const initialData = createMock({report1: exampleReportAction, report2: exampleReportAction, report3: exampleReportAction}); const mockUpdate1 = OnyxUpdateMockUtils.createUpdate(1, [ { diff --git a/tests/actions/connections/QuickbooksOnline.ts b/tests/actions/connections/QuickbooksOnline.ts index a72192a4956e..82deea5c81d8 100644 --- a/tests/actions/connections/QuickbooksOnline.ts +++ b/tests/actions/connections/QuickbooksOnline.ts @@ -2,15 +2,16 @@ import * as API from '@libs/API'; import type {WriteCommand} from '@libs/API/types'; import {WRITE_COMMANDS} from '@libs/API/types'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; +import {isRecord} from '@libs/ObjectUtils'; import CONST from '@src/CONST'; import {updateQuickbooksOnlineSyncReimbursedReports, updateQuickbooksOnlineTravelInvoicingPayableAccount} from '@src/libs/actions/connections/QuickbooksOnline'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy as PolicyType} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {QBOConnectionConfig} from '@src/types/onyx/Policy'; import type {AnyOnyxData} from '@src/types/onyx/Request'; -import type {OnyxKey, OnyxUpdate} from 'react-native-onyx'; +import type {NullishDeep, OnyxKey, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -24,19 +25,50 @@ const writeSpy = jest.spyOn(API, 'write'); const MOCK_POLICY_ID = 'MOCK_POLICY_ID'; const MOCK_ACCOUNT_ID = 'account-123'; const MOCK_OLD_ACCOUNT_ID = 'account-456'; -const MOCK_ONYX_ERROR = {key: 'error'}; +const MOCK_ONYX_ERROR: Errors = {key: 'error'}; -function getQuickBooksConfig(update?: OnyxUpdate): QBOConnectionConfig | undefined { - if (!update || typeof update.value !== 'object' || update.value === null) { +type QuickBooksConfigUpdate = Pick< + Partial>, + 'collectionAccountID' | 'reimbursementAccountID' | 'travelInvoicingPayableAccountID' | 'pendingFields' | 'errorFields' +>; + +function isQuickBooksConfigUpdate(value: unknown): value is QuickBooksConfigUpdate { + if (!isRecord(value)) { + return false; + } + + return ( + (value.collectionAccountID === undefined || value.collectionAccountID === null || typeof value.collectionAccountID === 'string') && + (value.reimbursementAccountID === undefined || value.reimbursementAccountID === null || typeof value.reimbursementAccountID === 'string') && + (value.travelInvoicingPayableAccountID === undefined || value.travelInvoicingPayableAccountID === null || typeof value.travelInvoicingPayableAccountID === 'string') && + (value.pendingFields === undefined || + value.pendingFields === null || + (isRecord(value.pendingFields) && + Object.values(value.pendingFields).every((field) => field === null || Object.values(CONST.RED_BRICK_ROAD_PENDING_ACTION).some((action) => action === field)))) && + (value.errorFields === undefined || + value.errorFields === null || + (isRecord(value.errorFields) && + Object.values(value.errorFields).every( + (error) => error === undefined || error === null || (isRecord(error) && Object.values(error).every((message) => message === null || typeof message === 'string')), + ))) + ); +} + +function getQuickBooksConfig(update?: OnyxUpdate): QuickBooksConfigUpdate | undefined { + const value: unknown = update?.value; + if (!isRecord(value) || !isRecord(value.connections)) { + return undefined; + } + + const connection = value.connections[CONST.POLICY.CONNECTIONS.NAME.QBO]; + if (!isRecord(connection) || !('config' in connection) || !isQuickBooksConfigUpdate(connection.config)) { return undefined; } - const policyData = update.value as Pick; - const connection = policyData.connections?.[CONST.POLICY.CONNECTIONS.NAME.QBO]; - return connection?.config; + return connection.config; } -function getRequiredQuickBooksConfig(update?: OnyxUpdate): QBOConnectionConfig { +function getRequiredQuickBooksConfig(update?: OnyxUpdate): QuickBooksConfigUpdate { const config = getQuickBooksConfig(update); if (!config) { throw new Error('QuickBooks config is missing from the provided Onyx update'); @@ -62,7 +94,7 @@ describe('actions/connections/QuickbooksOnline', () => { beforeEach(() => { jest.clearAllMocks(); - (getMicroSecondOnyxErrorWithTranslationKey as jest.Mock).mockReturnValue(MOCK_ONYX_ERROR); + jest.mocked(getMicroSecondOnyxErrorWithTranslationKey).mockReturnValue(MOCK_ONYX_ERROR); return Onyx.clear().then(waitForBatchedUpdates); }); @@ -152,8 +184,8 @@ describe('actions/connections/QuickbooksOnline', () => { }); it('handles null setting values', () => { - const nullSettingValue = null as unknown as QBOConnectionConfig[Extract]; - updateQuickbooksOnlineSyncReimbursedReports(MOCK_POLICY_ID, nullSettingValue, MOCK_OLD_ACCOUNT_ID, MOCK_OLD_ACCOUNT_ID); + // @ts-expect-error -- null is intentionally exercised as invalid runtime input. + updateQuickbooksOnlineSyncReimbursedReports(MOCK_POLICY_ID, null, MOCK_OLD_ACCOUNT_ID, MOCK_OLD_ACCOUNT_ID); const {onyxData} = getFirstWriteCall(); const optimisticUpdate = onyxData?.optimisticData?.at(0); @@ -175,10 +207,13 @@ describe('actions/connections/QuickbooksOnline', () => { expect(command).toBe(WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_TRAVEL_INVOICING_PAYABLE_ACCOUNT); const call = writeSpy.mock.calls.at(0); - const params = call?.[1] as {policyID: string; settingValue: string; idempotencyKey: string}; - expect(params.policyID).toBe(MOCK_POLICY_ID); - expect(params.settingValue).toBe(MOCK_ACCOUNT_ID); - expect(params.idempotencyKey).toBe(String(CONST.QUICKBOOKS_CONFIG.TRAVEL_INVOICING_PAYABLE_ACCOUNT)); + expect(call?.[1]).toEqual( + expect.objectContaining({ + policyID: MOCK_POLICY_ID, + settingValue: MOCK_ACCOUNT_ID, + idempotencyKey: String(CONST.QUICKBOOKS_CONFIG.TRAVEL_INVOICING_PAYABLE_ACCOUNT), + }), + ); }); it('updates travelInvoicingPayableAccountID optimistically and reverts to the old value on failure', () => { diff --git a/tests/navigation/ResizeScreenTests.tsx b/tests/navigation/ResizeScreenTests.tsx index 87988c767a28..4206991bf1d9 100644 --- a/tests/navigation/ResizeScreenTests.tsx +++ b/tests/navigation/ResizeScreenTests.tsx @@ -20,6 +20,8 @@ import type {ParamListBase} from '@react-navigation/native'; import {NavigationContainer} from '@react-navigation/native'; import React from 'react'; +import createMock from '../utils/createMock'; + const Split = createSplitNavigator(); jest.mock('@hooks/useResponsiveLayout', () => jest.fn()); @@ -37,8 +39,8 @@ const INITIAL_STATE = { ], }; -const mockedGetIsNarrowLayout = getIsNarrowLayout as jest.MockedFunction; -const mockedUseResponsiveLayout = useResponsiveLayout as jest.MockedFunction; +const mockedGetIsNarrowLayout = jest.mocked(getIsNarrowLayout); +const mockedUseResponsiveLayout = jest.mocked(useResponsiveLayout); describe('Resize screen', () => { it('Should display the settings profile after resizing the screen with the settings page opened to the wide layout', () => { @@ -68,14 +70,34 @@ describe('Resize screen', () => { , ); - const {rerender} = renderHook(() => - useNavigationResetOnLayoutChange({ - navigation: navigationRef.current as unknown as CustomEffectsHookProps['navigation'], - displayName: 'SplitNavigator', - descriptors: {}, - state: navigationRef.current?.getState() as CustomEffectsHookProps['state'], - }), - ); + const navigation = navigationRef.current; + if (!navigation) { + throw new Error('Navigation container ref was not initialized'); + } + + const customEffectsState = createMock['state']>({ + ...navigation.getState(), + type: 'stack', + preloadedRoutes: [], + }); + let stateReturnedOnResize: ReturnType | undefined; + const mockedGetState = jest.fn(() => { + const state = navigation.getState(); + stateReturnedOnResize = state; + return state; + }); + const mockedReset = jest.fn((state: ReturnType) => navigation.reset(state)); + const customEffectsProps = createMock>({ + navigation: { + getState: mockedGetState, + reset: mockedReset, + }, + displayName: 'SplitNavigator', + descriptors: {}, + state: customEffectsState, + }); + + const {rerender} = renderHook(() => useNavigationResetOnLayoutChange(customEffectsProps)); const rootStateBeforeResize = navigationRef.current?.getRootState(); @@ -90,9 +112,14 @@ describe('Resize screen', () => { const rootStateAfterResize = navigationRef.current?.getRootState(); + if (!stateReturnedOnResize) { + throw new Error('Expected the navigation getState mock to be called during resize'); + } + // Then the settings profile page should be displayed on the screen expect(rootStateAfterResize?.routes.at(0)?.name).toBe(SCREENS.SETTINGS.ROOT); expect(rootStateAfterResize?.routes.at(1)?.name).toBe(SCREENS.SETTINGS.PROFILE.ROOT); expect(rootStateAfterResize?.index).toBe(1); + expect(mockedReset).toHaveBeenLastCalledWith(stateReturnedOnResize); }); }); diff --git a/tests/perf-test/ReportActionsUtils.perf-test.ts b/tests/perf-test/ReportActionsUtils.perf-test.ts index 67bd116468b9..06b6ffae8c19 100644 --- a/tests/perf-test/ReportActionsUtils.perf-test.ts +++ b/tests/perf-test/ReportActionsUtils.perf-test.ts @@ -2,7 +2,7 @@ import {getLastVisibleAction, getLastVisibleMessage, getSortedReportActionsForDi import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportActions} from '@src/types/onyx/ReportAction'; +import type {ReportActions, ReportActionsCollectionDataSet} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import {getLastClosedReportAction} from '@selectors/ReportAction'; @@ -13,24 +13,27 @@ import createCollection from '../utils/collections/createCollection'; import createRandomReportAction from '../utils/collections/reportActions'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -const getMockedReportActionsMap = (reportsLength = 10, actionsPerReportLength = 100) => { - const mockReportActions = Array.from({length: actionsPerReportLength}, (v, i) => { - const reportActionKey = i + 1; - const reportAction = createRandomReportAction(reportActionKey); +type ActionsToMerge = NonNullable[2]>; - return {[reportActionKey]: reportAction}; - }); - - const reportKeysMap = Array.from({length: reportsLength}, (v, i) => { - const key = i + 1; +const getMockedReportActionsMap = (reportsLength = 10, actionsPerReportLength = 100): ReportActionsCollectionDataSet => { + const mockReportActions: ReportActions = {}; + for (let actionIndex = 1; actionIndex <= actionsPerReportLength; actionIndex++) { + mockReportActions[actionIndex] = createRandomReportAction(actionIndex); + } - return {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${key}`]: Object.assign({}, ...mockReportActions) as Partial}; - }); + const reportActionsMap: ReportActionsCollectionDataSet = {}; + for (let reportIndex = 1; reportIndex <= reportsLength; reportIndex++) { + const reportActions: ReportActions = {}; + for (const [actionKey, reportAction] of Object.entries(mockReportActions)) { + reportActions[actionKey] = reportAction; + } + reportActionsMap[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIndex}`] = reportActions; + } - return Object.assign({}, ...reportKeysMap) as Partial; + return reportActionsMap; }; -const mockedReportActionsMap: Partial = getMockedReportActionsMap(2, 10000); +const mockedReportActionsMap: ReportActionsCollectionDataSet = getMockedReportActionsMap(2, 10000); const reportActions = createCollection( (item) => `${item.reportActionID}`, @@ -46,9 +49,7 @@ describe('ReportActionsUtils', () => { evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], }); - Onyx.multiSet({ - ...mockedReportActionsMap, - }); + Onyx.multiSet(mockedReportActionsMap); }); afterAll(() => { @@ -73,7 +74,7 @@ describe('ReportActionsUtils', () => { test('[ReportActionsUtils] getLastVisibleAction on 10k reportActions with actionsToMerge', async () => { const parentReportActionId = '1'; const fakeParentAction = reportActions[parentReportActionId]; - const actionsToMerge = { + const actionsToMerge: ActionsToMerge = { [parentReportActionId]: { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, previousMessage: fakeParentAction.message, @@ -88,9 +89,9 @@ describe('ReportActionsUtils', () => { }, ], errors: null, - linkMetaData: [], + linkMetadata: [], }, - } as unknown as ReportActions; + }; await waitForBatchedUpdates(); await measureFunction(() => getLastVisibleAction(reportId, true, actionsToMerge)); @@ -104,7 +105,7 @@ describe('ReportActionsUtils', () => { test('[ReportActionsUtils] getLastVisibleMessage on 10k ReportActions with actionsToMerge', async () => { const parentReportActionId = '1'; const fakeParentAction = reportActions[parentReportActionId]; - const actionsToMerge = { + const actionsToMerge: ActionsToMerge = { [parentReportActionId]: { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, previousMessage: fakeParentAction.message, @@ -119,9 +120,9 @@ describe('ReportActionsUtils', () => { }, ], errors: null, - linkMetaData: [], + linkMetadata: [], }, - } as unknown as ReportActions; + }; await waitForBatchedUpdates(); await measureFunction(() => getLastVisibleMessage(reportId, true, actionsToMerge)); diff --git a/tests/ui/ProfilePageTest.tsx b/tests/ui/ProfilePageTest.tsx index e4ef81203eef..66312618f914 100644 --- a/tests/ui/ProfilePageTest.tsx +++ b/tests/ui/ProfilePageTest.tsx @@ -21,6 +21,8 @@ import SCREENS from '@src/SCREENS'; import type {PersonalDetails, PersonalDetailsList} from '@src/types/onyx'; import type * as ReactNavigation from '@react-navigation/native'; +// eslint-disable-next-line no-restricted-imports -- React Native Text is required only to type the actual Jest module export; this does not import it at runtime. +import type {Text as ReactNativeText} from 'react-native'; import type {ValueOf} from 'type-fest'; import {PortalProvider} from '@gorhom/portal'; @@ -29,6 +31,7 @@ import React from 'react'; import Onyx from 'react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; +import {isObject} from '../utils/typeGuards'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; @@ -41,8 +44,8 @@ jest.mock('@libs/Navigation/Navigation', () => ({ })); jest.mock('@components/RenderHTML', () => { - const ReactMock = require('react') as typeof React; - const {Text} = require('react-native') as {Text: React.ComponentType<{children?: React.ReactNode}>}; + const ReactMock = jest.requireActual('react'); + const {Text} = jest.requireActual<{Text: typeof ReactNativeText}>('react-native'); return ({html}: {html: string}) => { const plainText = html.replaceAll(/<[^>]*>/g, ''); @@ -71,8 +74,8 @@ jest.mock('@react-navigation/native', () => { // Replace MenuItemWithTopDescription with a simple test double that exposes props in the tree jest.mock('@components/MenuItemWithTopDescription', () => { - const ReactMock = require('react') as typeof React; - const {Text} = require('react-native') as {Text: React.ComponentType<{testID: string; children?: React.ReactNode}>}; + const ReactMock = jest.requireActual('react'); + const {Text} = jest.requireActual<{Text: typeof ReactNativeText}>('react-native'); return ({pressableTestID, brickRoadIndicator}: {pressableTestID: string; brickRoadIndicator?: ValueOf}) => ReactMock.createElement(Text, {testID: pressableTestID}, `${brickRoadIndicator ?? 'none'}-brickRoadIndicator`); }); @@ -353,8 +356,8 @@ describe('ProfilePage - agent account', () => { renderPageWithNavigation(SCREENS.SETTINGS.PROFILE.ROOT); await waitForBatchedUpdatesWithAct(); - const saveButtonProps = screen.getByTestId('save-prompt-button').props as {accessibilityState?: {disabled?: boolean}}; - expect(saveButtonProps.accessibilityState?.disabled).toBe(false); + const saveButtonAccessibilityState: unknown = screen.getByTestId('save-prompt-button').props.accessibilityState; + expect(isObject(saveButtonAccessibilityState) ? saveButtonAccessibilityState.disabled : undefined).toBe(false); }); it('shows loading state on save button while a user-initiated prompt update is pending', async () => { @@ -383,8 +386,8 @@ describe('ProfilePage - agent account', () => { }); await waitForBatchedUpdatesWithAct(); - const saveButtonProps = screen.getByTestId('save-prompt-button').props as {accessibilityState?: {disabled?: boolean}}; - expect(saveButtonProps.accessibilityState?.disabled).toBe(true); + const saveButtonAccessibilityState: unknown = screen.getByTestId('save-prompt-button').props.accessibilityState; + expect(isObject(saveButtonAccessibilityState) ? saveButtonAccessibilityState.disabled : undefined).toBe(true); }); it('allows re-saving an edited prompt while offline even when a previous save is still pending', async () => { @@ -441,8 +444,8 @@ describe('ProfilePage - agent account', () => { }); await waitForBatchedUpdatesWithAct(); - const loadingButtonProps = screen.getByTestId('save-prompt-button').props as {accessibilityState?: {disabled?: boolean; busy?: boolean}}; - expect(loadingButtonProps.accessibilityState?.disabled).toBe(true); + const loadingButtonAccessibilityState: unknown = screen.getByTestId('save-prompt-button').props.accessibilityState; + expect(isObject(loadingButtonAccessibilityState) ? loadingButtonAccessibilityState.disabled : undefined).toBe(true); // Network drops while the request is still in flight: pendingAction stays 'update'. await act(async () => { @@ -450,8 +453,8 @@ describe('ProfilePage - agent account', () => { }); await waitForBatchedUpdatesWithAct(); - const offlineButtonProps = screen.getByTestId('save-prompt-button').props as {accessibilityState?: {disabled?: boolean; busy?: boolean}}; - expect(offlineButtonProps.accessibilityState?.disabled).toBe(false); + const offlineButtonAccessibilityState: unknown = screen.getByTestId('save-prompt-button').props.accessibilityState; + expect(isObject(offlineButtonAccessibilityState) ? offlineButtonAccessibilityState.disabled : undefined).toBe(false); }); it('does not call updateAgentPrompt when saving blank prompt', async () => { diff --git a/tests/ui/ReportActionAvatarsTest.tsx b/tests/ui/ReportActionAvatarsTest.tsx index 6cb56abb46a8..dfd4b7826997 100644 --- a/tests/ui/ReportActionAvatarsTest.tsx +++ b/tests/ui/ReportActionAvatarsTest.tsx @@ -24,16 +24,30 @@ import personalDetails from '../../__mocks__/reportData/personalDetails'; import {policy420A} from '../../__mocks__/reportData/policies'; import {chatReportR14932, iouReportR14932} from '../../__mocks__/reportData/reports'; import {transactionR14932} from '../../__mocks__/reportData/transactions'; +import {isObject} from '../utils/typeGuards'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; type AvatarData = { uri: string; - avatarID?: number; + avatarID?: number | string; name?: string; parent: string; }; +function getAvatarData(dataSet: unknown): AvatarData { + if (!isObject(dataSet) || typeof dataSet.uri !== 'string' || typeof dataSet.parent !== 'string') { + throw new Error('Expected an avatar test double to expose AvatarData'); + } + + return { + uri: dataSet.uri, + avatarID: typeof dataSet.avatarID === 'number' || typeof dataSet.avatarID === 'string' ? dataSet.avatarID : undefined, + name: typeof dataSet.name === 'string' ? dataSet.name : undefined, + parent: dataSet.parent, + }; +} + /* --- UI Mocks --- */ const parseSource = (source: AvatarSource | IconAsset): string => { @@ -41,7 +55,9 @@ const parseSource = (source: AvatarSource | IconAsset): string => { return source; } if (typeof source === 'object' && 'name' in source) { - return source.name as string; + if (typeof source.name === 'string') { + return source.name; + } } if (typeof source === 'object' && 'uri' in source) { return source.uri ?? 'No Source'; @@ -309,9 +325,15 @@ async function retrieveDataFromAvatarView(props: Parameters img.props.dataSet as AvatarData); - const iconData = icons.map((icon) => icon.props.dataSet as AvatarData); - const fragmentsData = reportAvatarFragments.map((fragment) => fragment.props.testID as string); + const imageData = images.map((img) => getAvatarData(img.props.dataSet)); + const iconData = icons.map((icon) => getAvatarData(icon.props.dataSet)); + const fragmentsData = reportAvatarFragments.map((fragment) => { + const testID: unknown = fragment.props.testID; + if (typeof testID !== 'string') { + throw new Error('Expected a report avatar fragment testID'); + } + return testID; + }); return { images: imageData, diff --git a/tests/ui/components/ComposedButton.tsx b/tests/ui/components/ComposedButton.tsx index 3f996b3dbbf7..06a31e62ce8e 100644 --- a/tests/ui/components/ComposedButton.tsx +++ b/tests/ui/components/ComposedButton.tsx @@ -557,7 +557,10 @@ describe('ButtonComposed — Button', () => { // `style` maps to PressableWithFeedback's wrapperStyle on the outer OpacityView. // We find it by scanning for any host View in the tree that carries the margin. - const wrapperView = renderResult.UNSAFE_getAllByType(View).find((v) => (StyleSheet.flatten(v.props.style ?? []) as {margin?: number})?.margin === 10); + const wrapperView = renderResult.UNSAFE_getAllByType(View).find((v) => { + const flatStyle: unknown = StyleSheet.flatten(v.props.style ?? []); + return typeof flatStyle === 'object' && flatStyle !== null && 'margin' in flatStyle && flatStyle.margin === 10; + }); expect(wrapperView).toBeDefined(); }); @@ -569,7 +572,10 @@ describe('ButtonComposed — Button', () => { // contentContainerStyle is applied to the flexRow View wrapping all children. // We find it by scanning host Views for the custom padding. - const contentWrapper = renderResult.UNSAFE_getAllByType(View).find((v) => (StyleSheet.flatten(v.props.style ?? []) as {paddingTop?: number})?.paddingTop === 8); + const contentWrapper = renderResult.UNSAFE_getAllByType(View).find((v) => { + const flatStyle: unknown = StyleSheet.flatten(v.props.style ?? []); + return typeof flatStyle === 'object' && flatStyle !== null && 'paddingTop' in flatStyle && flatStyle.paddingTop === 8; + }); expect(contentWrapper).toBeDefined(); }); @@ -579,22 +585,14 @@ describe('ButtonComposed — Button', () => { const renderResult = renderButton({blendOpacity: true}); const overlayView = renderResult.UNSAFE_getAllByType(View).find((v) => { - const flat = StyleSheet.flatten(v.props.style ?? []) as { - position?: string; - }; - return flat?.position === 'absolute'; + const flatStyle: unknown = StyleSheet.flatten(v.props.style ?? []); + return typeof flatStyle === 'object' && flatStyle !== null && 'position' in flatStyle && flatStyle.position === 'absolute'; }); expect(overlayView).toBeDefined(); // The overlay must cover the full button — verify absoluteFill dimensions - const overlayStyle = StyleSheet.flatten(overlayView?.props.style ?? []) as { - position?: string; - top?: number; - left?: number; - right?: number; - bottom?: number; - }; + const overlayStyle: unknown = StyleSheet.flatten(overlayView?.props.style ?? []); expect(overlayStyle).toMatchObject({ position: 'absolute', top: 0, diff --git a/tests/ui/components/IOURequestStepConfirmationPageTest.tsx b/tests/ui/components/IOURequestStepConfirmationPageTest.tsx index c217ea7370ca..227f1e3c10bd 100644 --- a/tests/ui/components/IOURequestStepConfirmationPageTest.tsx +++ b/tests/ui/components/IOURequestStepConfirmationPageTest.tsx @@ -1055,7 +1055,7 @@ describe('IOURequestStepConfirmationPageTest', () => { fireEvent.press(await screen.findByText(getConfirmButtonRegex())); await waitFor(() => expect(TrackExpense.requestMoney).toHaveBeenCalled()); - const requestMoneyMock = TrackExpense.requestMoney as jest.MockedFunction; + const requestMoneyMock = jest.mocked(TrackExpense.requestMoney); const params = requestMoneyMock.mock.calls.at(0)?.at(0); expect(params?.report).toBeUndefined(); }); @@ -1122,7 +1122,7 @@ describe('IOURequestStepConfirmationPageTest', () => { fireEvent.press(await screen.findByText(getConfirmButtonRegex())); await waitFor(() => expect(TrackExpense.requestMoney).toHaveBeenCalled()); - const requestMoneyMock = TrackExpense.requestMoney as jest.MockedFunction; + const requestMoneyMock = jest.mocked(TrackExpense.requestMoney); const params = requestMoneyMock.mock.calls.at(0)?.at(0); expect(params?.report?.reportID).toBe(routeReportID); }); @@ -1198,7 +1198,7 @@ describe('IOURequestStepConfirmationPageTest', () => { fireEvent.press(await screen.findByText(getConfirmButtonRegex())); await waitFor(() => expect(TrackExpense.requestMoney).toHaveBeenCalled()); - const requestMoneyMock = TrackExpense.requestMoney as jest.MockedFunction; + const requestMoneyMock = jest.mocked(TrackExpense.requestMoney); const params = requestMoneyMock.mock.calls.at(0)?.at(0); expect(params?.report?.reportID).toBe(transactionReportID); } finally { @@ -1416,7 +1416,7 @@ describe('IOURequestStepConfirmationPageTest', () => { fireEvent.press(await screen.findByText(/^Create .*expense/i)); await waitFor(() => expect(Split.createDistanceRequest).toHaveBeenCalled()); - const createDistanceRequestMock = Split.createDistanceRequest as jest.MockedFunction; + const createDistanceRequestMock = jest.mocked(Split.createDistanceRequest); const params = createDistanceRequestMock.mock.calls.at(0)?.at(0); expect(params?.personalDetails).toBeDefined(); }); diff --git a/tests/unit/ValidateAttachmentFileTest.ts b/tests/unit/ValidateAttachmentFileTest.ts index 4f86e96fe1f7..0835ddafff7c 100644 --- a/tests/unit/ValidateAttachmentFileTest.ts +++ b/tests/unit/ValidateAttachmentFileTest.ts @@ -4,6 +4,7 @@ import type {FileObject} from '@src/types/utils/Attachment'; import CONST from '../../src/CONST'; import * as FileUtils from '../../src/libs/fileDownload/FileUtils'; +import createMock from '../utils/createMock'; // Mock only normalizeFileObject and validateImageForCorruption; keep real hasHeicOrHeifExtension and isValidReceiptExtension jest.mock('@src/libs/fileDownload/FileUtils', () => { @@ -15,7 +16,7 @@ jest.mock('@src/libs/fileDownload/FileUtils', () => { }; }); -const mockFileUtils = FileUtils as jest.Mocked; +const mockFileUtils = jest.mocked(FileUtils); const createMockFile = (name: string, size: number): FileObject => ({ name, @@ -54,7 +55,7 @@ describe('validateAttachmentFile', () => { }); it('returns invalid result with FILE_INVALID when file has null size', async () => { - const file: FileObject = {name: 'receipt.jpg', size: null as unknown as number}; + const file = {name: 'receipt.jpg', size: null}; const error = await validateAttachmentFile(file, undefined, true); if (error.isValid) { @@ -191,12 +192,10 @@ describe('validateAttachmentFile', () => { describe('FOLDER_NOT_ALLOWED', () => { it('returns invalid result with FOLDER_NOT_ALLOWED when DataTransferItem is a directory', async () => { - const mockItem = { + const mockItem = createMock({ kind: 'file' as const, - webkitGetAsEntry: jest.fn(() => ({ - isDirectory: true, - })), - } as unknown as DataTransferItem; + webkitGetAsEntry: jest.fn(() => createMock({isDirectory: true})), + }); const file = createMockFile('folder', 0); const error = await validateAttachmentFile(file, mockItem); @@ -209,12 +208,10 @@ describe('validateAttachmentFile', () => { }); it('returns valid result when DataTransferItem is not a directory', async () => { - const mockItem = { + const mockItem = createMock({ kind: 'file' as const, - webkitGetAsEntry: jest.fn(() => ({ - isDirectory: false, - })), - } as unknown as DataTransferItem; + webkitGetAsEntry: jest.fn(() => createMock({isDirectory: false})), + }); const file = createMockFile('file.pdf', 100); const error = await validateAttachmentFile(file, mockItem); @@ -316,11 +313,11 @@ describe('validateAttachmentFile', () => { try { const blob = new Blob(['content'], {type: 'text/plain'}); const convertedFile = new File([blob], 'file.txt', {type: 'text/plain'}); - const file = { + const file = createMock({ name: 'file.txt', size: 7, getAsFile: () => convertedFile, - } as unknown as FileObject; + }); const error = await validateAttachmentFile(file);