diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index 8a123ffb66a3..9206aab98b99 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -21,6 +21,8 @@ import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type {Account, BankAccountList, Session} from '@src/types/onyx'; import type AccountData from '@src/types/onyx/AccountData'; +import createMock from '../utils/createMock'; + describe('BankAccountUtils', () => { describe('isPersonalBankAccountMissingInfo', () => { const completeAccountData: AccountData = { @@ -327,25 +329,25 @@ describe('BankAccountUtils', () => { describe('hasPartiallySetupBankAccount', () => { it('returns true when at least one account is in SETUP state', () => { - const bankAccountList = { + const bankAccountList = createMock({ accountOne: {accountData: {state: CONST.BANK_ACCOUNT.STATE.OPEN}, bankCurrency: 'USD', bankCountry: 'US'}, accountTwo: {accountData: {state: CONST.BANK_ACCOUNT.STATE.SETUP}, bankCurrency: 'USD', bankCountry: 'US'}, - } as unknown as BankAccountList; + }); expect(hasPartiallySetupBankAccount(bankAccountList)).toBe(true); }); it('returns true when at least one account is in VERIFYING state', () => { - const bankAccountList = { + const bankAccountList = createMock({ accountOne: {accountData: {state: CONST.BANK_ACCOUNT.STATE.VERIFYING}, bankCurrency: 'USD', bankCountry: 'US'}, - } as unknown as BankAccountList; + }); expect(hasPartiallySetupBankAccount(bankAccountList)).toBe(true); }); it('returns false when all accounts are in OPEN state', () => { - const bankAccountList = { + const bankAccountList = createMock({ accountOne: {accountData: {state: CONST.BANK_ACCOUNT.STATE.OPEN}, bankCurrency: 'USD', bankCountry: 'US'}, accountTwo: {accountData: {state: CONST.BANK_ACCOUNT.STATE.OPEN}, bankCurrency: 'USD', bankCountry: 'US'}, - } as unknown as BankAccountList; + }); expect(hasPartiallySetupBankAccount(bankAccountList)).toBe(false); }); @@ -391,7 +393,7 @@ describe('BankAccountUtils', () => { describe('hasPersonalBankAccountMissingInfo', () => { it('returns true when at least one account has missing info', () => { - const bankAccountList = { + const bankAccountList = createMock({ accountOne: { accountData: { type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, @@ -401,12 +403,12 @@ describe('BankAccountUtils', () => { bankCurrency: 'USD', bankCountry: 'US', }, - } as unknown as BankAccountList; + }); expect(hasPersonalBankAccountMissingInfo(bankAccountList)).toBe(true); }); it('returns false when all accounts have complete info', () => { - const bankAccountList = { + const bankAccountList = createMock({ accountOne: { accountData: { type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, @@ -425,7 +427,7 @@ describe('BankAccountUtils', () => { bankCurrency: 'USD', bankCountry: 'US', }, - } as unknown as BankAccountList; + }); expect(hasPersonalBankAccountMissingInfo(bankAccountList)).toBe(false); }); @@ -438,7 +440,7 @@ describe('BankAccountUtils', () => { }); it('returns false when account uses NewDot legalFirstName/legalLastName naming', () => { - const bankAccountList = { + const bankAccountList = createMock({ accountOne: { accountData: { type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, @@ -457,7 +459,7 @@ describe('BankAccountUtils', () => { bankCurrency: 'USD', bankCountry: 'US', }, - } as unknown as BankAccountList; + }); expect(hasPersonalBankAccountMissingInfo(bankAccountList)).toBe(false); }); }); @@ -477,9 +479,9 @@ describe('BankAccountUtils', () => { }; it('returns all steps when all data is present', () => { - const bankAccountList = { + const bankAccountList = createMock({ [bankAccountKey]: {accountData: {additionalData: fullAdditionalData}, bankCurrency: 'USD', bankCountry: 'US'}, - } as unknown as BankAccountList; + }); const result = getCompletedStepsForBankAccount(bankAccountList, bankAccountID); expect(result).toEqual([PERSONAL_INFO_STEP.NAME, PERSONAL_INFO_STEP.ADDRESS, PERSONAL_INFO_STEP.PHONE]); }); @@ -494,77 +496,77 @@ describe('BankAccountUtils', () => { }); it('returns only NAME step when only name fields are present', () => { - const bankAccountList = { + const bankAccountList = createMock({ [bankAccountKey]: {accountData: {additionalData: {firstName: 'John', lastName: 'Doe'}}, bankCurrency: 'USD', bankCountry: 'US'}, - } as unknown as BankAccountList; + }); expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([PERSONAL_INFO_STEP.NAME]); }); it('returns only ADDRESS step when only address fields are present', () => { - const bankAccountList = { + const bankAccountList = createMock({ [bankAccountKey]: { accountData: {additionalData: {addressStreet: '123 Main St', addressCity: 'New York', addressState: 'NY', addressZipCode: '10001'}}, bankCurrency: 'USD', bankCountry: 'US', }, - } as unknown as BankAccountList; + }); expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([PERSONAL_INFO_STEP.ADDRESS]); }); it('returns only PHONE step when only phone is present', () => { - const bankAccountList = { + const bankAccountList = createMock({ [bankAccountKey]: {accountData: {additionalData: {companyPhone: '+15551234567'}}, bankCurrency: 'USD', bankCountry: 'US'}, - } as unknown as BankAccountList; + }); expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([PERSONAL_INFO_STEP.PHONE]); }); it('returns empty array when accountData has no additionalData', () => { - const bankAccountList = { + const bankAccountList = createMock({ [bankAccountKey]: {accountData: {}, bankCurrency: 'USD', bankCountry: 'US'}, - } as unknown as BankAccountList; + }); expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([]); }); it('does not include NAME when only firstName is present (lastName missing)', () => { - const bankAccountList = { + const bankAccountList = createMock({ [bankAccountKey]: {accountData: {additionalData: {firstName: 'John'}}, bankCurrency: 'USD', bankCountry: 'US'}, - } as unknown as BankAccountList; + }); expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([]); }); it('returns multiple steps when some groups are complete', () => { - const bankAccountList = { + const bankAccountList = createMock({ [bankAccountKey]: { accountData: {additionalData: {firstName: 'John', lastName: 'Doe', companyPhone: '+15551234567'}}, bankCurrency: 'USD', bankCountry: 'US', }, - } as unknown as BankAccountList; + }); expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([PERSONAL_INFO_STEP.NAME, PERSONAL_INFO_STEP.PHONE]); }); it('does not include ADDRESS when one address field is missing', () => { - const bankAccountList = { + const bankAccountList = createMock({ [bankAccountKey]: { accountData: {additionalData: {addressStreet: '123 Main St', addressCity: 'New York', addressState: 'NY'}}, bankCurrency: 'USD', bankCountry: 'US', }, - } as unknown as BankAccountList; + }); expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([]); }); it('includes NAME step when only NewDot legalFirstName/legalLastName are present', () => { - const bankAccountList = { + const bankAccountList = createMock({ [bankAccountKey]: {accountData: {additionalData: {legalFirstName: 'John', legalLastName: 'Doe'}}, bankCurrency: 'USD', bankCountry: 'US'}, - } as unknown as BankAccountList; + }); expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([PERSONAL_INFO_STEP.NAME]); }); it('does not include NAME when only legalFirstName is present (legalLastName missing)', () => { - const bankAccountList = { + const bankAccountList = createMock({ [bankAccountKey]: {accountData: {additionalData: {legalFirstName: 'John'}}, bankCurrency: 'USD', bankCountry: 'US'}, - } as unknown as BankAccountList; + }); expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([]); }); }); diff --git a/tests/unit/ImportTransactions.test.ts b/tests/unit/ImportTransactions.test.ts index 17d85e737155..0921010359dd 100644 --- a/tests/unit/ImportTransactions.test.ts +++ b/tests/unit/ImportTransactions.test.ts @@ -10,6 +10,11 @@ import type {SavedCSVColumnLayoutData} from '@src/types/onyx/SavedCSVColumnLayou /* eslint-disable @typescript-eslint/naming-convention */ import Onyx from 'react-native-onyx'; +import createMock from '../utils/createMock'; +import {getRequiredOnyxUpdate, getRequiredOnyxUpdates, getRequiredWriteCall} from '../utils/TestHelper'; + +let writeSpy: jest.SpiedFunction; + describe('ImportTransactions', () => { beforeEach(() => { jest.clearAllMocks(); @@ -261,7 +266,7 @@ describe('ImportTransactions', () => { describe('buildTransactionListFromSpreadsheet', () => { it('should return empty array when data is empty', () => { - const spreadsheet = { + const spreadsheet = createMock({ data: [], columns: { 0: 'date', @@ -269,7 +274,7 @@ describe('ImportTransactions', () => { 2: 'amount', }, containsHeader: true, - } as Partial as ImportedSpreadsheet; + }); const result = buildTransactionListFromSpreadsheet(spreadsheet, {}); @@ -277,7 +282,7 @@ describe('ImportTransactions', () => { }); it('should build transactions from valid spreadsheet data', () => { - const spreadsheet = { + const spreadsheet = createMock({ data: [ ['Date', '2024-01-15', '2024-01-20'], ['Merchant', 'Coffee Shop', 'Restaurant'], @@ -289,7 +294,7 @@ describe('ImportTransactions', () => { 2: 'amount', }, containsHeader: true, - } as Partial as ImportedSpreadsheet; + }); const result = buildTransactionListFromSpreadsheet(spreadsheet, {}); @@ -313,7 +318,7 @@ describe('ImportTransactions', () => { }); it('should include category when provided', () => { - const spreadsheet = { + const spreadsheet = createMock({ data: [ ['Date', '2024-01-15'], ['Merchant', 'Store'], @@ -327,7 +332,7 @@ describe('ImportTransactions', () => { 3: 'category', }, containsHeader: true, - } as Partial as ImportedSpreadsheet; + }); const result = buildTransactionListFromSpreadsheet(spreadsheet, {}); @@ -336,7 +341,7 @@ describe('ImportTransactions', () => { }); it('should skip rows with missing required fields (date or amount)', () => { - const spreadsheet = { + const spreadsheet = createMock({ data: [ ['Date', '2024-01-15', '', '2024-01-20'], ['Merchant', 'Store A', 'Store B', 'Store C'], @@ -348,7 +353,7 @@ describe('ImportTransactions', () => { 2: 'amount', }, containsHeader: true, - } as Partial as ImportedSpreadsheet; + }); const result = buildTransactionListFromSpreadsheet(spreadsheet, {}); @@ -358,7 +363,7 @@ describe('ImportTransactions', () => { }); it('should flip amount sign when flipAmountSign is true', () => { - const spreadsheet = { + const spreadsheet = createMock({ data: [ ['Date', '2024-01-15'], ['Merchant', 'Store'], @@ -370,7 +375,7 @@ describe('ImportTransactions', () => { 2: 'amount', }, containsHeader: true, - } as Partial as ImportedSpreadsheet; + }); const result = buildTransactionListFromSpreadsheet(spreadsheet, {flipAmountSign: true}); @@ -379,7 +384,7 @@ describe('ImportTransactions', () => { }); it('should handle amounts with currency symbols and commas', () => { - const spreadsheet = { + const spreadsheet = createMock({ data: [ ['Date', '2024-01-15', '2024-01-16'], ['Merchant', 'Store A', 'Store B'], @@ -391,7 +396,7 @@ describe('ImportTransactions', () => { 2: 'amount', }, containsHeader: true, - } as Partial as ImportedSpreadsheet; + }); const result = buildTransactionListFromSpreadsheet(spreadsheet, {}); @@ -401,7 +406,7 @@ describe('ImportTransactions', () => { }); it('should handle negative amounts', () => { - const spreadsheet = { + const spreadsheet = createMock({ data: [ ['Date', '2024-01-15'], ['Merchant', 'Refund'], @@ -413,7 +418,7 @@ describe('ImportTransactions', () => { 2: 'amount', }, containsHeader: true, - } as Partial as ImportedSpreadsheet; + }); const result = buildTransactionListFromSpreadsheet(spreadsheet, {}); @@ -422,7 +427,7 @@ describe('ImportTransactions', () => { }); it('should work with containsHeader false', () => { - const spreadsheet = { + const spreadsheet = createMock({ data: [ ['2024-01-15', '2024-01-16'], ['Store A', 'Store B'], @@ -434,7 +439,7 @@ describe('ImportTransactions', () => { 2: 'amount', }, containsHeader: false, - } as Partial as ImportedSpreadsheet; + }); const result = buildTransactionListFromSpreadsheet(spreadsheet, {}); @@ -447,7 +452,7 @@ describe('ImportTransactions', () => { }); it('should handle various date formats', () => { - const spreadsheet = { + const spreadsheet = createMock({ data: [ ['Date', '2024-01-15', '01/20/2024', '20-01-2024', 'Jan 25, 2024'], ['Merchant', 'A', 'B', 'C', 'D'], @@ -459,7 +464,7 @@ describe('ImportTransactions', () => { 2: 'amount', }, containsHeader: true, - } as Partial as ImportedSpreadsheet; + }); const result = buildTransactionListFromSpreadsheet(spreadsheet, {}); @@ -471,7 +476,7 @@ describe('ImportTransactions', () => { }); it('should skip rows with invalid dates', () => { - const spreadsheet = { + const spreadsheet = createMock({ data: [ ['Date', '2024-01-15', 'invalid-date', '2024-01-20'], ['Merchant', 'Store A', 'Store B', 'Store C'], @@ -483,7 +488,7 @@ describe('ImportTransactions', () => { 2: 'amount', }, containsHeader: true, - } as Partial as ImportedSpreadsheet; + }); const result = buildTransactionListFromSpreadsheet(spreadsheet, {}); @@ -493,7 +498,7 @@ describe('ImportTransactions', () => { }); it('should handle missing merchant gracefully', () => { - const spreadsheet = { + const spreadsheet = createMock({ data: [ ['Date', '2024-01-15'], ['Amount', '10.00'], @@ -505,7 +510,7 @@ describe('ImportTransactions', () => { // No merchant column mapped }, containsHeader: true, - } as Partial as ImportedSpreadsheet; + }); const result = buildTransactionListFromSpreadsheet(spreadsheet, {}); @@ -520,10 +525,10 @@ describe('ImportTransactions', () => { ['Date', '2024-01-01'], ['Merchant', 'Store'], ]; - const savedLayout = { + const savedLayout = createMock({ name: 'Test', columnMapping: {}, - } as SavedCSVColumnLayoutData; + }); applySavedColumnMappings(spreadsheetData, savedLayout); @@ -535,12 +540,12 @@ describe('ImportTransactions', () => { ['Date', '2024-01-01'], ['Merchant', 'Store'], ]; - const savedLayout = { + const savedLayout = createMock({ name: 'Test', columnMapping: { indexes: {}, }, - } as SavedCSVColumnLayoutData; + }); applySavedColumnMappings(spreadsheetData, savedLayout); @@ -870,7 +875,7 @@ describe('ImportTransactions', () => { describe('importTransactionsFromCSV', () => { const CURRENT_USER_ACCOUNT_ID = 12345; - const validSpreadsheet = { + const validSpreadsheet = createMock({ data: [ ['Date', '2024-01-15', '2024-01-20'], ['Merchant', 'Coffee Shop', 'Restaurant'], @@ -882,9 +887,7 @@ describe('ImportTransactions', () => { 2: 'amount', }, containsHeader: true, - } as Partial as ImportedSpreadsheet; - - let writeSpy: jest.SpyInstance; + }); beforeEach(() => { writeSpy = jest.spyOn(API, 'write').mockRejectedValue(new Error('forced')); @@ -905,9 +908,9 @@ describe('ImportTransactions', () => { await importTransactionsFromCSV(validSpreadsheet, CURRENT_USER_ACCOUNT_ID); expect(writeSpy).toHaveBeenCalledTimes(1); - const [command, , onyxData] = writeSpy.mock.calls.at(0) as [string, unknown, {optimisticData: Array<{key: string}>}]; + const [command, , onyxData] = getRequiredWriteCall(writeSpy.mock.calls, 0); expect(command).toBe('ImportCSVTransactions'); - expect(onyxData.optimisticData.some((entry) => entry.key === ONYXKEYS.CARD_LIST)).toBe(true); + getRequiredOnyxUpdate(onyxData, 'optimisticData', ONYXKEYS.CARD_LIST, Onyx.METHOD.MERGE); }); it('reuses an existingCardID without queuing an optimistic card', async () => { @@ -915,9 +918,10 @@ describe('ImportTransactions', () => { await importTransactionsFromCSV(validSpreadsheet, CURRENT_USER_ACCOUNT_ID, existingCardID); - const [, params, onyxData] = writeSpy.mock.calls.at(0) as [string, {cardID: number}, {optimisticData: Array<{key: string}>}]; + const [, params, onyxData] = getRequiredWriteCall(writeSpy.mock.calls, 0); expect(params.cardID).toBe(existingCardID); - expect(onyxData.optimisticData.some((entry) => entry.key === ONYXKEYS.CARD_LIST)).toBe(false); + const optimisticData = getRequiredOnyxUpdates(onyxData, 'optimisticData'); + expect(optimisticData).not.toEqual(expect.arrayContaining([expect.objectContaining({key: ONYXKEYS.CARD_LIST})])); }); }); }); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index e109d6b54701..218863a05f99 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -22,7 +22,7 @@ import getReportURLForCurrentContext from '@libs/Navigation/helpers/getReportURL import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; -import {getOriginalMessage, getReportAction, isWhisperAction} from '@libs/ReportActionsUtils'; +import {getOriginalMessage, getReportAction, isActionOfType, isWhisperAction} from '@libs/ReportActionsUtils'; // Testing only so it's okay to import computeReportName // eslint-disable-next-line no-restricted-imports import {buildReportNameFromParticipantNames, computeReportName as computeReportNameOriginal, getGroupChatName, getPolicyExpenseChatName, getReportName} from '@libs/ReportNameUtils'; @@ -217,12 +217,11 @@ import type { Transaction, TransactionViolation, } from '@src/types/onyx'; -import type {ErrorFields, Errors, OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; -import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage'; +import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; import type {ACHAccount, PolicyReportField} from '@src/types/onyx/Policy'; import type {Participant, Participants, ReportCollectionDataSet} from '@src/types/onyx/Report'; import type {ReportActionsCollectionDataSet} from '@src/types/onyx/ReportAction'; -import type {TransactionCollectionDataSet} from '@src/types/onyx/Transaction'; +import type {ReceiptErrors, TransactionCollectionDataSet} from '@src/types/onyx/Transaction'; import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -265,6 +264,13 @@ import {fakePersonalDetails} from '../utils/LHNTestUtils'; import {convertToDisplayString, formatPhoneNumber, localeCompare, translateLocal} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +type ClosedReportActionMessage = ReportAction['message']; +type ClosedReportActionMessageArray = Extract, readonly unknown[]>; + +function isClosedReportActionMessageArray(message: ClosedReportActionMessage): message is ClosedReportActionMessageArray { + return Array.isArray(message); +} + // Be sure to include the mocked permissions library or else the beta tests won't work jest.mock('@libs/Permissions'); @@ -308,7 +314,7 @@ jest.mock('@libs/PolicyUtils', () => { }; }); -const mockedPolicyUtils = PolicyUtils as jest.Mocked; +const mockedPolicyUtils = jest.mocked(PolicyUtils); const testDate = DateUtils.getDBTime(); const currentUserEmail = 'bjorn@vikings.net'; @@ -950,14 +956,27 @@ describe('ReportUtils', () => { }); const personalDetailsCall = mergeSpy.mock.calls.find((call) => call[0] === ONYXKEYS.PERSONAL_DETAILS_LIST); - const personalDetailsData = personalDetailsCall?.[1] as Record; - const accountExecutiveDetail = Object.values(personalDetailsData ?? {}).at(0); + if (!personalDetailsCall || typeof personalDetailsCall[1] !== 'object' || personalDetailsCall[1] === null) { + throw new Error('Expected personal details merge call'); + } + const personalDetailsData = personalDetailsCall[1]; + const accountExecutiveDetail = Object.values(personalDetailsData).at(0); + if ( + typeof accountExecutiveDetail !== 'object' || + accountExecutiveDetail === null || + !('accountID' in accountExecutiveDetail) || + typeof accountExecutiveDetail.accountID !== 'number' || + !('avatar' in accountExecutiveDetail) || + typeof accountExecutiveDetail.avatar !== 'string' + ) { + throw new Error('Expected Account Executive personal details'); + } expect(accountExecutiveDetail).toBeDefined(); expect(accountExecutiveDetail?.accountID).toBeDefined(); expect(accountExecutiveDetail?.accountID).toBe(CONST.ACCOUNT_ID.QA_GUIDE); - expect(accountExecutiveDetail?.avatar).toBeDefined(); - expect(accountExecutiveDetail?.avatar).toContain('images/avatars/'); + expect(accountExecutiveDetail.avatar).toBeDefined(); + expect(accountExecutiveDetail.avatar).toContain('images/avatars/'); mergeSpy.mockRestore(); }); @@ -1268,7 +1287,7 @@ describe('ReportUtils', () => { it('should return true for transaction has receipt error', () => { const parentReport = LHNTestUtils.getFakeReport(); const report = LHNTestUtils.getFakeReport(); - const errors: Errors | ErrorFields = { + const errors: ReceiptErrors = { '1231231231313221': { error: CONST.IOU.RECEIPT_ERROR, source: 'blob:https://dev.new.expensify.com:8082/6c5b7110-42c2-4e6d-8566-657ff24caf21', @@ -1281,7 +1300,7 @@ describe('ReportUtils', () => { const currentReportId = ''; const transactionID = 1; - const transaction = { + const transaction: OnyxInputOrEntry = { ...createRandomTransaction(transactionID), category: '', tag: '', @@ -1293,7 +1312,7 @@ describe('ReportUtils', () => { }, errors, }; - expect(hasReceiptError(transaction as OnyxInputOrEntry)).toBe(true); + expect(hasReceiptError(transaction)).toBe(true); }); }); @@ -1305,7 +1324,7 @@ describe('ReportUtils', () => { const currentReportId = ''; const transactionID = 1; - const transaction = { + const transaction: OnyxInputOrEntry = { ...createRandomTransaction(transactionID), category: '', tag: '', @@ -1316,7 +1335,7 @@ describe('ReportUtils', () => { liabilityType: CONST.TRANSACTION.LIABILITY_TYPE.RESTRICT, }, }; - expect(hasReceiptError(transaction as OnyxInputOrEntry)).toBe(false); + expect(hasReceiptError(transaction)).toBe(false); }); }); @@ -6950,12 +6969,14 @@ describe('ReportUtils', () => { [currentUserAccountID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, }, }; - const reportCollectionDataSet = toCollectionDataSet( - ONYXKEYS.COLLECTION.REPORT, - [invoiceReport, taskReport, iouReport, groupChatReport, oneOnOneChatReport], - (item) => item.reportID, - ); - return Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, reportCollectionDataSet as OnyxMergeCollectionInput); + const reportCollectionDataSet: OnyxMergeCollectionInput = { + [`${ONYXKEYS.COLLECTION.REPORT}${invoiceReport.reportID}`]: invoiceReport, + [`${ONYXKEYS.COLLECTION.REPORT}${taskReport.reportID}`]: taskReport, + [`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`]: iouReport, + [`${ONYXKEYS.COLLECTION.REPORT}${groupChatReport.reportID}`]: groupChatReport, + [`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.reportID}`]: oneOnOneChatReport, + }; + return Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, reportCollectionDataSet); }); it('should return the 1:1 chat', () => { const report = getChatByParticipants([currentUserAccountID, userAccountID]); @@ -8088,7 +8109,8 @@ describe('ReportUtils', () => { it('should return false when the report is null', () => { expect( shouldReportBeInOptionList({ - report: null as unknown as Report, + // @ts-expect-error -- null intentionally exercises the runtime null report case. + report: null, chatReport: mockedChatReport, currentReportId: '', isInFocusMode: false, @@ -8282,24 +8304,27 @@ describe('ReportUtils', () => { it('should not assign a role to participants for workspace chat type', () => { const currentUser = 100; const result = buildOptimisticAnnounceChat('policyID123', [100, 200, 300], currentUser); - const reportData = result.announceChatData.onyxOptimisticData.find((data) => data.key.startsWith(`${ONYXKEYS.COLLECTION.REPORT}`)); - const report = reportData?.value as Report | undefined; - expect(report?.participants?.[currentUser]?.role).toBeUndefined(); + const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${result.announceChatReportID}`; + const reportData = result.announceChatData.onyxOptimisticData.find((data) => data.key === reportKey); + expect(reportData).toBeDefined(); + expect(reportData?.value).toHaveProperty(['participants', String(currentUser)]); + + expect(reportData?.value).not.toHaveProperty(['participants', String(currentUser), 'role']); }); it('should create announce chat with POLICY_ANNOUNCE chat type', () => { const currentUser = 100; const result = buildOptimisticAnnounceChat('policyID123', [100, 200, 300], currentUser); const reportData = result.announceChatData.onyxOptimisticData.find((data) => data.key.startsWith(`${ONYXKEYS.COLLECTION.REPORT}`) && !data.key.includes('Draft')); - const report = reportData?.value as Report | undefined; - expect(report?.chatType).toBe(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE); + expect(reportData).toBeDefined(); + expect(reportData?.value).toMatchObject({chatType: CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE}); }); it('should set writeCapability to ADMINS for announce chat', () => { const result = buildOptimisticAnnounceChat('policyID123', [100, 200, 300], 100); const reportData = result.announceChatData.onyxOptimisticData.find((data) => data.key.startsWith(`${ONYXKEYS.COLLECTION.REPORT}`) && !data.key.includes('Draft')); - const report = reportData?.value as Report | undefined; - expect(report?.writeCapability).toBe(CONST.REPORT.WRITE_CAPABILITIES.ADMINS); + expect(reportData).toBeDefined(); + expect(reportData?.value).toMatchObject({writeCapability: CONST.REPORT.WRITE_CAPABILITIES.ADMINS}); }); }); @@ -8374,7 +8399,7 @@ describe('ReportUtils', () => { oldName: 'workspace 1', }, }; - expect(getWorkspaceNameUpdatedMessage(translateLocal, action as ReportAction)).toEqual( + expect(getWorkspaceNameUpdatedMessage(translateLocal, createMock(action))).toEqual( 'updated the name of this workspace to "&#104;&#101;&#108;&#108;&#111;" (previously "workspace 1")', ); }); @@ -8392,14 +8417,14 @@ describe('ReportUtils', () => { }; function buildCopyAction(actionName: ReportAction['actionName'], originalMessage?: {sourcePolicyID?: string; quantity?: number}): ReportAction { - return { + return createMock({ actionName, reportActionID: '1', reportID: '123', created: '2026-05-15 10:00:00.000', message: [], originalMessage, - } as unknown as ReportAction; + }); } beforeEach(async () => { @@ -10421,7 +10446,8 @@ describe('ReportUtils', () => { const joinRequestReportAction: ReportAction = { ...createRandomReportAction(50400), originalMessage: { - choice: '' as JoinWorkspaceResolution, + // @ts-expect-error -- empty choice intentionally models an incomplete legacy join request. + choice: '', policyID: '1', }, actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_JOIN_REQUEST, @@ -13137,7 +13163,7 @@ describe('ReportUtils', () => { await flushPromises(); }); - const mockGetActiveRoute = Navigation.getActiveRoute as jest.Mock; + const mockGetActiveRoute = jest.mocked(Navigation.getActiveRoute); afterAll(() => { mockIsSearchTopmostFullScreenRoute.mockRestore(); @@ -13567,10 +13593,11 @@ describe('ReportUtils', () => { }); it('should handle empty string policy type gracefully', () => { - const policyWithEmptyType = { + const policyWithEmptyType: Policy = { ...createRandomPolicy(5), + // @ts-expect-error -- empty type intentionally exercises runtime handling. type: '', - } as unknown as Policy; + }; expect(shouldEnableNegative(chatReport, policyWithEmptyType)).toBe(false); }); }); @@ -18909,11 +18936,7 @@ describe('ReportUtils', () => { describe('getAddExpenseDropdownOptions', () => { const mockTranslate: LocaleContextProps['translate'] = (path, ...params) => translate(CONST.LOCALES.EN, path, ...params); - const mockIcons = { - Location: jest.fn() as unknown as IconAsset, - ReceiptPlus: jest.fn() as unknown as IconAsset, - Plus: jest.fn() as unknown as IconAsset, - }; + const mockIcons = createMock>({Location: jest.fn(), ReceiptPlus: jest.fn(), Plus: jest.fn()}); const mockIouReportID = '12345'; it('should return exactly 3 dropdown options', () => { @@ -19483,7 +19506,8 @@ describe('ReportUtils', () => { it('should return empty string for unknown column', () => { const transaction = createMockTransaction(); - const result = getTransactionSortValue(transaction, 'UNKNOWN_COLUMN' as typeof CONST.SEARCH.TABLE_COLUMNS.DATE, mockReport, mockPolicy); + // @ts-expect-error -- unknown column intentionally exercises the default branch. + const result = getTransactionSortValue(transaction, 'UNKNOWN_COLUMN', mockReport, mockPolicy); expect(result).toBe(''); }); }); @@ -19958,7 +19982,11 @@ describe('ReportUtils', () => { const currency = 'GBP'; const action = buildOptimisticCancelPaymentReportAction(expenseReportID, amount, currency, currentUserAccountID); - expect(getOriginalMessage(action as ReportAction)).toMatchObject({ + if (!isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_DEQUEUED)) { + throw new Error('Expected a reimbursement dequeued report action'); + } + + expect(getOriginalMessage(action)).toMatchObject({ expenseReportID, amount, currency, @@ -20052,9 +20080,11 @@ describe('ReportUtils', () => { it('should set message with the emailClosingReport as the first text', () => { const emailClosingReport = 'admin@company.com'; const action = buildOptimisticClosedReportAction(emailClosingReport, 'Test Policy', currentUserAccountID); - const messages = action.message as Array<{type: string; style: string; text: string}>; + if (!isClosedReportActionMessageArray(action.message)) { + throw new Error('Expected closed report action message to be an array'); + } - expect(messages.at(0)).toMatchObject({ + expect(action.message.at(0)).toMatchObject({ type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', text: emailClosingReport, diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 1b1e2657ab42..5876a895fabf 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -16,6 +16,7 @@ import type {TransactionViolation} from '../../src/types/onyx/TransactionViolati import * as TransactionUtils from '../../src/libs/TransactionUtils'; import createRandomPolicy, {createCategoryTaxExpenseRules} from '../utils/collections/policies'; import {createRandomReport} from '../utils/collections/reports'; +import createMock from '../utils/createMock'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; function generateTransaction(values: Partial = {}): Transaction { @@ -261,19 +262,19 @@ describe('TransactionUtils', () => { describe('getIsFromGlobalCreate', () => { it('returns true when isFromFloatingActionButton is true', () => { - expect(TransactionUtils.getIsFromGlobalCreate({isFromFloatingActionButton: true} as Transaction)).toBe(true); + expect(TransactionUtils.getIsFromGlobalCreate(createMock({isFromFloatingActionButton: true}))).toBe(true); }); it('returns false when isFromFloatingActionButton is explicitly false (FAB takes precedence over isFromGlobalCreate via ?? semantics)', () => { - expect(TransactionUtils.getIsFromGlobalCreate({isFromFloatingActionButton: false, isFromGlobalCreate: true} as Transaction)).toBe(false); + expect(TransactionUtils.getIsFromGlobalCreate(createMock({isFromFloatingActionButton: false, isFromGlobalCreate: true}))).toBe(false); }); it('falls back to isFromGlobalCreate when isFromFloatingActionButton is undefined', () => { - expect(TransactionUtils.getIsFromGlobalCreate({isFromGlobalCreate: true} as Transaction)).toBe(true); + expect(TransactionUtils.getIsFromGlobalCreate(createMock({isFromGlobalCreate: true}))).toBe(true); }); it('returns undefined when both flags are absent', () => { - expect(TransactionUtils.getIsFromGlobalCreate({} as Transaction)).toBeUndefined(); + expect(TransactionUtils.getIsFromGlobalCreate(createMock({}))).toBeUndefined(); }); it('returns undefined when the transaction is undefined', () => { @@ -681,7 +682,8 @@ describe('TransactionUtils', () => { describe('getTransactionType', () => { it('returns card when the transaction is null', () => { - expect(TransactionUtils.getTransactionType(null as unknown as Transaction)).toBe(CONST.SEARCH.TRANSACTION_TYPE.CASH); + // @ts-expect-error -- null intentionally exercises the runtime fallback. + expect(TransactionUtils.getTransactionType(null)).toBe(CONST.SEARCH.TRANSACTION_TYPE.CASH); }); it('returns distance when the transaction iouRequestType is a distance type', () => { @@ -702,9 +704,9 @@ describe('TransactionUtils', () => { }); it('returns cash when the card has a cash card name', () => { - const card = { + const card = createMock({ cardName: CONST.COMPANY_CARDS.CARD_NAME.CASH, - } as Card; + }); const transaction = generateTransaction({ cardID: 101, }); @@ -829,7 +831,7 @@ describe('TransactionUtils', () => { }); it('should return true when a broken connection violation exists for one transaction and the user is the policy member', () => { - const policy = {role: CONST.POLICY.ROLE.USER} as Policy; + const policy = createMock({role: CONST.POLICY.ROLE.USER}); const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(undefined, policy, transactionViolations); @@ -837,11 +839,11 @@ describe('TransactionUtils', () => { }); it('should return true when a broken connection violation exists for any of the provided transactions and the user is the policy member', () => { - const policy = { + const policy = createMock({ role: CONST.POLICY.ROLE.USER, autoReporting: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, - } as Policy; + }); const transaction1 = generateTransaction(); const transaction2 = generateTransaction(); const transactionViolations = { @@ -867,7 +869,7 @@ describe('TransactionUtils', () => { }); it('should return true when a broken connection violation exists and the user is the policy admin and the expense submitter', () => { - const policy = {role: CONST.POLICY.ROLE.ADMIN} as Policy; + const policy = createMock({role: CONST.POLICY.ROLE.ADMIN}); const report = processingReport; const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(report, policy, transactionViolations); @@ -876,7 +878,7 @@ describe('TransactionUtils', () => { }); it('should return true when a broken connection violation exists, the user is the policy admin and the expense report is in the open state', () => { - const policy = {role: CONST.POLICY.ROLE.ADMIN} as Policy; + const policy = createMock({role: CONST.POLICY.ROLE.ADMIN}); const report = secondUserOpenReport; const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(report, policy, transactionViolations); @@ -885,7 +887,7 @@ describe('TransactionUtils', () => { }); it('should return true when a broken connection violation exists, the user is the policy admin, the expense report is in the processing state and instant submit is enabled', () => { - const policy = {role: CONST.POLICY.ROLE.ADMIN, autoReporting: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT} as Policy; + const policy = createMock({role: CONST.POLICY.ROLE.ADMIN, autoReporting: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT}); const report = processingReport; const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(report, policy, transactionViolations); @@ -894,7 +896,7 @@ describe('TransactionUtils', () => { }); it('should return false when a broken connection violation exists, the user is the policy admin but the expense report is in the approved state', () => { - const policy = {role: CONST.POLICY.ROLE.ADMIN} as Policy; + const policy = createMock({role: CONST.POLICY.ROLE.ADMIN}); const report = approvedReport; const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(report, policy, transactionViolations); @@ -1562,7 +1564,8 @@ describe('TransactionUtils', () => { const transaction = generateTransaction({ reportID: FAKE_OPEN_REPORT_ID, comment: { - attendees: Object.fromEntries(attendeesArray.entries()) as unknown as Attendee[], + // @ts-expect-error -- plain-object attendees intentionally exercise the legacy serialized shape. + attendees: Object.fromEntries(attendeesArray.entries()), }, }); @@ -1695,7 +1698,8 @@ describe('TransactionUtils', () => { const transaction = generateTransaction({ reportID: FAKE_OPEN_REPORT_ID, comment: { - attendees: Object.fromEntries(attendeesArray.entries()) as unknown as Attendee[], + // @ts-expect-error -- plain-object attendees intentionally exercise the legacy serialized shape. + attendees: Object.fromEntries(attendeesArray.entries()), }, }); @@ -1712,7 +1716,8 @@ describe('TransactionUtils', () => { comment: { attendees: [], }, - modifiedAttendees: Object.fromEntries(modifiedAttendeesArray.entries()) as unknown as Attendee[], + // @ts-expect-error -- plain-object attendees intentionally exercise the legacy serialized shape. + modifiedAttendees: Object.fromEntries(modifiedAttendeesArray.entries()), }); const result = TransactionUtils.getAttendees(transaction); @@ -1725,7 +1730,8 @@ describe('TransactionUtils', () => { const transaction = generateTransaction({ reportID: FAKE_OPEN_REPORT_ID, comment: { - attendees: {} as unknown as Attendee[], + // @ts-expect-error -- an empty object intentionally models legacy serialized attendees. + attendees: {}, }, }); @@ -1741,7 +1747,8 @@ describe('TransactionUtils', () => { comment: { attendees: [], }, - modifiedAttendees: {} as unknown as Attendee[], + // @ts-expect-error -- an empty object intentionally models legacy serialized attendees. + modifiedAttendees: {}, }); const result = TransactionUtils.getAttendees(transaction, {email: CURRENT_USER_EMAIL, avatarUrl: '', displayName: ''}); @@ -3723,7 +3730,8 @@ describe('TransactionUtils', () => { describe('when iouRequestType is null', () => { it('returns false', () => { - const transaction = {...generateTransaction(), iouRequestType: null} as unknown as Transaction; + const transaction = {...generateTransaction(), iouRequestType: null}; + // @ts-expect-error -- null intentionally models a legacy runtime value. expect(TransactionUtils.isDistanceRequest(transaction)).toBe(false); }); }); @@ -3768,7 +3776,8 @@ describe('TransactionUtils', () => { describe('when iouRequestType is null', () => { it('returns false', () => { - const transaction = {...generateTransaction(), iouRequestType: null} as unknown as Transaction; + const transaction = {...generateTransaction(), iouRequestType: null}; + // @ts-expect-error -- null intentionally models a legacy runtime value. expect(TransactionUtils[fn](transaction)).toBe(false); }); }); @@ -3801,7 +3810,8 @@ describe('TransactionUtils', () => { describe('when iouRequestType is null', () => { it('returns manual as the fallback', () => { - const transaction = {...generateTransaction(), iouRequestType: null} as unknown as Transaction; + const transaction = {...generateTransaction(), iouRequestType: null}; + // @ts-expect-error -- null intentionally models a legacy runtime value. expect(TransactionUtils.getRequestType(transaction)).toBe(CONST.IOU.REQUEST_TYPE.MANUAL); }); }); diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index 6206de7af95a..31cc3305d904 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -132,6 +132,18 @@ const duplicatedTransactionViolation = { type: CONST.VIOLATION_TYPES.WARNING, }; +function getTransactionViolationsFromResult(result: ReturnType) { + if (result.onyxMethod !== Onyx.METHOD.SET) { + throw new Error('Expected a SET transaction violation update with a value'); + } + + if (result.value === null || result.value === undefined) { + throw new Error('Expected a SET transaction violation update with a value'); + } + + return result.value; +} + describe('getViolationsOnyxData', () => { let transaction: Transaction; let transactionViolations: TransactionViolation[]; @@ -150,7 +162,7 @@ describe('getViolationsOnyxData', () => { currency: CONST.CURRENCY.USD, }; transactionViolations = []; - policy = {requiresTag: false, requiresCategory: false} as Policy; + policy = createMock({requiresTag: false, requiresCategory: false}); policyTags = {}; policyCategories = {}; }); @@ -904,7 +916,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = result.value as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); const itemizedReceiptViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED); expect(itemizedReceiptViolation).toBeDefined(); expect(itemizedReceiptViolation?.type).toBe(CONST.VIOLATION_TYPES.VIOLATION); @@ -926,7 +938,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = result.value as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); const foundReceiptRequiredViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.RECEIPT_REQUIRED); expect(foundReceiptRequiredViolation).toBeUndefined(); }); @@ -947,7 +959,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = result.value as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); const receiptViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.RECEIPT_REQUIRED); const itemizedReceiptViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED); // Should have itemized receipt violation but NOT regular receipt violation @@ -971,7 +983,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = result.value as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); const itemizedReceiptViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED); expect(itemizedReceiptViolation).toBeUndefined(); }); @@ -992,7 +1004,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = result.value as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); const itemizedReceiptViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED); expect(itemizedReceiptViolation).toBeUndefined(); }); @@ -1044,7 +1056,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = result.value as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); const itemizedReceiptViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED); expect(itemizedReceiptViolation).toBeDefined(); expect(itemizedReceiptViolation?.data).toBeUndefined(); // Category-level violations don't have data @@ -1064,7 +1076,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = result.value as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); const itemizedReceiptViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED); expect(itemizedReceiptViolation).toBeUndefined(); // Category "Never" should override policy }); @@ -1083,7 +1095,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = result.value as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); const itemizedReceiptViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED); expect(itemizedReceiptViolation).toBeDefined(); // Should follow policy threshold }); @@ -1108,7 +1120,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = result.value as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); // Then the itemized violation should be removed and replaced with receiptRequired because the policy still requires receipts const itemizedViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED); @@ -1136,7 +1148,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = result.value as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); // Then the violation should have updated threshold data to reflect the current policy settings const itemizedViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED); @@ -1165,7 +1177,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = result.value as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); // Then itemized should supersede receipt because itemized is more restrictive const receiptViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.RECEIPT_REQUIRED); @@ -1194,7 +1206,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = result.value as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); // Then no receipt violations should exist because category overrides take precedence over policy settings const itemizedViolation = violations.find((v: TransactionViolation) => v.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED); @@ -1420,7 +1432,7 @@ describe('getViolationsOnyxData', () => { }; const result = ViolationsUtils.getViolationsOnyxData({ ownerLogin: undefined, - updatedTransaction: transactionWithModifiedDetails as unknown as Transaction, + updatedTransaction: createMock(transactionWithModifiedDetails), transactionViolations, policy, policyTagList: policyTags, @@ -2002,10 +2014,10 @@ describe('getViolationsOnyxData', () => { }, }; transaction.category = 'Meals'; - iouReport = { + iouReport = createMock({ reportID: '1234', ownerAccountID, - } as Report; + }); }); it('should add missingAttendees violation when no attendees are present', () => { @@ -2746,7 +2758,7 @@ describe('getViolationsOnyxData', () => { // Pass a `vendors` array to control the synced list, or `null` to simulate the list still // hydrating (`data.vendors` absent, so `isMatchingVendorListLoaded` returns false). const policyWithQBOVendorFeature = (vendors: Array<{id: string; name: string; currency: string}> | null = [{id: 'v-active', name: 'Acme Co', currency: 'USD'}]) => - ({ + createMock({ requiresTag: false, requiresCategory: false, connections: { @@ -2755,7 +2767,7 @@ describe('getViolationsOnyxData', () => { data: vendors ? {vendors} : {}, }, }, - }) as unknown as Policy; + }); beforeEach(async () => { // Default to beta-enabled so the four branches of the violation logic are reachable. @@ -2804,7 +2816,8 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - expect((result.value as TransactionViolation[]).filter((v) => v.name === CONST.VIOLATIONS.INACTIVE_VENDOR)).toHaveLength(1); + const violations = getTransactionViolationsFromResult(result); + expect(violations.filter((v) => v.name === CONST.VIOLATIONS.INACTIVE_VENDOR)).toHaveLength(1); }); it('removes an existing violation when the vendor is restored in the policy list', () => { @@ -2840,7 +2853,7 @@ describe('getViolationsOnyxData', () => { }); it('removes an existing violation when the vendor feature is disabled (QBO export type changed)', () => { - policy = { + policy = createMock({ requiresTag: false, requiresCategory: false, connections: { @@ -2849,7 +2862,7 @@ describe('getViolationsOnyxData', () => { data: {vendors: [{id: 'v-active', name: 'Acme Co', currency: 'USD'}]}, }, }, - } as unknown as Policy; + }); transaction.comment = {...transaction.comment, vendor: {externalID: 'v-active', isManuallySet: true}}; const result = ViolationsUtils.getViolationsOnyxData({ ownerLogin: undefined, @@ -3161,7 +3174,7 @@ describe('getViolationsOnyxData', () => { isInvoiceTransaction: false, shouldRemoveRejectedExpenseViolation: true, }); - const violations = (result.value ?? []) as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); expect(violations.some((v) => v.name === CONST.VIOLATIONS.AUTO_REPORTED_REJECTED_EXPENSE)).toBe(false); }); @@ -3176,7 +3189,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, }); - const violations = (result.value ?? []) as TransactionViolation[]; + const violations = getTransactionViolationsFromResult(result); expect(violations.some((v) => v.name === CONST.VIOLATIONS.AUTO_REPORTED_REJECTED_EXPENSE)).toBe(true); }); }); @@ -3614,29 +3627,28 @@ describe('hasVisibleViolationsForUser', () => { const testTransactionID = 'test-transaction-123'; const testPolicyID = 'test-policy-123'; - const mockReport = { + const mockReport = createMock({ reportID: testReportID, ownerAccountID: submitterAccountID, policyID: testPolicyID, stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN, - } as Report; + }); - const mockPolicy = { + const mockPolicy = createMock({ id: testPolicyID, role: CONST.POLICY.ROLE.ADMIN, type: CONST.POLICY.TYPE.TEAM, - } as Policy; + }); - const mockTransaction = { + const mockTransaction = createMock({ transactionID: testTransactionID, reportID: testReportID, - accountID: submitterAccountID, amount: 1000, created: '2023-01-01', currency: 'USD', merchant: 'Test Merchant', - } as Transaction; + }); beforeEach(() => { Onyx.set(ONYXKEYS.SESSION, {accountID: submitterAccountID}); @@ -3733,15 +3745,14 @@ describe('hasVisibleViolationsForUser', () => { it('should handle multiple transactions correctly', () => { const secondTransactionID = 'test-transaction-456'; - const secondTransaction = { + const secondTransaction = createMock({ transactionID: secondTransactionID, reportID: testReportID, - accountID: submitterAccountID, amount: 2000, created: '2023-01-02', currency: 'USD', merchant: 'Test Merchant 2', - } as Transaction; + }); const violations = { [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${testTransactionID}`]: [ diff --git a/tests/unit/hooks/useAssignCard.test.ts b/tests/unit/hooks/useAssignCard.test.ts index 9d1904f9addd..158377e293f7 100644 --- a/tests/unit/hooks/useAssignCard.test.ts +++ b/tests/unit/hooks/useAssignCard.test.ts @@ -10,20 +10,21 @@ import usePolicy from '@hooks/usePolicy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {CompanyCardFeed, CompanyCardFeedWithDomainID} from '@src/types/onyx/CardFeeds'; +import type {CombinedCardFeeds, CompanyCardFeedWithDomainID, Policy} from '@src/types/onyx'; import Onyx from 'react-native-onyx'; +import createMock from '../../utils/createMock'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; const mockPolicyID = 'policy123'; const workspaceAccountID = 11111111; // Custom feed (VCF) - commercial feed -const mockCustomFeed: CompanyCardFeedWithDomainID = `${CONST.COMPANY_CARD.FEED_BANK_NAME.VISA}#${workspaceAccountID}` as CompanyCardFeedWithDomainID; +const mockCustomFeed: CompanyCardFeedWithDomainID = `${CONST.COMPANY_CARD.FEED_BANK_NAME.VISA}#${workspaceAccountID}`; -// Direct feed (Plaid) - has accountList -const mockPlaidFeed: CompanyCardFeedWithDomainID = `plaid.ins_123#${workspaceAccountID}` as CompanyCardFeedWithDomainID; +// Direct feed (Chase) - has accountList +const mockDirectFeed: CompanyCardFeedWithDomainID = `${CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE}#${workspaceAccountID}`; const mockCustomFeedData = { [mockCustomFeed]: { @@ -31,31 +32,31 @@ const mockCustomFeedData = { pending: false, domainID: workspaceAccountID, customFeedName: 'Custom VCF feed', - feed: CONST.COMPANY_CARD.FEED_BANK_NAME.VISA as CompanyCardFeed, + feed: CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, }, }; -const mockPlaidFeedData = { - [mockPlaidFeed]: { +const mockDirectFeedData: CombinedCardFeeds = { + [mockDirectFeed]: { liabilityType: 'corporate', pending: false, domainID: workspaceAccountID, - customFeedName: 'Plaid Bank cards', - feed: 'plaid.ins_123' as CompanyCardFeed, - accountList: ['Plaid Checking 0000', 'Plaid Credit Card 3333'], + customFeedName: 'Chase Bank cards', + feed: CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE, + accountList: ['Chase Checking 0000', 'Chase Credit Card 3333'], credentials: 'xxxxx', expiration: Date.now() / 1000 + 86400, // expires tomorrow }, }; -const mockPolicy = { +const mockPolicy = createMock({ id: mockPolicyID, policyAccountID: workspaceAccountID, employeeList: { 'user1@example.com': {email: 'user1@example.com'}, 'user2@example.com': {email: 'user2@example.com'}, }, -}; +}); // Mock useOnyx hook jest.mock('@hooks/useOnyx', () => ({ @@ -129,10 +130,10 @@ describe('useAssignCard', () => { jest.clearAllMocks(); // Default mock returns - (usePolicy as jest.Mock).mockReturnValue(mockPolicy); - (useNetwork as jest.Mock).mockReturnValue({isOffline: false, onReconnect: jest.fn()}); - (useIsAllowedToIssueCompanyCard as jest.Mock).mockReturnValue(true); - (useOnyx as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]); + jest.mocked(usePolicy).mockReturnValue(mockPolicy); + jest.mocked(useNetwork).mockReturnValue({isOffline: false}); + jest.mocked(useIsAllowedToIssueCompanyCard).mockReturnValue(true); + jest.mocked(useOnyx).mockReturnValue([undefined, {status: 'loaded'}]); }); afterEach(async () => { @@ -148,7 +149,7 @@ describe('useAssignCard', () => { pending: true, }, }; - (useCardFeeds as jest.Mock).mockReturnValue([pendingFeedData, {status: 'loaded'}, undefined]); + jest.mocked(useCardFeeds).mockReturnValue([pendingFeedData, {status: 'loaded'}, undefined, {}, workspaceAccountID]); const {result} = renderHook(() => useAssignCard({ @@ -162,8 +163,8 @@ describe('useAssignCard', () => { }); it('should return isAssigningCardDisabled true when user is not allowed to issue cards', () => { - (useCardFeeds as jest.Mock).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined]); - (useIsAllowedToIssueCompanyCard as jest.Mock).mockReturnValue(false); + jest.mocked(useCardFeeds).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined, {}, workspaceAccountID]); + jest.mocked(useIsAllowedToIssueCompanyCard).mockReturnValue(false); const {result} = renderHook(() => useAssignCard({ @@ -177,7 +178,7 @@ describe('useAssignCard', () => { }); it('should return isAssigningCardDisabled false when all conditions are met', () => { - (useCardFeeds as jest.Mock).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined]); + jest.mocked(useCardFeeds).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined, {}, workspaceAccountID]); const {result} = renderHook(() => useAssignCard({ @@ -193,25 +194,25 @@ describe('useAssignCard', () => { describe('assignCard function - offline handling', () => { it('should show offline modal for direct feed when offline', () => { - (useCardFeeds as jest.Mock).mockReturnValue([mockPlaidFeedData, {status: 'loaded'}, undefined]); - (useNetwork as jest.Mock).mockReturnValue({isOffline: true, onReconnect: jest.fn()}); + jest.mocked(useCardFeeds).mockReturnValue([mockDirectFeedData, {status: 'loaded'}, undefined, {}, workspaceAccountID]); + jest.mocked(useNetwork).mockReturnValue({isOffline: true}); const {result} = renderHook(() => useAssignCard({ - feedName: mockPlaidFeed, + feedName: mockDirectFeed, policyID: mockPolicyID, setShouldShowOfflineModal: mockSetShouldShowOfflineModal, }), ); - result.current.assignCard('Plaid Checking 0000', 'Plaid Checking 0000'); + result.current.assignCard('Chase Checking 0000', 'Chase Checking 0000'); expect(mockSetShouldShowOfflineModal).toHaveBeenCalledWith(true); }); it('should not show offline modal for commercial feed when offline', () => { - (useCardFeeds as jest.Mock).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined]); - (useNetwork as jest.Mock).mockReturnValue({isOffline: true, onReconnect: jest.fn()}); + jest.mocked(useCardFeeds).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined, {}, workspaceAccountID]); + jest.mocked(useNetwork).mockReturnValue({isOffline: true}); const {result} = renderHook(() => useAssignCard({ @@ -230,7 +231,7 @@ describe('useAssignCard', () => { describe('assignCard function - card identifiers', () => { it('should accept different cardName and cardID for commercial feeds', () => { - (useCardFeeds as jest.Mock).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined]); + jest.mocked(useCardFeeds).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined, {}, workspaceAccountID]); const {result} = renderHook(() => useAssignCard({ @@ -251,18 +252,18 @@ describe('useAssignCard', () => { }); it('should accept same cardName and cardID for direct feeds', () => { - (useCardFeeds as jest.Mock).mockReturnValue([mockPlaidFeedData, {status: 'loaded'}, undefined]); + jest.mocked(useCardFeeds).mockReturnValue([mockDirectFeedData, {status: 'loaded'}, undefined, {}, workspaceAccountID]); const {result} = renderHook(() => useAssignCard({ - feedName: mockPlaidFeed, + feedName: mockDirectFeed, policyID: mockPolicyID, setShouldShowOfflineModal: mockSetShouldShowOfflineModal, }), ); - const cardName = 'Plaid Checking 0000'; - const cardID = 'Plaid Checking 0000'; + const cardName = 'Chase Checking 0000'; + const cardID = 'Chase Checking 0000'; // For direct feeds, cardName equals cardID expect(cardName).toBe(cardID); diff --git a/tests/unit/hooks/useFilterFormValues.test.ts b/tests/unit/hooks/useFilterFormValues.test.ts index f80bd015341d..c4c7db7cfd05 100644 --- a/tests/unit/hooks/useFilterFormValues.test.ts +++ b/tests/unit/hooks/useFilterFormValues.test.ts @@ -5,10 +5,12 @@ import {exportedToPoliciesSelector} from '@hooks/useExportedToFilterOptions'; import {policiesSelector, policyCategoriesSelector, policyTagsSelector} from '@hooks/useFilterFormValues'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyCategories, PolicyTagLists} from '@src/types/onyx'; +import type {Policy, PolicyCategories, PolicyReportField, PolicyTagLists} from '@src/types/onyx'; import type {OnyxCollection} from 'react-native-onyx'; +import createMock from '../../utils/createMock'; + const POLICY_KEY = `${ONYXKEYS.COLLECTION.POLICY}1`; const POLICY_KEY_2 = `${ONYXKEYS.COLLECTION.POLICY}2`; const CATEGORIES_KEY = `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}1`; @@ -25,8 +27,8 @@ describe('useFilterFormValues selectors', () => { it('extracts only taxRates from each policy', () => { const taxRates = {name: 'Tax', defaultValue: '10%', taxes: {}}; const policies: OnyxCollection = { - [POLICY_KEY]: {id: '1', name: 'Policy 1', taxRates, employeeList: [{email: 'a@b.com'}]} as unknown as Policy, - [POLICY_KEY_2]: {id: '2', name: 'Policy 2', taxRates: undefined, employeeList: [{email: 'c@d.com'}]} as unknown as Policy, + [POLICY_KEY]: createMock({id: '1', name: 'Policy 1', taxRates}), + [POLICY_KEY_2]: createMock({id: '2', name: 'Policy 2', taxRates: undefined}), }; const result = policiesSelector(policies); @@ -41,7 +43,7 @@ describe('useFilterFormValues selectors', () => { it('skips undefined policy entries', () => { const policies: OnyxCollection = { - [POLICY_KEY]: {id: '1', taxRates: {}} as unknown as Policy, + [POLICY_KEY]: createMock({id: '1', taxRates: {}}), [POLICY_KEY_2]: undefined, }; @@ -59,10 +61,10 @@ describe('useFilterFormValues selectors', () => { it('extracts only category names', () => { const categories: OnyxCollection = { - [CATEGORIES_KEY]: { + [CATEGORIES_KEY]: createMock({ Food: {name: 'Food', enabled: true, unencodedName: 'Food', areCommentsRequired: false, externalID: '123', origin: 'abc'}, Travel: {name: 'Travel', enabled: false, unencodedName: 'Travel', areCommentsRequired: true, externalID: '456', origin: 'def'}, - } as unknown as PolicyCategories, + }), }; const result = policyCategoriesSelector(categories); @@ -75,9 +77,9 @@ describe('useFilterFormValues selectors', () => { it('skips undefined collection entries', () => { const categories: OnyxCollection = { - [CATEGORIES_KEY]: { - Food: {name: 'Food'} as PolicyCategories[string], - } as unknown as PolicyCategories, + [CATEGORIES_KEY]: createMock({ + Food: {name: 'Food'}, + }), [CATEGORIES_KEY_2]: undefined, }; @@ -95,7 +97,7 @@ describe('useFilterFormValues selectors', () => { it('extracts only tag names', () => { const tags: OnyxCollection = { - [TAGS_KEY]: { + [TAGS_KEY]: createMock({ Department: { name: 'Department', required: true, @@ -104,7 +106,7 @@ describe('useFilterFormValues selectors', () => { Marketing: {name: 'Marketing', enabled: false}, }, }, - } as unknown as PolicyTagLists, + }), }; const result = policyTagsSelector(tags); @@ -121,13 +123,13 @@ describe('useFilterFormValues selectors', () => { it('skips undefined collection entries', () => { const tags: OnyxCollection = { - [TAGS_KEY]: { + [TAGS_KEY]: createMock({ Department: { tags: { Engineering: {name: 'Engineering'}, }, }, - } as unknown as PolicyTagLists, + }), [TAGS_KEY_2]: undefined, }; @@ -143,12 +145,12 @@ describe('useFilterFormValues selectors', () => { it('handles tag lists with empty tags object', () => { const tags: OnyxCollection = { - [TAGS_KEY]: { + [TAGS_KEY]: createMock({ Department: { name: 'Department', tags: {}, }, - } as unknown as PolicyTagLists, + }), }; const result = policyTagsSelector(tags); @@ -168,7 +170,7 @@ describe('useFilterFormValues selectors', () => { const connections = {quickbooksOnline: {config: {}, lastSync: {isConnected: true}}}; const exportLayouts = {template1: {name: 'Template 1'}}; const policies: OnyxCollection = { - [POLICY_KEY]: { + [POLICY_KEY]: createMock({ id: '1', name: 'Policy 1', connections, @@ -177,7 +179,7 @@ describe('useFilterFormValues selectors', () => { employeeList: {'a@b.com': {email: 'a@b.com', role: 'user'}}, taxRates: {name: 'Tax', taxes: {}}, customUnits: {unit1: {name: 'Miles'}}, - } as unknown as Policy, + }), }; const result = exportedToPoliciesSelector(policies); @@ -190,7 +192,7 @@ describe('useFilterFormValues selectors', () => { it('skips undefined policy entries', () => { const policies: OnyxCollection = { - [POLICY_KEY]: {id: '1', name: 'P1', connections: undefined, exportLayouts: undefined} as unknown as Policy, + [POLICY_KEY]: createMock({id: '1', name: 'P1', connections: undefined, exportLayouts: undefined}), [POLICY_KEY_2]: undefined, }; @@ -211,7 +213,7 @@ describe('useFilterFormValues selectors', () => { // eslint-disable-next-line @typescript-eslint/naming-convention {'a@b.com': {email: 'a@b.com', role: 'admin'}}; const policies: OnyxCollection = { - [POLICY_KEY]: { + [POLICY_KEY]: createMock({ id: '1', type: 'team', role: 'admin', @@ -226,7 +228,7 @@ describe('useFilterFormValues selectors', () => { taxRates: {name: 'Tax', taxes: {}}, customUnits: {unit1: {name: 'Miles'}}, fieldList: {field1: {name: 'Field 1'}}, - } as unknown as Policy, + }), }; const result = typeOptionsPoliciesSelector(policies); @@ -251,7 +253,7 @@ describe('useFilterFormValues selectors', () => { it('skips undefined policy entries', () => { const policies: OnyxCollection = { - [POLICY_KEY]: {id: '1', type: 'team'} as unknown as Policy, + [POLICY_KEY]: createMock({id: '1', type: 'team'}), [POLICY_KEY_2]: undefined, }; @@ -273,9 +275,9 @@ describe('useFilterFormValues selectors', () => { {'a@b.com': {email: 'a@b.com', role: 'admin'}}; const taxRates = {name: 'Tax', defaultValue: '10%', taxes: {tax1: {name: 'VAT', value: '20%'}}}; const tax = {trackingEnabled: true}; - const fieldList = {field1: {name: 'Project', type: 'text'}}; + const fieldList = {field1: createMock({name: 'Project', type: 'text'})}; const policies: OnyxCollection = { - [POLICY_KEY]: { + [POLICY_KEY]: createMock({ id: '1', name: 'Policy 1', type: 'team', @@ -297,7 +299,7 @@ describe('useFilterFormValues selectors', () => { customUnits: {unit1: {name: 'Miles'}}, rules: {approvalRules: []}, exportLayouts: {template1: {name: 'T1'}}, - } as unknown as Policy, + }), }; const result = advancedSearchPoliciesSelector(policies); @@ -329,7 +331,7 @@ describe('useFilterFormValues selectors', () => { it('skips undefined policy entries', () => { const policies: OnyxCollection = { - [POLICY_KEY]: {id: '1', name: 'P1', type: 'team'} as unknown as Policy, + [POLICY_KEY]: createMock({id: '1', name: 'P1', type: 'team'}), [POLICY_KEY_2]: undefined, }; diff --git a/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts b/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts index fea8f9091d8b..4d8d121d6384 100644 --- a/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts +++ b/tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts @@ -20,6 +20,7 @@ import Onyx from 'react-native-onyx'; import type * as MockUsePaymentContextUtil from '../../utils/mockUsePaymentContext'; import createRandomTransaction, {createRandomDistanceRequestTransaction} from '../../utils/collections/transaction'; +import createMock from '../../utils/createMock'; jest.mock('@libs/actions/IOU/Duplicate', () => ({ bulkDuplicateExpenses: jest.fn(), @@ -404,7 +405,7 @@ describe('useSearchBulkActions - duplicate option', () => { it('should show duplicate option for a Per Diem expense with dates', async () => { const txnID = '1500'; const policyID = 'policy1'; - mockDefaultExpensePolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'}); const txn = { ...createRandomTransaction(1), transactionID: txnID, @@ -450,7 +451,7 @@ describe('useSearchBulkActions - duplicate option', () => { it('should show duplicate option for a mix of cash, Per Diem, and Distance expenses', async () => { const policyID = 'policy1'; - mockDefaultExpensePolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'}); const cashTxn = {...createRandomTransaction(1), transactionID: '1700', managedCard: false}; const perDiemTxn = { ...createRandomTransaction(2), @@ -632,7 +633,7 @@ describe('useSearchBulkActions - duplicate option', () => { it('should pass defaultExpensePolicy as targetPolicy when available', async () => { const policyID = 'POLICY_TEAM_1'; - const teamPolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test Workspace'} as Policy; + const teamPolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test Workspace'}); mockDefaultExpensePolicy = teamPolicy; const txnID = '700'; @@ -680,7 +681,7 @@ describe('useSearchBulkActions - duplicate option', () => { it('should resolve policy categories and tags from defaultExpensePolicy', async () => { const policyID = 'POLICY_CAT_TEST'; - const teamPolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Category WS'} as Policy; + const teamPolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Category WS'}); mockDefaultExpensePolicy = teamPolicy; const categories: PolicyCategories = { @@ -718,7 +719,7 @@ describe('useSearchBulkActions - duplicate option', () => { it('should pass activePolicyExpenseChat as targetReport when it exists', async () => { const policyID = 'POLICY_REPORT_TEST'; - const teamPolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Report WS'} as Policy; + const teamPolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Report WS'}); mockDefaultExpensePolicy = teamPolicy; const policyExpenseChat: Report = { @@ -758,7 +759,7 @@ describe('useSearchBulkActions - duplicate option', () => { it('should duplicate expenses from multiple policies using the default expense policy data', async () => { const defaultPolicyID = 'DEFAULT_POLICY'; const otherPolicyID = 'OTHER_POLICY'; - const teamPolicy = {id: defaultPolicyID, type: CONST.POLICY.TYPE.TEAM, name: 'Default WS'} as Policy; + const teamPolicy = createMock({id: defaultPolicyID, type: CONST.POLICY.TYPE.TEAM, name: 'Default WS'}); mockDefaultExpensePolicy = teamPolicy; const defaultCategories: PolicyCategories = { @@ -900,7 +901,7 @@ describe('useSearchBulkActions - duplicate option', () => { it('should not show duplicate option for per-diem expense on non-default workspace', async () => { const defaultPolicyID = 'DEFAULT_POLICY'; const otherPolicyID = 'OTHER_POLICY'; - mockDefaultExpensePolicy = {id: defaultPolicyID, type: CONST.POLICY.TYPE.TEAM, name: 'Default WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: defaultPolicyID, type: CONST.POLICY.TYPE.TEAM, name: 'Default WS'}); const txnID = '2100'; const txn = { @@ -971,7 +972,7 @@ describe('useSearchBulkActions - duplicate option', () => { it('should not show duplicate option for distance expense from DM chat', async () => { const defaultPolicyID = 'DEFAULT_POLICY'; - mockDefaultExpensePolicy = {id: defaultPolicyID, type: CONST.POLICY.TYPE.TEAM, name: 'Default WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: defaultPolicyID, type: CONST.POLICY.TYPE.TEAM, name: 'Default WS'}); const txnID = '2400'; const expenseReportID = 'r_expense_dm'; @@ -1037,7 +1038,7 @@ describe('useSearchBulkActions - duplicate option', () => { it('should not show duplicate option for unreported per diem expense (no policyID on report)', async () => { const defaultPolicyID = 'DEFAULT_POLICY'; - mockDefaultExpensePolicy = {id: defaultPolicyID, type: CONST.POLICY.TYPE.TEAM, name: 'Default WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: defaultPolicyID, type: CONST.POLICY.TYPE.TEAM, name: 'Default WS'}); const txnID = '2500'; const txn = { @@ -1065,7 +1066,7 @@ describe('useSearchBulkActions - duplicate option', () => { it('should not show duplicate option for unreported distance expense when policy expense chat exists', async () => { const defaultPolicyID = 'DEFAULT_POLICY'; - mockDefaultExpensePolicy = {id: defaultPolicyID, type: CONST.POLICY.TYPE.TEAM, name: 'Default WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: defaultPolicyID, type: CONST.POLICY.TYPE.TEAM, name: 'Default WS'}); const txnID = '2600'; const txn = { @@ -1181,7 +1182,7 @@ describe('useSearchBulkActions - duplicate report option', () => { it('should show duplicate report option for a single expense report owned by current user', async () => { const policyID = 'policy1'; - mockDefaultExpensePolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'}); const report: Report = { reportID: 'report1', @@ -1205,7 +1206,7 @@ describe('useSearchBulkActions - duplicate report option', () => { it('should show duplicate report option for multiple expense reports owned by current user', async () => { const policyID = 'policy1'; - mockDefaultExpensePolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'}); for (const reportID of ['rpt1', 'rpt2', 'rpt3']) { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { @@ -1254,7 +1255,7 @@ describe('useSearchBulkActions - duplicate report option', () => { it('should not show duplicate report option on expense (non-report) search type', async () => { const policyID = 'policy1'; - mockDefaultExpensePolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'}); const report: Report = { reportID: 'report1', @@ -1276,7 +1277,7 @@ describe('useSearchBulkActions - duplicate report option', () => { it('should not show duplicate report option when current user is not the submitter', async () => { const policyID = 'policy1'; - mockDefaultExpensePolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'}); const otherUserAccountID = 9999; const report: Report = { @@ -1299,7 +1300,7 @@ describe('useSearchBulkActions - duplicate report option', () => { it('should not show duplicate report option when one selected report is not an expense report', async () => { const policyID = 'policy1'; - mockDefaultExpensePolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'}); const expenseReport: Report = { reportID: 'rpt1', @@ -1332,7 +1333,7 @@ describe('useSearchBulkActions - duplicate report option', () => { it('should not show duplicate report option when no reports are selected', async () => { const policyID = 'policy1'; - mockDefaultExpensePolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'}); mockSelectedReports = []; @@ -1343,7 +1344,7 @@ describe('useSearchBulkActions - duplicate report option', () => { it('should not show duplicate report option when one report has undefined reportID', async () => { const policyID = 'policy1'; - mockDefaultExpensePolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'}); const report: Report = { reportID: 'rpt1', @@ -1372,7 +1373,7 @@ describe('useSearchBulkActions - duplicate report option', () => { it('should call bulkDuplicateReports with correct reportIDs when invoked', async () => { const policyID = 'policy1'; - const teamPolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'} as Policy; + const teamPolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'}); mockDefaultExpensePolicy = teamPolicy; for (const reportID of ['rpt1', 'rpt2']) { @@ -1410,7 +1411,7 @@ describe('useSearchBulkActions - duplicate report option', () => { it('should clear selected transactions after invoking duplicate report', async () => { const policyID = 'policy1'; - mockDefaultExpensePolicy = {id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'} as Policy; + mockDefaultExpensePolicy = createMock({id: policyID, type: CONST.POLICY.TYPE.TEAM, name: 'Test WS'}); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}rpt1`, { reportID: 'rpt1', diff --git a/tests/unit/libs/SplitExpenseUtils.test.ts b/tests/unit/libs/SplitExpenseUtils.test.ts index 6e438f58adb0..c8dbc4005252 100644 --- a/tests/unit/libs/SplitExpenseUtils.test.ts +++ b/tests/unit/libs/SplitExpenseUtils.test.ts @@ -1,13 +1,16 @@ +import type {LocalizedTranslate} from '@components/LocaleContextProvider'; + import {computeSplitSaveErrorMessage, computeSplitWarningMessage} from '@libs/SplitExpenseUtils'; import CONST from '@src/CONST'; +import type {TranslationParameters, TranslationPaths} from '@src/languages/types'; import type {SplitExpense} from '@src/types/onyx/IOU'; import {convertToDisplayString} from '../../utils/TestHelper'; -// Minimal translate mock: returns the translation key plus any first argument for verifying +// Minimal translate implementation: returns the translation key plus any first argument for verifying // which message was chosen without relying on exact English strings. -const translate = jest.fn((key: string, ...args: unknown[]) => (args.length ? `${key}:${String(args.at(0))}` : key)); +const translate: LocalizedTranslate = (key: TPath, ...args: TranslationParameters) => (args.length ? `${key}:${String(args.at(0))}` : key); function makeSplit(amount: number, transactionID = `tx-${amount}`): SplitExpense { return {transactionID, amount, created: '2024-01-01'}; @@ -16,10 +19,6 @@ function makeSplit(amount: number, transactionID = `tx-${amount}`): SplitExpense const GREATER = 'iou.totalAmountGreaterThanOriginal'; const LESS = 'iou.totalAmountLessThanOriginal'; -beforeEach(() => { - translate.mockClear(); -}); - describe('computeSplitWarningMessage', () => { const currency = 'USD'; @@ -29,7 +28,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [], transactionDetailsAmount: 1000, currency, - translate: translate as never, + translate, convertToDisplayString, }); expect(result).toContain(LESS); @@ -41,7 +40,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [], transactionDetailsAmount: 0, currency, - translate: translate as never, + translate, convertToDisplayString, }), ).toBe(''); @@ -53,7 +52,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [makeSplit(600), makeSplit(400)], transactionDetailsAmount: 1000, currency, - translate: translate as never, + translate, convertToDisplayString, }), ).toBe(''); @@ -64,7 +63,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [makeSplit(600), makeSplit(500)], transactionDetailsAmount: 1000, currency, - translate: translate as never, + translate, convertToDisplayString, }); expect(result).toContain(GREATER); @@ -75,7 +74,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [makeSplit(300), makeSplit(200)], transactionDetailsAmount: 1000, currency, - translate: translate as never, + translate, convertToDisplayString, }); expect(result).toContain(LESS); @@ -87,7 +86,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [makeSplit(1500)], transactionDetailsAmount: 1000, currency, - translate: translate as never, + translate, convertToDisplayString, }); expect(result).toContain(GREATER); @@ -99,7 +98,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [makeSplit(1500, 'a'), makeSplit(-700, 'b')], transactionDetailsAmount: 1000, currency, - translate: translate as never, + translate, convertToDisplayString, }); // sum (800) < total (1000) → Less @@ -112,7 +111,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [makeSplit(1500, 'a'), makeSplit(-500, 'b')], transactionDetailsAmount: 1000, currency, - translate: translate as never, + translate, convertToDisplayString, }); // invalidSplit && sum !== total is false → falls to else branches, sum === total → no warning @@ -126,7 +125,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [makeSplit(600, 'a'), makeSplit(-200, 'b')], transactionDetailsAmount: 1000, currency, - translate: translate as never, + translate, convertToDisplayString, }); expect(result).toContain(LESS); @@ -138,7 +137,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [makeSplit(1200, 'a'), makeSplit(-100, 'b')], transactionDetailsAmount: 500, currency, - translate: translate as never, + translate, convertToDisplayString, }); expect(result).toContain(GREATER); @@ -150,7 +149,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [makeSplit(1200, 'a'), makeSplit(-200, 'b')], transactionDetailsAmount: 1000, currency, - translate: translate as never, + translate, convertToDisplayString, }); expect(result).toBe(''); @@ -164,7 +163,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [makeSplit(-600, 'a'), makeSplit(-600, 'b')], transactionDetailsAmount: -1000, currency, - translate: translate as never, + translate, convertToDisplayString, }); expect(result).toContain(LESS); @@ -176,7 +175,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [makeSplit(-300, 'a'), makeSplit(-200, 'b')], transactionDetailsAmount: -1000, currency, - translate: translate as never, + translate, convertToDisplayString, }); expect(result).toContain(GREATER); @@ -187,7 +186,7 @@ describe('computeSplitWarningMessage', () => { splitExpenses: [makeSplit(-400, 'a'), makeSplit(-600, 'b')], transactionDetailsAmount: -1000, currency, - translate: translate as never, + translate, convertToDisplayString, }); expect(result).toBe(''); @@ -203,7 +202,7 @@ describe('computeSplitSaveErrorMessage', () => { isDistance: false, isPerDiem: false, isCard: false, - translate: translate as never, + translate, convertToDisplayString, }; diff --git a/tests/unit/useAccountIndicatorChecksTest.ts b/tests/unit/useAccountIndicatorChecksTest.ts index 64fb613a893b..2d209fa41ad4 100644 --- a/tests/unit/useAccountIndicatorChecksTest.ts +++ b/tests/unit/useAccountIndicatorChecksTest.ts @@ -10,6 +10,7 @@ import type {OnyxMultiSetInput} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import createMock from '../utils/createMock'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; const userID = 'johndoe12@expensify.com'; @@ -30,13 +31,15 @@ describe('useAccountIndicatorChecks', () => { describe('account error statuses', () => { it('returns HAS_USER_WALLET_ERRORS when wallet has errors', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.USER_WALLET]: { - bankAccountID: 12345, - errors: {error: 'Something went wrong'}, - }, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.USER_WALLET]: { + bankAccountID: 12345, + errors: {error: 'Something went wrong'}, + }, + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -48,17 +51,19 @@ describe('useAccountIndicatorChecks', () => { it('returns HAS_PAYMENT_METHOD_ERROR when bank account has errors', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.USER_WALLET]: {}, - [ONYXKEYS.BANK_ACCOUNT_LIST]: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 12345: { - methodID: 12345, - errors: {error: 'Something went wrong'}, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.USER_WALLET]: {}, + [ONYXKEYS.BANK_ACCOUNT_LIST]: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 12345: { + methodID: 12345, + errors: {error: 'Something went wrong'}, + }, }, - }, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -70,15 +75,17 @@ describe('useAccountIndicatorChecks', () => { it('returns HAS_REIMBURSEMENT_ACCOUNT_ERRORS when reimbursement account has errors', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.USER_WALLET]: {}, - [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: { - achData: {bankAccountID: 12345}, - errors: {error: 'Something went wrong'}, - }, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.USER_WALLET]: {}, + [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: { + achData: {bankAccountID: 12345}, + errors: {error: 'Something went wrong'}, + }, + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -90,21 +97,22 @@ describe('useAccountIndicatorChecks', () => { it('returns HAS_LOGIN_LIST_ERROR when login list has errors', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.USER_WALLET]: {}, - [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, - [ONYXKEYS.LOGINS]: { - [`1_${userID}`]: { - partnerID: 1, - partnerName: 'John Doe', - partnerUserID: userID, - validatedDate: new Date().toISOString(), - errorFields: {field: {error: 'Something went wrong'}}, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.USER_WALLET]: {}, + [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, + [ONYXKEYS.LOGINS]: { + [`1_${userID}`]: { + partnerID: 1, + partnerUserID: userID, + validatedDate: new Date().toISOString(), + errorFields: {addedLogin: {error: 'Something went wrong'}}, + }, }, - }, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -116,16 +124,18 @@ describe('useAccountIndicatorChecks', () => { it('returns HAS_WALLET_TERMS_ERRORS when wallet terms has errors without chatReportID', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.USER_WALLET]: {}, - [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, - [ONYXKEYS.LOGINS]: {}, - [ONYXKEYS.WALLET_TERMS]: { - errors: {error: 'Something went wrong'}, - }, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.USER_WALLET]: {}, + [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, + [ONYXKEYS.LOGINS]: {}, + [ONYXKEYS.WALLET_TERMS]: { + errors: {error: 'Something went wrong'}, + }, + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -137,17 +147,19 @@ describe('useAccountIndicatorChecks', () => { it('does not return wallet terms error when chatReportID exists', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.USER_WALLET]: {}, - [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, - [ONYXKEYS.LOGINS]: {}, - [ONYXKEYS.WALLET_TERMS]: { - errors: {error: 'Something went wrong'}, - chatReportID: '123', - }, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.USER_WALLET]: {}, + [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, + [ONYXKEYS.LOGINS]: {}, + [ONYXKEYS.WALLET_TERMS]: { + errors: {error: 'Something went wrong'}, + chatReportID: '123', + }, + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -159,17 +171,19 @@ describe('useAccountIndicatorChecks', () => { it('returns HAS_PHONE_NUMBER_ERROR when phone number has errors', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.USER_WALLET]: {}, - [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, - [ONYXKEYS.LOGINS]: {}, - [ONYXKEYS.WALLET_TERMS]: {}, - [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: { - errorFields: {phoneNumber: 'Invalid phone number'}, - }, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.USER_WALLET]: {}, + [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, + [ONYXKEYS.LOGINS]: {}, + [ONYXKEYS.WALLET_TERMS]: {}, + [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: { + errorFields: {phoneNumber: {error: 'Invalid phone number'}}, + }, + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -181,16 +195,18 @@ describe('useAccountIndicatorChecks', () => { it('returns HAS_SUBSCRIPTION_ERRORS when subscription has billing dispute', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.USER_WALLET]: {}, - [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, - [ONYXKEYS.LOGINS]: {}, - [ONYXKEYS.WALLET_TERMS]: {}, - [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, - [ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]: 1, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.USER_WALLET]: {}, + [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, + [ONYXKEYS.LOGINS]: {}, + [ONYXKEYS.WALLET_TERMS]: {}, + [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, + [ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]: 1, + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -202,23 +218,25 @@ describe('useAccountIndicatorChecks', () => { it('returns HAS_EMPLOYEE_CARD_FEED_ERRORS for non-admin with broken card connection', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.USER_WALLET]: {}, - [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, - [ONYXKEYS.LOGINS]: {}, - [ONYXKEYS.WALLET_TERMS]: {}, - [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, - [ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]: 0, - [ONYXKEYS.CARD_LIST]: { - card1: { - bank: cardFeed.feedName, - fundID: String(cardFeed.policyAccountID), - lastScrapeResult: 403, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.USER_WALLET]: {}, + [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, + [ONYXKEYS.LOGINS]: {}, + [ONYXKEYS.WALLET_TERMS]: {}, + [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, + [ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]: 0, + [ONYXKEYS.CARD_LIST]: { + card1: { + bank: cardFeed.feedName, + fundID: String(cardFeed.policyAccountID), + lastScrapeResult: 403, + }, }, - }, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -237,24 +255,24 @@ describe('useAccountIndicatorChecks', () => { it('returns HAS_LOGIN_LIST_INFO when login list has unvalidated contact', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.LOGINS]: { - [`1_${userID}`]: { - partnerID: 1, - partnerName: 'John Doe', - partnerUserID: userID, - validatedDate: new Date().toISOString(), - }, - // eslint-disable-next-line @typescript-eslint/naming-convention - '1_otheruser@expensify.com': { - partnerID: 1, - partnerName: 'Other User', - partnerUserID: 'different@expensify.com', - validatedDate: undefined, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.LOGINS]: { + [`1_${userID}`]: { + partnerID: 1, + partnerUserID: userID, + validatedDate: new Date().toISOString(), + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + '1_otheruser@expensify.com': { + partnerID: 1, + partnerUserID: 'different@expensify.com', + validatedDate: undefined, + }, }, - }, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -266,19 +284,21 @@ describe('useAccountIndicatorChecks', () => { it('returns HAS_PENDING_CARD_INFO when card has pending action', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.LOGINS]: {}, - [ONYXKEYS.CARD_LIST]: { - card1: { - cardID: 'card1', - bank: CONST.EXPENSIFY_CARD.BANK, - nameValuePairs: {isVirtual: false}, - state: CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.LOGINS]: {}, + [ONYXKEYS.CARD_LIST]: { + card1: { + cardID: 1, + bank: CONST.EXPENSIFY_CARD.BANK, + nameValuePairs: {isVirtual: false}, + state: CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED, + }, }, - }, - [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -290,13 +310,15 @@ describe('useAccountIndicatorChecks', () => { it('returns HAS_SUBSCRIPTION_INFO when retry billing was successful', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.LOGINS]: {}, - [ONYXKEYS.CARD_LIST]: {}, - [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, - [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: true, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.LOGINS]: {}, + [ONYXKEYS.CARD_LIST]: {}, + [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: true, + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -308,20 +330,22 @@ describe('useAccountIndicatorChecks', () => { it('returns HAS_PARTIALLY_SETUP_BANK_ACCOUNT_INFO when bank account is partially setup', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.LOGINS]: {}, - [ONYXKEYS.CARD_LIST]: {}, - [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, - [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: false, - [ONYXKEYS.BANK_ACCOUNT_LIST]: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 12345: { - methodID: 12345, - accountData: {state: CONST.BANK_ACCOUNT.STATE.SETUP}, + await Onyx.multiSet( + createMock({ + [ONYXKEYS.LOGINS]: {}, + [ONYXKEYS.CARD_LIST]: {}, + [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: false, + [ONYXKEYS.BANK_ACCOUNT_LIST]: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 12345: { + methodID: 12345, + accountData: {state: CONST.BANK_ACCOUNT.STATE.SETUP}, + }, }, - }, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); @@ -335,16 +359,18 @@ describe('useAccountIndicatorChecks', () => { describe('no errors or info', () => { beforeAll(async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, - [ONYXKEYS.USER_WALLET]: {}, - [ONYXKEYS.WALLET_TERMS]: {}, - [ONYXKEYS.LOGINS]: {}, - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, - [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, - [ONYXKEYS.CARD_LIST]: {}, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, + [ONYXKEYS.USER_WALLET]: {}, + [ONYXKEYS.WALLET_TERMS]: {}, + [ONYXKEYS.LOGINS]: {}, + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, + [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, + [ONYXKEYS.CARD_LIST]: {}, + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); }); }); @@ -361,16 +387,18 @@ describe('useAccountIndicatorChecks', () => { describe('missing data', () => { beforeAll(async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.BANK_ACCOUNT_LIST]: null, - [ONYXKEYS.USER_WALLET]: null, - [ONYXKEYS.WALLET_TERMS]: null, - [ONYXKEYS.LOGINS]: null, - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: null, - [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: null, - [ONYXKEYS.CARD_LIST]: null, - [ONYXKEYS.SESSION]: null, - } as unknown as OnyxMultiSetInput); + await Onyx.multiSet( + createMock({ + [ONYXKEYS.BANK_ACCOUNT_LIST]: null, + [ONYXKEYS.USER_WALLET]: null, + [ONYXKEYS.WALLET_TERMS]: null, + [ONYXKEYS.LOGINS]: null, + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: null, + [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: null, + [ONYXKEYS.CARD_LIST]: null, + [ONYXKEYS.SESSION]: null, + }), + ); await waitForBatchedUpdatesWithAct(); }); }); @@ -387,33 +415,33 @@ describe('useAccountIndicatorChecks', () => { describe('error takes priority over info', () => { it('returns both accountStatus and infoStatus independently', async () => { await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.USER_WALLET]: { - bankAccountID: 12345, - errors: {error: 'Wallet error'}, - }, - [ONYXKEYS.LOGINS]: { - [`1_${userID}`]: { - partnerID: 1, - partnerName: 'John Doe', - partnerUserID: userID, - validatedDate: new Date().toISOString(), + await Onyx.multiSet( + createMock({ + [ONYXKEYS.USER_WALLET]: { + bankAccountID: 12345, + errors: {error: 'Wallet error'}, }, - // eslint-disable-next-line @typescript-eslint/naming-convention - '1_otheruser@expensify.com': { - partnerID: 1, - partnerName: 'Other User', - partnerUserID: 'different@expensify.com', - validatedDate: undefined, + [ONYXKEYS.LOGINS]: { + [`1_${userID}`]: { + partnerID: 1, + partnerUserID: userID, + validatedDate: new Date().toISOString(), + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + '1_otheruser@expensify.com': { + partnerID: 1, + partnerUserID: 'different@expensify.com', + validatedDate: undefined, + }, }, - }, - [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, - [ONYXKEYS.WALLET_TERMS]: {}, - [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, - [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, - [ONYXKEYS.CARD_LIST]: {}, - [ONYXKEYS.SESSION]: {email: userID}, - } as unknown as OnyxMultiSetInput); + [ONYXKEYS.BANK_ACCOUNT_LIST]: {}, + [ONYXKEYS.WALLET_TERMS]: {}, + [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: {}, + [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: {}, + [ONYXKEYS.CARD_LIST]: {}, + [ONYXKEYS.SESSION]: {email: userID}, + }), + ); await waitForBatchedUpdatesWithAct(); });