diff --git a/tests/unit/GithubUtilsTest.ts b/tests/unit/GithubUtilsTest.ts index 5aee158acef4..48e88c732dff 100644 --- a/tests/unit/GithubUtilsTest.ts +++ b/tests/unit/GithubUtilsTest.ts @@ -2,8 +2,6 @@ import CONST from '@github/libs/CONST'; import type {InternalOctokit} from '@github/libs/GithubUtils'; import GithubUtils from '@github/libs/GithubUtils'; -import type {Writable} from 'type-fest'; - /** * @jest-environment node */ @@ -11,45 +9,21 @@ import type {Writable} from 'type-fest'; import * as core from '@actions/core'; import {RequestError} from '@octokit/request-error'; -const mockGetInput = jest.fn(); -const mockListIssues = jest.fn(); - -type ObjectMethodData = { - data: T; -}; +import createMock from '../utils/createMock'; -type OctokitCreateIssue = InternalOctokit['rest']['issues']['create']; +type OctokitCompareCommits = InternalOctokit['rest']['repos']['compareCommits']; +type OctokitCompareCommitsResponse = Awaited>; -const asMutable = (value: T): Writable => value as Writable; +let internalOctokit: InternalOctokit; beforeAll(() => { - // Mock core module - asMutable(core).getInput = mockGetInput; - - // Mock octokit module - const mockOctokit = { - rest: { - issues: { - create: jest.fn().mockImplementation((arg: Parameters[0]) => - Promise.resolve({ - data: { - ...arg, - html_url: `https://github.com/${process.env.GITHUB_REPOSITORY}/issues/29`, - }, - }), - ), - listForRepo: mockListIssues, - }, - }, - paginate: jest.fn().mockImplementation((objectMethod: () => Promise>) => objectMethod().then(({data}) => data)), - } as unknown as InternalOctokit; - - GithubUtils.internalOctokit = mockOctokit; -}); + GithubUtils.initOctokitWithToken('fake_token'); + const initializedOctokit = GithubUtils.internalOctokit; + if (!initializedOctokit) { + throw new Error('Expected GithubUtils to initialize an Octokit client.'); + } -afterEach(() => { - mockGetInput.mockClear(); - mockListIssues.mockClear(); + internalOctokit = initializedOctokit; }); describe('GithubUtils', () => { @@ -200,7 +174,7 @@ describe('GithubUtils', () => { }; describe('getCommitHistoryBetweenTags', () => { - let mockCompareCommits: jest.Mock; + let mockCompareCommits: jest.SpiedFunction; beforeEach(() => { jest.spyOn(core, 'getInput').mockImplementation((name) => { @@ -211,19 +185,11 @@ describe('GithubUtils', () => { }); // Prepare the mocked GitHub API - mockCompareCommits = jest.fn(); - const mockOctokitInstance = { - rest: { - repos: { - compareCommits: mockCompareCommits, - }, - }, - paginate: jest.fn(), - } as unknown as InternalOctokit; + mockCompareCommits = jest.spyOn(internalOctokit.rest.repos, 'compareCommits'); // Replace the real initOctokit with our mocked one jest.spyOn(GithubUtils, 'initOctokit').mockImplementation(() => {}); - GithubUtils.internalOctokit = mockOctokitInstance; + GithubUtils.internalOctokit = internalOctokit; }); afterEach(() => { @@ -231,7 +197,7 @@ describe('GithubUtils', () => { }); test('should call GitHub API with correct parameters', async () => { - mockCompareCommits.mockResolvedValue(commitHistoryData.emptyResponse); + mockCompareCommits.mockResolvedValue(createMock(commitHistoryData.emptyResponse)); await GithubUtils.getCommitHistoryBetweenTags('v1.0.0', 'v1.0.1', CONST.APP_REPO); @@ -246,21 +212,21 @@ describe('GithubUtils', () => { }); test('should return empty array when no commits found', async () => { - mockCompareCommits.mockResolvedValue(commitHistoryData.emptyResponse); + mockCompareCommits.mockResolvedValue(createMock(commitHistoryData.emptyResponse)); const result = await GithubUtils.getCommitHistoryBetweenTags('1.0.0', '1.0.1', CONST.APP_REPO); expect(result).toEqual([]); }); test('should return formatted commit history when commits exist', async () => { - mockCompareCommits.mockResolvedValue(commitHistoryData.singleCommit); + mockCompareCommits.mockResolvedValue(createMock(commitHistoryData.singleCommit)); const result = await GithubUtils.getCommitHistoryBetweenTags('1.0.0', '1.0.1', CONST.APP_REPO); expect(result).toEqual(commitHistoryData.expectedFormattedCommit); }); test('should handle multiple commits correctly', async () => { - mockCompareCommits.mockResolvedValue(commitHistoryData.multipleCommitsResponse); + mockCompareCommits.mockResolvedValue(createMock(commitHistoryData.multipleCommitsResponse)); const result = await GithubUtils.getCommitHistoryBetweenTags('1.0.0', '1.0.1', CONST.APP_REPO); diff --git a/tests/unit/HomePage/YourSpendSection/YourSpendSectionTest.tsx b/tests/unit/HomePage/YourSpendSection/YourSpendSectionTest.tsx index 6b083db4db02..bb47cb0492e3 100644 --- a/tests/unit/HomePage/YourSpendSection/YourSpendSectionTest.tsx +++ b/tests/unit/HomePage/YourSpendSection/YourSpendSectionTest.tsx @@ -14,10 +14,11 @@ import type * as NativeNavigation from '@react-navigation/native'; import type {ReactNode} from 'react'; // eslint-disable-next-line no-restricted-imports import type {Pressable as RNPressable, Text as RNText, View as RNView} from 'react-native'; -import type {ValueOf} from 'type-fest'; import React from 'react'; +import createMock from '../../../utils/createMock'; + jest.mock('@libs/Navigation/Navigation', () => ({ navigate: jest.fn(), setNavigationActionToMicrotaskQueue: jest.fn((cb: () => void) => cb()), @@ -152,30 +153,11 @@ jest.mock('@pages/home/YourSpendSection/useYourSpendData', () => { }; }); -type MockCardRow = { - cardID: number; - query: string; - lastFour: string; - total: number | undefined; - currency: string | undefined; - spentFraction?: number | undefined; - kind?: 'expensify' | 'thirdParty'; - bank?: CardFeedWithNumber; - fundID?: string | undefined; -}; - -type MockHookData = { - approvalRowState: ValueOf; - approvalTotals: {total: number | undefined; currency: string | undefined}; - paymentRowState: ValueOf; - paymentTotals: {total: number | undefined; currency: string | undefined}; - cardRows: MockCardRow[]; - awaitingApprovalQuery: string; - repaidLast30DaysQuery: string; -}; +type MockHookData = ReturnType; +type MockCardRow = MockHookData['cardRows'][number]; function mockHook(data: Partial) { - (useYourSpendData as jest.Mock).mockReturnValue({ + jest.mocked(useYourSpendData).mockReturnValue({ approvalRowState: YOUR_SPEND_ROW_STATE.HIDDEN, approvalTotals: {total: undefined, currency: undefined}, paymentRowState: YOUR_SPEND_ROW_STATE.HIDDEN, @@ -240,8 +222,8 @@ describe('YourSpendSection', () => { it('renders a card row for each entry in cardRows', () => { mockHook({ cardRows: [ - {cardID: 1, query: 'type:expense cardID:1', lastFour: '1234', total: undefined, currency: undefined}, - {cardID: 2, query: 'type:expense cardID:2', lastFour: '5678', total: undefined, currency: undefined}, + createMock({cardID: 1, query: 'type:expense cardID:1', lastFour: '1234', total: undefined, currency: undefined}), + createMock({cardID: 2, query: 'type:expense cardID:2', lastFour: '5678', total: undefined, currency: undefined}), ], }); render(); @@ -276,6 +258,7 @@ describe('YourSpendSection — third-party rows', () => { kind: 'thirdParty', bank: THIRD_PARTY_BANK, fundID: '767578', + isPersonal: false, ...overrides, }; } @@ -291,6 +274,7 @@ describe('YourSpendSection — third-party rows', () => { kind: 'expensify', bank: 'Expensify Card' as CardFeedWithNumber, fundID: '999', + isPersonal: false, ...overrides, }; } @@ -316,7 +300,7 @@ describe('YourSpendSection — third-party rows', () => { }); it('navigates to SEARCH_ROOT with the third-party row query when tapped (R-8)', () => { - (Navigation.navigate as jest.Mock).mockClear(); + jest.mocked(Navigation.navigate).mockClear(); mockHook({cardRows: [thirdPartyRow()]}); render(); const row = screen.getByTestId(`your-spend-card-row-${THIRD_PARTY_CARD_ID}`); @@ -407,7 +391,11 @@ describe('YourSpendSection — third-party rows', () => { // Match approval-row, payment-row, and any card-row-* element under the section, // then assert their relative order in the rendered tree. const allRows = within(section).getAllByTestId(/^your-spend-(approval-row|payment-row|card-row-\d+)$/); - const collectedTestIDs = allRows.map((el) => (el.props as {testID: string}).testID); - expect(collectedTestIDs).toEqual(['your-spend-approval-row', 'your-spend-payment-row', `your-spend-card-row-${expRow.cardID}`, `your-spend-card-row-${tpRow.cardID}`]); + expect(allRows).toEqual([ + within(section).getByTestId('your-spend-approval-row'), + within(section).getByTestId('your-spend-payment-row'), + within(section).getByTestId(`your-spend-card-row-${expRow.cardID}`), + within(section).getByTestId(`your-spend-card-row-${tpRow.cardID}`), + ]); }); }); diff --git a/tests/unit/ImageSVGCachePolicyTest.tsx b/tests/unit/ImageSVGCachePolicyTest.tsx index 6d44be900d22..2b2536c46edd 100644 --- a/tests/unit/ImageSVGCachePolicyTest.tsx +++ b/tests/unit/ImageSVGCachePolicyTest.tsx @@ -1,20 +1,23 @@ import {render} from '@testing-library/react-native'; +import type {ImageProps as ExpoImageProps} from 'expo-image'; + import React from 'react'; import ImageSVGAndroid from '../../src/components/ImageSVG/index.android'; import ImageSVGiOS from '../../src/components/ImageSVG/index.ios'; -type MockImageType = jest.Mock & {clearMemoryCache: jest.Mock}; - const mockClearMemoryCache = jest.fn(() => Promise.resolve(true)); -const mockImageComponent: MockImageType = Object.assign( - jest.fn(() => null), +const mockImageComponent = Object.assign( + jest.fn((props: ExpoImageProps) => { + Object.keys(props); + return null; + }), { clearMemoryCache: mockClearMemoryCache, }, -) as MockImageType; +); jest.mock('expo-image', () => ({ get Image() { @@ -27,8 +30,8 @@ jest.mock('@libs/getImageRecyclingKey', () => if (typeof source === 'number') { return String(source); } - if (typeof source === 'object' && source !== null && 'uri' in source) { - return (source as {uri: string}).uri; + if (typeof source === 'object' && source !== null && 'uri' in source && typeof source.uri === 'string') { + return source.uri; } return undefined; }), @@ -36,9 +39,12 @@ jest.mock('@libs/getImageRecyclingKey', () => const MOCK_STATIC_SOURCE = 42; -function getFirstCallProps(): Record { - const firstCall = mockImageComponent.mock.calls.at(0) as unknown[] | undefined; - return firstCall?.at(0) as Record; +function getFirstCallProps(): ExpoImageProps { + const firstCall = mockImageComponent.mock.calls.at(0); + if (!firstCall) { + throw new Error('Expected Expo Image to be called'); + } + return firstCall[0]; } describe('ImageSVG cache policy', () => { diff --git a/tests/unit/MoneyRequestUtilsTest.ts b/tests/unit/MoneyRequestUtilsTest.ts index 520c7e71488f..5864e0e46392 100644 --- a/tests/unit/MoneyRequestUtilsTest.ts +++ b/tests/unit/MoneyRequestUtilsTest.ts @@ -16,6 +16,7 @@ import type Transaction from '@src/types/onyx/Transaction'; import type {TransactionCustomUnit, WaypointCollection} from '@src/types/onyx/Transaction'; import createRandomTransaction from '../utils/collections/transaction'; +import createMock from '../utils/createMock'; describe('ReportActionsUtils', () => { describe('validateAmount', () => { @@ -207,7 +208,8 @@ describe('ReportActionsUtils', () => { describe('invalid inputs', () => { it('should return false for nullish and NaN values', () => { expect(isValidMoneyRequestAmount(undefined, CONST.IOU.TYPE.SUBMIT)).toBe(false); - expect(isValidMoneyRequestAmount(null as unknown as number, CONST.IOU.TYPE.SUBMIT)).toBe(false); + // @ts-expect-error -- Deliberately verifies the defensive runtime behavior for null input. + expect(isValidMoneyRequestAmount(null, CONST.IOU.TYPE.SUBMIT)).toBe(false); expect(isValidMoneyRequestAmount(NaN, CONST.IOU.TYPE.SUBMIT)).toBe(false); }); }); @@ -274,15 +276,15 @@ describe('ReportActionsUtils', () => { type: CONST.REPORT.TYPE.EXPENSE, } as Report; - const unreportedTransaction = { + const unreportedTransaction = createMock({ reportID: CONST.REPORT.UNREPORTED_REPORT_ID, amount: 0, - } as Transaction; + }); - const reportedTransaction = { + const reportedTransaction = createMock({ reportID: '123', amount: 0, - } as Transaction; + }); describe('empty merchants', () => { it('should return true for empty/undefined merchant when transaction is unreported or IOU', () => { diff --git a/tests/unit/getChartSkiaTypefaceTest.ts b/tests/unit/getChartSkiaTypefaceTest.ts index c3c4a90ccc1a..484a79d6ec8a 100644 --- a/tests/unit/getChartSkiaTypefaceTest.ts +++ b/tests/unit/getChartSkiaTypefaceTest.ts @@ -1,13 +1,17 @@ -import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; +import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes'; import {CHART_SKIA_TYPEFACE_ASSETS} from '@components/Charts/utils/chartFontAssets'; import getChartSkiaTypeface from '@components/Charts/utils/getChartSkiaTypeface'; +import ObjectUtils from '@src/types/utils/ObjectUtils'; + import type {SkTypeface} from '@shopify/react-native-skia'; -const CHART_SKIA_TYPEFACE_KEYS = Object.keys(CHART_SKIA_TYPEFACE_ASSETS) as ChartSkiaTypefaceKey[]; +import createMock from '../utils/createMock'; + +const CHART_SKIA_TYPEFACE_KEYS = ObjectUtils.typedKeys(CHART_SKIA_TYPEFACE_ASSETS); function makeTypefaces(): ChartDefaultTypeface { - return Object.fromEntries(CHART_SKIA_TYPEFACE_KEYS.map((key) => [key, {id: key} as unknown as SkTypeface])) as ChartDefaultTypeface; + return ObjectUtils.typedFromEntries(CHART_SKIA_TYPEFACE_KEYS.map((key) => [key, createMock({})] as const)); } describe('getChartSkiaTypeface', () => { diff --git a/tests/unit/hooks/useAutocompleteSuggestions.test.ts b/tests/unit/hooks/useAutocompleteSuggestions.test.ts index 2e019ccc2b95..82e4f1c8cadb 100644 --- a/tests/unit/hooks/useAutocompleteSuggestions.test.ts +++ b/tests/unit/hooks/useAutocompleteSuggestions.test.ts @@ -4,6 +4,7 @@ import useAutocompleteSuggestions from '@hooks/useAutocompleteSuggestions'; import useNetwork from '@hooks/useNetwork'; import {openSearchCategoryFiltersPage} from '@libs/actions/Search'; +import {getSearchOptions} from '@libs/OptionsListUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -11,6 +12,8 @@ import type {Policy} from '@src/types/onyx'; import type {OnyxCollection} from 'react-native-onyx'; +import createMock from '../../utils/createMock'; + const onyxData: Record = {}; jest.mock('@hooks/useOnyx', () => ({ @@ -103,28 +106,29 @@ jest.mock('@hooks/useExportedToFilterOptions', () => ({ })); const {parseForAutocomplete} = jest.requireMock<{parseForAutocomplete: jest.Mock}>('@libs/SearchAutocompleteUtils'); -const {getSearchOptions} = jest.requireMock<{getSearchOptions: jest.Mock}>('@libs/OptionsListUtils'); const {getExpensifyTeamExclusions} = jest.requireMock<{getExpensifyTeamExclusions: jest.Mock}>('@libs/PolicyUtils'); const mockedUseNetwork = jest.mocked(useNetwork); const mockedOpenSearchCategoryFiltersPage = jest.mocked(openSearchCategoryFiltersPage); +const mockedGetSearchOptions = jest.mocked(getSearchOptions); + +type Params = Parameters[0]; -const defaultParams = { +const defaultParams: Params = { autocompleteQueryValue: '', allCards: {}, allFeeds: {}, options: {reports: [], personalDetails: []}, draftComments: {}, - betas: [] as never[], + betas: [], countryCode: 1, loginList: {}, policies: {}, visibleReportActionsData: undefined, - sortedActions: undefined, currentUserAccountID: 100, currentUserEmail: 'me@example.com', personalDetails: {}, feedKeysWithCards: undefined, - translate: jest.fn((key: string) => key) as never, + translate: (key, ...parameters) => String([key, ...parameters].at(0)), }; describe('useAutocompleteSuggestions', () => { @@ -459,11 +463,11 @@ describe('useAutocompleteSuggestions', () => { ], }); - const policiesWithSameName = { - policyOne: {id: 'policyA', name: 'Test Workspace'}, - policyTwo: {id: 'policyB', name: 'Test Workspace'}, - policyThree: {id: 'policyC', name: 'Test Workspace'}, - } as unknown as NonNullable>; + const policiesWithSameName: NonNullable> = { + policyOne: createMock({id: 'policyA', name: 'Test Workspace'}), + policyTwo: createMock({id: 'policyB', name: 'Test Workspace'}), + policyThree: createMock({id: 'policyC', name: 'Test Workspace'}), + }; const {result} = renderHook(() => useAutocompleteSuggestions({ @@ -491,8 +495,7 @@ describe('useAutocompleteSuggestions', () => { }; const lastSearchOptionsCallExclusions = (): Record | undefined => { - const calls = getSearchOptions.mock.calls as Array<[{excludeFromSuggestionsOnly?: Record}]>; - return calls.at(-1)?.[0]?.excludeFromSuggestionsOnly; + return mockedGetSearchOptions.mock.calls.at(-1)?.[0]?.excludeFromSuggestionsOnly; }; it('passes Expensify-team exclusions to getSearchOptions for from: autocomplete', () => { diff --git a/tests/unit/hooks/useConfirmationAmount.test.tsx b/tests/unit/hooks/useConfirmationAmount.test.tsx index 197346813d86..7d27aaccbd13 100644 --- a/tests/unit/hooks/useConfirmationAmount.test.tsx +++ b/tests/unit/hooks/useConfirmationAmount.test.tsx @@ -3,6 +3,8 @@ import {renderHook} from '@testing-library/react-native'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import useConfirmationAmount from '@components/MoneyRequestConfirmationList/hooks/useConfirmationAmount'; +import type * as PerDiem from '@libs/actions/IOU/PerDiem'; + import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -10,6 +12,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import React from 'react'; import Onyx from 'react-native-onyx'; +import createMock from '../../utils/createMock'; import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithAct'; jest.mock('@hooks/useCurrencyList', () => ({ @@ -21,13 +24,13 @@ jest.mock('@hooks/useCurrencyList', () => ({ })); jest.mock('@libs/actions/IOU/PerDiem', () => ({ - computePerDiemExpenseAmount: ({subRates}: {subRates: Array<{amount: number}>}) => subRates.reduce((sum, r) => sum + (r.amount ?? 0), 0), + computePerDiemExpenseAmount: ({subRates}: Parameters[0]) => (subRates ?? []).reduce((sum, {quantity, rate}) => sum + quantity * rate, 0), })); type Params = Parameters[0]; const baseParams: Params = { - transaction: {transactionID: 'txn1', amount: 100, comment: {}} as unknown as OnyxTypes.Transaction, + transaction: createMock({transactionID: 'txn1', amount: 100, comment: {}}), iouAmount: 100, iouCurrencyCode: 'USD', iouAttendees: [], @@ -65,7 +68,10 @@ describe('useConfirmationAmount', () => { }); it('per-diem overrides iouAmount when sub-rates change', () => { - const subRates = [{amount: 30}, {amount: 70}]; + const subRates = [ + createMock({id: 'subRate1', quantity: 1, name: 'Breakfast', rate: 30}), + createMock({id: 'subRate2', quantity: 1, name: 'Dinner', rate: 70}), + ]; const {result} = renderHook( () => useConfirmationAmount({ @@ -73,7 +79,7 @@ describe('useConfirmationAmount', () => { iouAmount: 0, isPerDiemRequest: true, prevSubRates: [], - transaction: {transactionID: 'txn1', amount: 0, comment: {customUnit: {subRates}}} as unknown as OnyxTypes.Transaction, + transaction: createMock({transactionID: 'txn1', amount: 0, comment: {customUnit: {subRates}}}), }), {wrapper: Wrapper}, ); @@ -87,7 +93,8 @@ describe('useConfirmationAmount', () => { }); it('divides amount by attendee count for per-attendee total', () => { - const {result} = renderHook(() => useConfirmationAmount({...baseParams, iouAttendees: [{}, {}, {}, {}] as Params['iouAttendees']}), { + const iouAttendees = Array.from({length: 4}, () => createMock({})); + const {result} = renderHook(() => useConfirmationAmount({...baseParams, iouAttendees}), { wrapper: Wrapper, }); // 100 / 4 = 25 diff --git a/tests/unit/hooks/useConfirmationSections.test.tsx b/tests/unit/hooks/useConfirmationSections.test.tsx index 835e36a005e5..e318257d730d 100644 --- a/tests/unit/hooks/useConfirmationSections.test.tsx +++ b/tests/unit/hooks/useConfirmationSections.test.tsx @@ -13,13 +13,14 @@ import React from 'react'; import {View} from 'react-native'; import Onyx from 'react-native-onyx'; +import createMock from '../../utils/createMock'; import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithAct'; type Params = Parameters[0]; const payee = {accountID: 1, login: 'me@test.com'} as CurrentUserPersonalDetails; const smsPayee = {accountID: 3, login: '+18332403627@expensify.sms'} as CurrentUserPersonalDetails; -const otherParticipant = {accountID: 2, login: 'other@test.com', keyForList: '2'} as unknown as Participant; +const otherParticipant = createMock({accountID: 2, login: 'other@test.com', keyForList: '2'}); const splitParticipant = {accountID: 2, keyForList: '2', login: 'other@test.com'} as Participant & {keyForList: string}; function makeBase(overrides: Partial = {}): Params { @@ -79,11 +80,19 @@ describe('useConfirmationSections', () => { it('flags participants as interactive only when canEditParticipant is true', () => { const {result: editable} = renderHook(() => useConfirmationSections(makeBase({canEditParticipant: true})), {wrapper: Wrapper}); const {result: readonly} = renderHook(() => useConfirmationSections(makeBase({canEditParticipant: false})), {wrapper: Wrapper}); - const editableRow = editable.current.at(0)?.data.at(0) as {isInteractive?: boolean; shouldShowRightCaret?: boolean} | undefined; - const readonlyRow = readonly.current.at(0)?.data.at(0) as {isInteractive?: boolean; shouldShowRightCaret?: boolean} | undefined; - expect(editableRow?.isInteractive).toBe(true); - expect(editableRow?.shouldShowRightCaret).toBe(true); - expect(readonlyRow?.isInteractive).toBe(false); - expect(readonlyRow?.shouldShowRightCaret).toBe(false); + const editableRow = editable.current.at(0)?.data.find((item) => item.keyForList === otherParticipant.keyForList); + const readonlyRow = readonly.current.at(0)?.data.find((item) => item.keyForList === otherParticipant.keyForList); + + if (!editableRow || !('isInteractive' in editableRow) || !('shouldShowRightCaret' in editableRow)) { + throw new Error('Expected the editable participant row to expose its interaction state'); + } + if (!readonlyRow || !('isInteractive' in readonlyRow) || !('shouldShowRightCaret' in readonlyRow)) { + throw new Error('Expected the read-only participant row to expose its interaction state'); + } + + expect(editableRow.isInteractive).toBe(true); + expect(editableRow.shouldShowRightCaret).toBe(true); + expect(readonlyRow.isInteractive).toBe(false); + expect(readonlyRow.shouldShowRightCaret).toBe(false); }); }); diff --git a/tests/unit/hooks/useFilterPendingDeleteReports.test.ts b/tests/unit/hooks/useFilterPendingDeleteReports.test.ts index a652914cbac4..d2942a121c40 100644 --- a/tests/unit/hooks/useFilterPendingDeleteReports.test.ts +++ b/tests/unit/hooks/useFilterPendingDeleteReports.test.ts @@ -12,7 +12,7 @@ const onyxData: Record = {}; const mockUseOnyx = jest.fn((key: string, options?: {selector?: (value: unknown) => unknown}) => { const value = onyxData[key]; - const selectedValue = options?.selector ? options.selector(value as never) : value; + const selectedValue = options?.selector ? options.selector(value) : value; return [selectedValue]; }); @@ -31,8 +31,13 @@ describe('useFilterPendingDeleteReports', () => { describe('selectPendingDeleteReportKeys', () => { it('returns empty array for null/undefined collection', () => { - expect(selectPendingDeleteReportKeys(null as unknown as OnyxCollection)).toEqual([]); - expect(selectPendingDeleteReportKeys(undefined as unknown as OnyxCollection)).toEqual([]); + expect( + selectPendingDeleteReportKeys( + // @ts-expect-error -- Deliberately verifies the selector's defensive runtime behavior for a null collection. + null, + ), + ).toEqual([]); + expect(selectPendingDeleteReportKeys(undefined)).toEqual([]); }); it('returns empty array when no reports are pending delete', () => { diff --git a/tests/unit/hooks/useFormErrorManagement.test.tsx b/tests/unit/hooks/useFormErrorManagement.test.tsx index 2159d066cdce..b8e89581461f 100644 --- a/tests/unit/hooks/useFormErrorManagement.test.tsx +++ b/tests/unit/hooks/useFormErrorManagement.test.tsx @@ -13,6 +13,7 @@ import type * as ReactNavigationModule from '@react-navigation/native'; import React from 'react'; import Onyx from 'react-native-onyx'; +import createMock from '../../utils/createMock'; import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithAct'; jest.mock('@react-navigation/native', () => { @@ -26,7 +27,7 @@ jest.mock('@react-navigation/native', () => { type Params = Parameters[0]; const baseParams: Params = { - transaction: {transactionID: 'txn1', amount: 100, merchant: 'Coffee', comment: {}} as unknown as OnyxTypes.Transaction, + transaction: createMock({transactionID: 'txn1', amount: 100, merchant: 'Coffee', comment: {}}), transactionReport: undefined, iouMerchant: 'Coffee', iouCategory: '', @@ -71,8 +72,8 @@ describe('useFormErrorManagement', () => { ...baseParams, isEditingSplitBill: true, hasSmartScanFailed: true, - transaction: {transactionID: 'txn1', amount: 0, merchant: '', comment: {}, receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}} as unknown as OnyxTypes.Transaction, - transactionReport: {type: CONST.REPORT.TYPE.IOU} as unknown as OnyxTypes.Report, + transaction: createMock({transactionID: 'txn1', amount: 0, merchant: '', comment: {}, receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}}), + transactionReport: createMock({type: CONST.REPORT.TYPE.IOU}), }), {wrapper: Wrapper}, ); diff --git a/tests/unit/hooks/useNonPersonalCardList.test.ts b/tests/unit/hooks/useNonPersonalCardList.test.ts index 85b5d5f17a19..867c323d4178 100644 --- a/tests/unit/hooks/useNonPersonalCardList.test.ts +++ b/tests/unit/hooks/useNonPersonalCardList.test.ts @@ -10,6 +10,7 @@ import type {Card, CardList} from '@src/types/onyx'; import Onyx from 'react-native-onyx'; import {createRandomExpensifyCard} from '../../utils/collections/card'; +import createMock from '../../utils/createMock'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; describe('useNonPersonalCardList', () => { @@ -33,7 +34,7 @@ describe('useNonPersonalCardList', () => { }); it('should return empty object when only personal cards exist', async () => { - const personalCard: Card = {cardID: 1, bank: CONST.PERSONAL_CARDS.BANK_NAME.CSV, lastUpdated: ''} as Card; + const personalCard = createMock({cardID: 1, bank: CONST.PERSONAL_CARDS.BANK_NAME.CSV, lastUpdated: ''}); const cardList: CardList = {'1': personalCard}; await Onyx.merge(ONYXKEYS.CARD_LIST, cardList); @@ -46,7 +47,7 @@ describe('useNonPersonalCardList', () => { it('should return Expensify cards and filter out personal cards', async () => { const expensifyCard = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN}); - const personalCard: Card = {cardID: 2, bank: CONST.PERSONAL_CARDS.BANK_NAME.CSV, lastUpdated: ''} as Card; + const personalCard = createMock({cardID: 2, bank: CONST.PERSONAL_CARDS.BANK_NAME.CSV, lastUpdated: ''}); const cardList: CardList = {'1': expensifyCard, '2': personalCard}; await Onyx.merge(ONYXKEYS.CARD_LIST, cardList); @@ -72,7 +73,7 @@ describe('useNonPersonalCardList', () => { domainName: '', lastFourPAN: '1111', } as Card; - const personalCard: Card = {cardID: 2, bank: CONST.PERSONAL_CARDS.BANK_NAME.CSV, lastUpdated: ''} as Card; + const personalCard = createMock({cardID: 2, bank: CONST.PERSONAL_CARDS.BANK_NAME.CSV, lastUpdated: ''}); const cardList: CardList = {'1': companyCard, '2': personalCard}; await Onyx.merge(ONYXKEYS.CARD_LIST, cardList); diff --git a/tests/unit/hooks/useSplitContextHooks.test.tsx b/tests/unit/hooks/useSplitContextHooks.test.tsx index 23258c17b26d..0e8184f89ac1 100644 --- a/tests/unit/hooks/useSplitContextHooks.test.tsx +++ b/tests/unit/hooks/useSplitContextHooks.test.tsx @@ -20,26 +20,10 @@ import {DEFAULT_STATE, MultifactorAuthenticationStateProvider} from '@components import {useMultifactorAuthenticationState} from '@components/MultifactorAuthentication/Context/MultifactorAuthenticationStateContext'; import type {PropsWithChildren} from 'react'; -import type {SharedValue} from 'react-native-reanimated'; import React from 'react'; -/** - * Creates a mock SharedValue that satisfies the SharedValue interface used in reanimated. - */ -function createMockSharedValue(initialValue: T): SharedValue { - let current = initialValue; - return { - value: initialValue, - get: () => current, - set: (newValue: T | ((val: T) => T)) => { - current = typeof newValue === 'function' ? (newValue as (val: T) => T)(current) : newValue; - }, - addListener: () => -1, - removeListener: () => {}, - modify: () => {}, - } as unknown as SharedValue; -} +import createSharedValueMock from '../../utils/createSharedValueMock'; describe('Split context hooks', () => { describe('AttachmentCarouselPager context hooks', () => { @@ -55,8 +39,8 @@ describe('Split context hooks', () => { const mockState: AttachmentCarouselPagerStateContextType = { pagerItems: [], activePage: 2, - isPagerScrolling: createMockSharedValue(false), - isScrollEnabled: createMockSharedValue(true), + isPagerScrolling: createSharedValueMock(false), + isScrollEnabled: createSharedValueMock(true), }; function wrapper({children}: PropsWithChildren) { @@ -95,8 +79,8 @@ describe('Split context hooks', () => { const mockState: AttachmentCarouselPagerStateContextType = { pagerItems: [], activePage: 0, - isPagerScrolling: createMockSharedValue(false), - isScrollEnabled: createMockSharedValue(true), + isPagerScrolling: createSharedValueMock(false), + isScrollEnabled: createSharedValueMock(true), }; function stateOnlyWrapper({children}: PropsWithChildren) { @@ -208,7 +192,7 @@ describe('Split context hooks', () => { describe('MultifactorAuthentication context hooks', () => { it('throws when used outside provider', () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); expect(() => { renderHook(() => useMultifactorAuthenticationState()); @@ -218,7 +202,7 @@ describe('Split context hooks', () => { renderHook(() => useMultifactorAuthenticationActions()); }).toThrow('useMultifactorAuthenticationActions must be used within a MultifactorAuthenticationStateProvider'); - (console.error as jest.Mock).mockRestore(); + consoleErrorSpy.mockRestore(); }); it('returns default state when wrapped in provider', () => { diff --git a/tests/unit/libs/TaskutilsTest.ts b/tests/unit/libs/TaskutilsTest.ts index 289b2ed606e8..a9db7403c295 100644 --- a/tests/unit/libs/TaskutilsTest.ts +++ b/tests/unit/libs/TaskutilsTest.ts @@ -3,6 +3,7 @@ import type ReportAction from '../../../src/types/onyx/ReportAction'; import CONST from '../../../src/CONST'; import {getTaskTitle, isTaskCompleted} from '../../../src/libs/TaskUtils'; import {createRegularTaskReport} from '../../utils/collections/reports'; +import createMock from '../../utils/createMock'; jest.mock('../../../src/libs/Localize'); @@ -34,28 +35,28 @@ describe('TaskUtils', () => { describe('isTaskCompleted', () => { it('should return true when both childStateNum and childStatusNum indicate completion', () => { - const reportAction = { + const reportAction = createMock({ childStateNum: CONST.REPORT.STATE_NUM.APPROVED, childStatusNum: CONST.REPORT.STATUS_NUM.APPROVED, - } as ReportAction; + }); expect(isTaskCompleted(reportAction)).toBe(true); }); it('should return false when childStateNum is not APPROVED', () => { - const reportAction = { + const reportAction = createMock({ childStateNum: CONST.REPORT.STATE_NUM.OPEN, childStatusNum: CONST.REPORT.STATUS_NUM.APPROVED, - } as ReportAction; + }); expect(isTaskCompleted(reportAction)).toBe(false); }); it('should return false when childStatusNum is not APPROVED', () => { - const reportAction = { + const reportAction = createMock({ childStateNum: CONST.REPORT.STATE_NUM.APPROVED, childStatusNum: CONST.REPORT.STATUS_NUM.OPEN, - } as ReportAction; + }); expect(isTaskCompleted(reportAction)).toBe(false); }); diff --git a/tests/unit/libs/TravelUtilsTest.ts b/tests/unit/libs/TravelUtilsTest.ts index 1b548e756f16..8d460d898b16 100644 --- a/tests/unit/libs/TravelUtilsTest.ts +++ b/tests/unit/libs/TravelUtilsTest.ts @@ -4,7 +4,8 @@ describe('TravelUtils', () => { describe('isTravelLink', () => { it('should return false for empty or undefined values', () => { expect(isTravelLink('')).toBe(false); - expect(isTravelLink(undefined as unknown as string)).toBe(false); + // @ts-expect-error -- Deliberately verifies the defensive runtime behavior for undefined input. + expect(isTravelLink(undefined)).toBe(false); }); it('should return true for direct travel domain links', () => { @@ -78,8 +79,10 @@ describe('TravelUtils', () => { }); it('should return an empty string for undefined or null values', () => { - expect(getRelativeUrl(undefined as unknown as string)).toBe(''); - expect(getRelativeUrl(null as unknown as string)).toBe(''); + // @ts-expect-error -- Deliberately verifies the defensive runtime behavior for undefined input. + expect(getRelativeUrl(undefined)).toBe(''); + // @ts-expect-error -- Deliberately verifies the defensive runtime behavior for null input. + expect(getRelativeUrl(null)).toBe(''); }); it('should return an empty string for invalid URLs', () => { diff --git a/tests/unit/pages/settings/EditAgentPageTest.tsx b/tests/unit/pages/settings/EditAgentPageTest.tsx index 1a88a1fc0844..22fd6dafe484 100644 --- a/tests/unit/pages/settings/EditAgentPageTest.tsx +++ b/tests/unit/pages/settings/EditAgentPageTest.tsx @@ -1,6 +1,7 @@ import {render} from '@testing-library/react-native'; import useOnyx from '@hooks/useOnyx'; +import type useStyleUtils from '@hooks/useStyleUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -12,6 +13,10 @@ import type SCREENS from '@src/SCREENS'; import React from 'react'; +import createMock from '../../../utils/createMock'; + +type ParsableStyle = Parameters['parseStyleFromFunction']>[0]; + jest.mock('@userActions/Agent', () => ({ deleteAgent: jest.fn(), clearAgentUpdateError: jest.fn(), @@ -43,8 +48,8 @@ jest.mock('@hooks/useStyleUtils', () => { get: (_, prop) => { if (prop === 'parseStyleFromFunction') { - return (style: unknown) => - typeof style === 'function' ? (style as (mockState: Record) => unknown)({pressed: false, focused: false, hovered: false}) : style; + return (style: ParsableStyle) => + typeof style === 'function' ? style({pressed: false, focused: false, hovered: false, isScreenReaderActive: false, isDisabled: false}) : style; } return jest.fn(() => ({})); }, @@ -150,8 +155,8 @@ const TEST_ACCOUNT_ID = 12345; type EditAgentPageRoute = PlatformStackScreenProps['route']; type EditAgentPageNavigation = PlatformStackScreenProps['navigation']; -const mockRoute = {params: {accountID: TEST_ACCOUNT_ID}} as EditAgentPageRoute; -const mockNavigation = {} as EditAgentPageNavigation; +const mockRoute = createMock({params: {accountID: TEST_ACCOUNT_ID}}); +const mockNavigation = createMock({}); describe('EditAgentPage', () => { beforeEach(() => {