From 15e917b734eea8d43a860730b6d02d0257c426ac Mon Sep 17 00:00:00 2001 From: KJ21-ENG <140263938+KJ21-ENG@users.noreply.github.com> Date: Sat, 1 Aug 2026 16:39:55 +0530 Subject: [PATCH] Remove unsafe type assertions from tests --- tests/ui/CountrySelectionListTest.tsx | 19 ++- tests/ui/CountrySelectorModalTest.tsx | 8 +- tests/ui/DynamicContactMethodsPageTest.tsx | 13 +- tests/ui/DynamicCountrySelectionPageTest.tsx | 8 +- tests/ui/ForYouSectionTest.tsx | 14 +- tests/ui/LocationPermissionModalTest.tsx | 12 +- tests/ui/ScanSkipConfirmationTest.tsx | 29 ++-- tests/ui/SearchReportAvatarTest.tsx | 33 +++- tests/ui/WorkEmailOnboarding.tsx | 31 ++-- .../unit/ContextMenuActionsCopyMessageTest.ts | 147 ++++++++++-------- tests/unit/ExportDownloadStatusModalTest.tsx | 6 +- tests/unit/FocusTrapForModalTest.tsx | 15 +- tests/unit/awaitStagingDeploysTest.ts | 38 +++-- ...dismissModalAndOpenReportInInboxTabTest.ts | 24 +-- tests/unit/findUnusedStylesTest.ts | 46 +++--- 15 files changed, 260 insertions(+), 183 deletions(-) diff --git a/tests/ui/CountrySelectionListTest.tsx b/tests/ui/CountrySelectionListTest.tsx index 215eb4871114..9c362eb2c9c1 100644 --- a/tests/ui/CountrySelectionListTest.tsx +++ b/tests/ui/CountrySelectionListTest.tsx @@ -45,7 +45,7 @@ jest.mock('@hooks/useLocalize', () => translate: (key: string) => { if (key.startsWith('allCountries.')) { const countryISO = key.split('.').at(-1) ?? ''; - return mockAllCountries[countryISO as keyof typeof mockAllCountries] ?? key; + return Object.entries(mockAllCountries).find(([iso]) => iso === countryISO)?.[1] ?? key; } return key; @@ -211,13 +211,16 @@ describe('CountrySelectionList', () => { const searchedProps = mockedSelectionList.mock.lastCall?.[0]; const expectedSearchResults = searchOptions( 'Uni', - countries.map((countryISO) => ({ - value: countryISO, - keyForList: countryISO, - text: CONST.ALL_COUNTRIES[countryISO as keyof typeof CONST.ALL_COUNTRIES], - isSelected: countryISO === initialCountry, - searchValue: StringUtils.sanitizeString(`${countryISO}${CONST.ALL_COUNTRIES[countryISO as keyof typeof CONST.ALL_COUNTRIES]}`), - })), + countries.map((countryISO) => { + const countryName = Object.entries(CONST.ALL_COUNTRIES).find(([iso]) => iso === countryISO)?.[1] ?? ''; + return { + value: countryISO, + keyForList: countryISO, + text: countryName, + isSelected: countryISO === initialCountry, + searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`), + }; + }), ); expect(searchedProps?.data.map((item) => item.keyForList)).toEqual(expectedSearchResults.map((item) => item.keyForList)); diff --git a/tests/ui/CountrySelectorModalTest.tsx b/tests/ui/CountrySelectorModalTest.tsx index d485a88bec4d..e2b5902f8a7b 100644 --- a/tests/ui/CountrySelectorModalTest.tsx +++ b/tests/ui/CountrySelectorModalTest.tsx @@ -40,7 +40,7 @@ jest.mock('@hooks/useLocalize', () => translate: (key: string) => { if (key.startsWith('allCountries.')) { const countryISO = key.split('.').at(-1) ?? ''; - return mockAllCountries[countryISO as keyof typeof mockAllCountries] ?? key; + return Object.entries(mockAllCountries).find(([iso]) => iso === countryISO)?.[1] ?? key; } return key; @@ -102,12 +102,12 @@ describe('CountrySelectorModal', () => { const searchedProps = mockedSelectionList.mock.lastCall?.[0]; const expectedSearchResults = searchOptions( 'Uni', - Object.keys(CONST.ALL_COUNTRIES).map((countryISO) => ({ + Object.entries(CONST.ALL_COUNTRIES).map(([countryISO, countryName]) => ({ value: countryISO, keyForList: countryISO, - text: CONST.ALL_COUNTRIES[countryISO as keyof typeof CONST.ALL_COUNTRIES], + text: countryName, isSelected: countryISO === 'US', - searchValue: StringUtils.sanitizeString(`${countryISO}${CONST.ALL_COUNTRIES[countryISO as keyof typeof CONST.ALL_COUNTRIES]}`), + searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`), })), ); diff --git a/tests/ui/DynamicContactMethodsPageTest.tsx b/tests/ui/DynamicContactMethodsPageTest.tsx index d4e4f5ba6898..fbeb9c811837 100644 --- a/tests/ui/DynamicContactMethodsPageTest.tsx +++ b/tests/ui/DynamicContactMethodsPageTest.tsx @@ -9,6 +9,7 @@ import LockedAccountModalProvider from '@src/components/LockedAccountModalProvid import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type ReactNative from 'react-native'; import type {ValueOf} from 'type-fest'; import React from 'react'; @@ -29,10 +30,8 @@ jest.mock('@hooks/useDynamicBackPath', () => jest.fn(() => '')); // Mock RenderHTML component jest.mock('@components/RenderHTML', () => { - const ReactMock = require('react') as typeof React; - const {Text} = require('react-native') as { - Text: React.ComponentType<{children?: React.ReactNode}>; - }; + const ReactMock = jest.requireActual('react'); + const {Text} = jest.requireActual('react-native'); return ({html}: {html: string}) => { const plainText = html.replaceAll(/<[^>]*>/g, ''); @@ -42,10 +41,8 @@ jest.mock('@components/RenderHTML', () => { // Replace MenuItem with a simple test double that exposes props in the tree jest.mock('@components/MenuItem', () => { - const ReactMock = require('react') as typeof React; - const {Text} = require('react-native') as { - Text: React.ComponentType<{testID: string; children?: React.ReactNode}>; - }; + const ReactMock = jest.requireActual('react'); + const {Text} = jest.requireActual('react-native'); return ({title, brickRoadIndicator}: {title: string; brickRoadIndicator?: ValueOf}) => ReactMock.createElement(Text, {testID: `menu-${String(title)}`}, `${brickRoadIndicator ?? 'none'}-brickRoadIndicator`); }); diff --git a/tests/ui/DynamicCountrySelectionPageTest.tsx b/tests/ui/DynamicCountrySelectionPageTest.tsx index 52fc8159cf75..5563265537ad 100644 --- a/tests/ui/DynamicCountrySelectionPageTest.tsx +++ b/tests/ui/DynamicCountrySelectionPageTest.tsx @@ -46,7 +46,7 @@ jest.mock('@hooks/useLocalize', () => translate: (key: string) => { if (key.startsWith('allCountries.')) { const countryISO = key.split('.').at(-1) ?? ''; - return mockAllCountries[countryISO as keyof typeof mockAllCountries] ?? key; + return Object.entries(mockAllCountries).find(([iso]) => iso === countryISO)?.[1] ?? key; } return key; @@ -101,12 +101,12 @@ describe('DynamicCountrySelectionPage', () => { const searchedProps = mockedSelectionList.mock.lastCall?.[0]; const expectedSearchResults = searchOptions( 'Uni', - Object.keys(CONST.ALL_COUNTRIES).map((countryISO) => ({ + Object.entries(CONST.ALL_COUNTRIES).map(([countryISO, countryName]) => ({ value: countryISO, keyForList: countryISO, - text: CONST.ALL_COUNTRIES[countryISO as keyof typeof CONST.ALL_COUNTRIES], + text: countryName, isSelected: countryISO === 'US', - searchValue: StringUtils.sanitizeString(`${countryISO}${CONST.ALL_COUNTRIES[countryISO as keyof typeof CONST.ALL_COUNTRIES]}`), + searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`), })), ); diff --git a/tests/ui/ForYouSectionTest.tsx b/tests/ui/ForYouSectionTest.tsx index 56dca0c33ae7..3ed081596824 100644 --- a/tests/ui/ForYouSectionTest.tsx +++ b/tests/ui/ForYouSectionTest.tsx @@ -132,7 +132,7 @@ jest.mock('react-native-reanimated', () => { }); const mockNavigate = jest.mocked(Navigation.navigate); -const mockUseResponsiveLayout = useResponsiveLayout as jest.MockedFunction; +const mockUseResponsiveLayout = jest.mocked(useResponsiveLayout); const mockUseTodoCounts = jest.mocked(useTodoCounts); const ACCOUNT_ID = 12345; @@ -813,7 +813,11 @@ describe('ForYouSection', () => { pressFirstBeginButton(); expect(mockNavigate).toHaveBeenCalledTimes(1); - const calledRoute = mockNavigate.mock.calls.at(0)?.at(0) as string; + const calledRoute = mockNavigate.mock.calls.at(0)?.at(0); + expect(typeof calledRoute).toBe('string'); + if (typeof calledRoute !== 'string') { + return; + } expect(calledRoute).toContain(ROUTES.SEARCH_ROOT.route); }); @@ -832,7 +836,11 @@ describe('ForYouSection', () => { pressFirstBeginButton(); expect(mockNavigate).toHaveBeenCalledTimes(1); - const calledRoute = mockNavigate.mock.calls.at(0)?.at(0) as string; + const calledRoute = mockNavigate.mock.calls.at(0)?.at(0); + expect(typeof calledRoute).toBe('string'); + if (typeof calledRoute !== 'string') { + return; + } expect(calledRoute).toContain(ROUTES.SEARCH_ROOT.route); }); }); diff --git a/tests/ui/LocationPermissionModalTest.tsx b/tests/ui/LocationPermissionModalTest.tsx index f2d2222e6333..88a8faa5ba53 100644 --- a/tests/ui/LocationPermissionModalTest.tsx +++ b/tests/ui/LocationPermissionModalTest.tsx @@ -5,6 +5,8 @@ import AndroidLocationPermissionModal from '@components/LocationPermissionModal/ import getPlatform from '@libs/getPlatform'; +import type * as LocationPermissionModule from '@pages/iou/request/step/IOURequestStepScan/LocationPermission'; + import CONST from '@src/CONST'; import React from 'react'; @@ -15,8 +17,8 @@ import type * as MockUseConfirmModalUtil from '../utils/mockUseConfirmModal'; import {getShowConfirmModalOption, mockShowConfirmModal, resetMockConfirmModal, resolveShowConfirmModal} from '../utils/mockUseConfirmModal'; -const mockGetLocationPermission = jest.fn(); -const mockRequestLocationPermission = jest.fn(); +const mockGetLocationPermission = jest.fn, Parameters>(); +const mockRequestLocationPermission = jest.fn, Parameters>(); const mockUpdateLastLocationPermissionPrompt = jest.fn(); jest.mock('@hooks/useConfirmModal', () => { @@ -59,8 +61,8 @@ jest.mock('@libs/Visibility', () => ({ })); jest.mock('@pages/iou/request/step/IOURequestStepScan/LocationPermission', () => ({ - getLocationPermission: (...args: unknown[]) => mockGetLocationPermission(...args) as Promise, - requestLocationPermission: (...args: unknown[]) => mockRequestLocationPermission(...args) as Promise, + getLocationPermission: (...args: Parameters) => mockGetLocationPermission(...args), + requestLocationPermission: (...args: Parameters) => mockRequestLocationPermission(...args), })); jest.mock('@userActions/IOU/MoneyRequest', () => ({ @@ -74,7 +76,7 @@ jest.mock('react-native-permissions', () => ({ })); const originalOpenSettings = Linking.openSettings?.bind(Linking); -const mockGetPlatform = getPlatform as jest.MockedFunction; +const mockGetPlatform = jest.mocked(getPlatform); function setOpenSettings(openSettings: typeof Linking.openSettings | undefined) { Object.defineProperty(Linking, 'openSettings', { diff --git a/tests/ui/ScanSkipConfirmationTest.tsx b/tests/ui/ScanSkipConfirmationTest.tsx index 6b21c513d9f4..513598e26155 100644 --- a/tests/ui/ScanSkipConfirmationTest.tsx +++ b/tests/ui/ScanSkipConfirmationTest.tsx @@ -20,6 +20,7 @@ import React from 'react'; import Onyx from 'react-native-onyx'; import createRandomTransaction from '../utils/collections/transaction'; +import createMock from '../utils/createMock'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; @@ -63,7 +64,7 @@ jest.mock('@pages/iou/request/step/IOURequestStepScan/hooks/useScanRouteParams', })); jest.mock('@hooks/useFilesValidation', () => { - const ReactLib = require('react') as {createElement: (type: unknown, props?: unknown, ...children: unknown[]) => unknown; Fragment: unknown}; + const ReactLib = jest.requireActual('react'); return (callback: (files: FileObject[]) => void) => { triggerFileSelection = callback; return { @@ -156,20 +157,18 @@ describe('ScanSkipConfirmation submit orchestration', () => { ['route'] - } - navigation={{} as never} + route={createMock['route']>({ + key: 'StepScanSkip', + name: SCREENS.MONEY_REQUEST.STEP_SCAN, + params: { + action: CONST.IOU.ACTION.CREATE, + iouType: CONST.IOU.TYPE.SUBMIT, + reportID: REPORT_ID, + transactionID: TRANSACTION_ID, + pageIndex: 0, + }, + })} + navigation={createMock['navigation']>({})} /> diff --git a/tests/ui/SearchReportAvatarTest.tsx b/tests/ui/SearchReportAvatarTest.tsx index bdfdc2a6b1d2..4c3bbbc19aff 100644 --- a/tests/ui/SearchReportAvatarTest.tsx +++ b/tests/ui/SearchReportAvatarTest.tsx @@ -29,7 +29,7 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' type AvatarData = { uri: string; - avatarID?: number; + avatarID?: number | string; name?: string; parent: string; }; @@ -40,8 +40,8 @@ const parseSource = (source: AvatarSource | IconAsset): string => { if (typeof source === 'string') { return source; } - if (typeof source === 'object' && 'name' in source) { - return source.name as string; + if (typeof source === 'object' && 'name' in source && typeof source.name === 'string') { + return source.name; } if (typeof source === 'object' && 'uri' in source) { return source.uri ?? 'No Source'; @@ -219,16 +219,39 @@ function renderSearchReportAvatar(props: {primaryAvatar?: Icon; secondaryAvatar? ); } +function parseRenderedAvatarData(dataSet: unknown): AvatarData { + if (typeof dataSet !== 'object' || dataSet === null || !('uri' in dataSet) || typeof dataSet.uri !== 'string' || !('parent' in dataSet) || typeof dataSet.parent !== 'string') { + throw new Error('Rendered avatar data is missing its URI or parent'); + } + + const avatarID = 'avatarID' in dataSet ? dataSet.avatarID : undefined; + const name = 'name' in dataSet ? dataSet.name : undefined; + if (avatarID !== undefined && typeof avatarID !== 'number' && typeof avatarID !== 'string') { + throw new Error('Rendered avatar data has an invalid avatar ID'); + } + if (name !== undefined && typeof name !== 'string') { + throw new Error('Rendered avatar data has an invalid name'); + } + + return {uri: dataSet.uri, parent: dataSet.parent, avatarID, name}; +} + async function retrieveAvatarData(props: {primaryAvatar?: Icon; secondaryAvatar?: Icon; avatarType?: ValueOf; reportID: string}) { renderSearchReportAvatar(props); await waitForBatchedUpdatesWithAct(); const images = screen.queryAllByTestId('MockedAvatarData'); - const fragments = screen.queryAllByTestId('ReportActionAvatars-', {exact: false}).map((fragment) => fragment.props.testID as string); + const fragments = screen.queryAllByTestId('ReportActionAvatars-', {exact: false}).map((fragment) => { + const testID: unknown = fragment.props.testID; + if (typeof testID !== 'string') { + throw new Error('Rendered report action avatar fragment is missing its test ID'); + } + return testID; + }); return { - images: images.map((img) => img.props.dataSet as AvatarData), + images: images.map((img) => parseRenderedAvatarData(img.props.dataSet)), fragments, }; } diff --git a/tests/ui/WorkEmailOnboarding.tsx b/tests/ui/WorkEmailOnboarding.tsx index b41b38fc8a9e..ae040eef16b1 100644 --- a/tests/ui/WorkEmailOnboarding.tsx +++ b/tests/ui/WorkEmailOnboarding.tsx @@ -33,6 +33,7 @@ import {NavigationContainer} from '@react-navigation/native'; import React from 'react'; import Onyx from 'react-native-onyx'; +import createMock from '../utils/createMock'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; @@ -282,10 +283,12 @@ describe('OnboardingWorkEmail Page', () => { }); beforeEach(() => { - jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({ - isSmallScreenWidth: false, - shouldUseNarrowLayout: false, - } as ResponsiveLayoutResult); + jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue( + createMock({ + isSmallScreenWidth: false, + shouldUseNarrowLayout: false, + }), + ); }); afterEach(async () => { @@ -639,10 +642,12 @@ describe('OnboardingWorkEmailValidation Page', () => { }); beforeEach(() => { - jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({ - isSmallScreenWidth: false, - shouldUseNarrowLayout: false, - } as ResponsiveLayoutResult); + jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue( + createMock({ + isSmallScreenWidth: false, + shouldUseNarrowLayout: false, + }), + ); }); afterEach(async () => { @@ -984,10 +989,12 @@ describe('OnboardingPrivateDomain Page', () => { }); beforeEach(() => { - jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({ - isSmallScreenWidth: false, - shouldUseNarrowLayout: false, - } as ResponsiveLayoutResult); + jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue( + createMock({ + isSmallScreenWidth: false, + shouldUseNarrowLayout: false, + }), + ); }); afterEach(async () => { diff --git a/tests/unit/ContextMenuActionsCopyMessageTest.ts b/tests/unit/ContextMenuActionsCopyMessageTest.ts index 3a79bb8ee35d..7734f08582f4 100644 --- a/tests/unit/ContextMenuActionsCopyMessageTest.ts +++ b/tests/unit/ContextMenuActionsCopyMessageTest.ts @@ -1,8 +1,13 @@ -import Clipboard from '@libs/Clipboard'; +import type Clipboard from '@libs/Clipboard'; import getClipboardText from '@libs/Clipboard/getClipboardText'; +import type {CanSetHtml} from '@libs/Clipboard/types'; + +import type * as ContextMenuActionsModule from '@pages/inbox/report/ContextMenu/ContextMenuActions'; import CONST from '@src/CONST'; +import type {TranslationParameters, TranslationPaths} from '@src/languages/types'; +import createMock from '../utils/createMock'; import {formatPhoneNumber} from '../utils/TestHelper'; jest.mock( @@ -16,12 +21,16 @@ jest.mock( jest.mock('@components/Reactions/MiniQuickEmojiReactions', () => 'MiniQuickEmojiReactions'); jest.mock('@components/Reactions/QuickEmojiReactions', () => 'QuickEmojiReactions'); +const mockCanSetHtml = jest.fn boolean>>, Parameters>(); +const mockSetString = jest.fn, Parameters>(); +const mockSetHtml = jest.fn, Parameters>(); + jest.mock('@libs/Clipboard', () => ({ __esModule: true, default: { - canSetHtml: jest.fn(), - setString: jest.fn(), - setHtml: jest.fn(), + canSetHtml: mockCanSetHtml, + setString: mockSetString, + setHtml: mockSetHtml, }, })); @@ -30,55 +39,61 @@ jest.mock('@libs/Clipboard/getClipboardText', () => ({ default: jest.fn(), })); -const mockClipboard = Clipboard as { - canSetHtml: jest.Mock; - setString: jest.Mock; - setHtml: jest.Mock; -}; -const mockGetClipboardText = getClipboardText as jest.Mock; - -type ContextMenuAction = { - sentryLabel?: string; - onPress?: (closePopover: boolean, payload: Record) => void; -}; - -const {default: ContextMenuActions} = require('@pages/inbox/report/ContextMenu/ContextMenuActions') as {default: ContextMenuAction[]}; - -const copyMessageAction = ContextMenuActions.find((action) => action.sentryLabel === CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE); - -const createPayload = (selection: string): Record => ({ - reportAction: { - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - message: [{html: selection}], - }, - selection, - report: {}, - originalReport: {}, - getLocalDateFromDatetime: jest.fn(), - policyTags: {}, - translate: (translateKey: string) => translateKey, - formatPhoneNumber, - currentUserPersonalDetails: { - accountID: 1, - login: 'user@expensify.com', - email: 'user@expensify.com', - }, -}); +const mockGetClipboardText = jest.mocked(getClipboardText); + +const {default: ContextMenuActions} = jest.requireActual('@pages/inbox/report/ContextMenu/ContextMenuActions'); + +const copyMessageAction = ContextMenuActions.find((action) => 'sentryLabel' in action && action.sentryLabel === CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE); +if (!copyMessageAction || !('onPress' in copyMessageAction)) { + throw new Error('Copy message context menu action was not found'); +} +type CopyMessagePayload = Parameters[1]; + +const createPayload = (selection: string): CopyMessagePayload => + createMock({ + reportAction: { + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + message: [{html: selection}], + }, + selection, + report: {}, + originalReport: {}, + getLocalDateFromDatetime: jest.fn(), + policyTags: {}, + translate: (path: TPath, ...parameters: TranslationParameters): string => { + if (parameters.length > 0) { + return path; + } + return path; + }, + formatPhoneNumber, + currentUserPersonalDetails: { + accountID: 1, + login: 'user@expensify.com', + email: 'user@expensify.com', + }, + }); -const createReportActionPayload = (reportAction: Record): Record => ({ - reportAction, - selection: '', - report: {}, - originalReport: {}, - getLocalDateFromDatetime: jest.fn(), - policyTags: {}, - translate: (translateKey: string) => translateKey, - currentUserPersonalDetails: { - accountID: 1, - login: 'user@expensify.com', - email: 'user@expensify.com', - }, -}); +const createReportActionPayload = (reportAction: CopyMessagePayload['reportAction']): CopyMessagePayload => + createMock({ + reportAction, + selection: '', + report: {}, + originalReport: {}, + getLocalDateFromDatetime: jest.fn(), + policyTags: {}, + translate: (path: TPath, ...parameters: TranslationParameters): string => { + if (parameters.length > 0) { + return path; + } + return path; + }, + currentUserPersonalDetails: { + accountID: 1, + login: 'user@expensify.com', + email: 'user@expensify.com', + }, + }); describe('ContextMenuActions copy message', () => { beforeEach(() => { @@ -87,7 +102,7 @@ describe('ContextMenuActions copy message', () => { it('uses plain text clipboard path when html clipboard is unavailable', () => { const selection = 'Expensify'; - mockClipboard.canSetHtml.mockReturnValue(false); + mockCanSetHtml.mockReturnValue(false); mockGetClipboardText.mockReturnValue('Expensify'); if (!copyMessageAction?.onPress) { @@ -97,13 +112,13 @@ describe('ContextMenuActions copy message', () => { copyMessageAction.onPress(false, createPayload(selection)); expect(mockGetClipboardText).toHaveBeenCalledWith(selection); - expect(mockClipboard.setString).toHaveBeenCalledWith('Expensify'); - expect(mockClipboard.setHtml).not.toHaveBeenCalled(); + expect(mockSetString).toHaveBeenCalledWith('Expensify'); + expect(mockSetHtml).not.toHaveBeenCalled(); }); it('uses html clipboard path when html clipboard is available', () => { const selection = 'Expensify'; - mockClipboard.canSetHtml.mockReturnValue(true); + mockCanSetHtml.mockReturnValue(true); mockGetClipboardText.mockReturnValue('Expensify'); if (!copyMessageAction?.onPress) { @@ -113,8 +128,8 @@ describe('ContextMenuActions copy message', () => { copyMessageAction.onPress(false, createPayload(selection)); expect(mockGetClipboardText).toHaveBeenCalledWith(selection); - expect(mockClipboard.setHtml).toHaveBeenCalledWith(selection, 'Expensify'); - expect(mockClipboard.setString).not.toHaveBeenCalled(); + expect(mockSetHtml).toHaveBeenCalledWith(selection, 'Expensify'); + expect(mockSetString).not.toHaveBeenCalled(); }); it.each([ @@ -130,7 +145,7 @@ describe('ContextMenuActions copy message', () => { ], [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_AGENT_RULE, {ruleTitle: 'Receipts required'}, 'workspaceActions.agentRule.deleted'], ])('copies the localized message for a %s action', (actionName, originalMessage, expectedTranslationKey) => { - mockClipboard.canSetHtml.mockReturnValue(false); + mockCanSetHtml.mockReturnValue(false); mockGetClipboardText.mockReturnValue('mocked clipboard text'); if (!copyMessageAction?.onPress) { @@ -139,14 +154,16 @@ describe('ContextMenuActions copy message', () => { copyMessageAction.onPress( false, - createReportActionPayload({ - actionName, - message: [{html: ''}], - originalMessage, - }), + createReportActionPayload( + createMock({ + actionName, + message: [{html: ''}], + originalMessage, + }), + ), ); expect(mockGetClipboardText).toHaveBeenCalledWith(expectedTranslationKey); - expect(mockClipboard.setString).toHaveBeenCalledWith('mocked clipboard text'); + expect(mockSetString).toHaveBeenCalledWith('mocked clipboard text'); }); }); diff --git a/tests/unit/ExportDownloadStatusModalTest.tsx b/tests/unit/ExportDownloadStatusModalTest.tsx index 520a01d61a28..23a8fa484cd3 100644 --- a/tests/unit/ExportDownloadStatusModalTest.tsx +++ b/tests/unit/ExportDownloadStatusModalTest.tsx @@ -49,9 +49,9 @@ jest.mock('@hooks/useCurrentUserPersonalDetails', () => ({ default: () => ({accountID: 123, login: 'test@example.com'}), })); -const mockFileDownload = fileDownload as jest.MockedFunction; -const mockSendFromConcierge = sendExportFileFromConcierge as jest.MockedFunction; -const mockClearExportDownload = clearExportDownload as jest.MockedFunction; +const mockFileDownload = jest.mocked(fileDownload); +const mockSendFromConcierge = jest.mocked(sendExportFileFromConcierge); +const mockClearExportDownload = jest.mocked(clearExportDownload); const EXPORT_ID = 'test-export-123'; const CSV_FILE_NAME = 'export_2026-06-09_02-41-38_6a277d629c569.csv'; diff --git a/tests/unit/FocusTrapForModalTest.tsx b/tests/unit/FocusTrapForModalTest.tsx index 283f7399b387..3dab4a547c3c 100644 --- a/tests/unit/FocusTrapForModalTest.tsx +++ b/tests/unit/FocusTrapForModalTest.tsx @@ -4,6 +4,8 @@ import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal/index.web import {markActivePopoverLauncherDeactivated, setActivePopoverLauncher} from '@libs/LauncherStack'; +import type {FocusTrapProps} from 'focus-trap-react'; + import React from 'react'; jest.mock('@libs/LauncherStack', () => ({ @@ -11,15 +13,18 @@ jest.mock('@libs/LauncherStack', () => ({ markActivePopoverLauncherDeactivated: jest.fn(), })); -let capturedOptions: {onActivate?: () => void; onPostDeactivate?: () => void} | null = null; +let capturedOptions: FocusTrapProps['focusTrapOptions'] | null = null; jest.mock('focus-trap-react', () => ({ - FocusTrap: ({focusTrapOptions, children}: {focusTrapOptions: unknown; children: React.ReactNode}) => { - capturedOptions = focusTrapOptions as typeof capturedOptions; + FocusTrap: ({focusTrapOptions, children}: Pick) => { + capturedOptions = focusTrapOptions; return children; }, })); +const mockSetActivePopoverLauncher = jest.mocked(setActivePopoverLauncher); +const mockMarkActivePopoverLauncherDeactivated = jest.mocked(markActivePopoverLauncherDeactivated); + jest.mock('@libs/Accessibility/blurActiveElement', () => ({__esModule: true, default: jest.fn()})); const mockRestoreFocusWithModality = jest.fn(); @@ -46,8 +51,8 @@ function withActiveElement(element: HTMLElement, fn: () => T): T { describe('FocusTrapForModal — launcher capture', () => { beforeEach(() => { capturedOptions = null; - (setActivePopoverLauncher as jest.Mock).mockClear(); - (markActivePopoverLauncherDeactivated as jest.Mock).mockClear(); + mockSetActivePopoverLauncher.mockClear(); + mockMarkActivePopoverLauncherDeactivated.mockClear(); mockRestoreFocusWithModality.mockReset(); document.body.innerHTML = ''; }); diff --git a/tests/unit/awaitStagingDeploysTest.ts b/tests/unit/awaitStagingDeploysTest.ts index 6363525b44a0..b597d97226d5 100644 --- a/tests/unit/awaitStagingDeploysTest.ts +++ b/tests/unit/awaitStagingDeploysTest.ts @@ -1,19 +1,22 @@ import run from '@github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys'; import type CONST from '@github/libs/CONST'; -import type {InternalOctokit} from '@github/libs/GithubUtils'; import GithubUtils from '@github/libs/GithubUtils'; import asMutable from '@src/types/utils/asMutable'; -/* eslint-disable @typescript-eslint/naming-convention */ /** * @jest-environment node */ import * as core from '@actions/core'; +import {GitHub} from '@actions/github/lib/utils'; + +import createMock from '../utils/createMock'; + +/* eslint-disable @typescript-eslint/naming-convention */ type Workflow = { workflow_id: string; - branch: string; + branch?: string; owner: string; }; @@ -37,7 +40,7 @@ const mockGetInput = jest.fn(); const mockListDeploysForTag: MockedFunctionListResponse = jest.fn(); const mockListDeploys: MockedFunctionListResponse = jest.fn(); const mockListPreDeploys: MockedFunctionListResponse = jest.fn(); -const mockListWorkflowRuns = jest.fn().mockImplementation((args: Workflow) => { +const mockListWorkflowRuns = jest.fn, [Workflow]>().mockImplementation((args) => { const defaultReturn = Promise.resolve({data: {workflow_runs: []}}); if (!args.workflow_id) { @@ -59,6 +62,21 @@ const mockListWorkflowRuns = jest.fn().mockImplementation((args: Workflow) => { return defaultReturn; }); +type ListWorkflowRunsMethod = typeof GithubUtils.octokit.actions.listWorkflowRuns; + +function listWorkflowRuns(...parameters: Parameters): ReturnType { + const [args] = parameters; + if (!args) { + return Promise.resolve(createMock>>({data: {workflow_runs: []}})); + } + + return mockListWorkflowRuns({ + workflow_id: String(args.workflow_id), + branch: args.branch, + owner: args.owner, + }).then((response) => createMock>>(response)); +} + jest.mock('@github/libs/CONST', () => ({ ...jest.requireActual('@github/libs/CONST'), POLL_RATE: TEST_POLL_RATE, @@ -69,16 +87,10 @@ beforeAll(() => { asMutable(core).getInput = mockGetInput; // Mock octokit module - const mockOctokit = { - rest: { - actions: { - ...(GithubUtils.internalOctokit as unknown as typeof GithubUtils.octokit.actions), - listWorkflowRuns: mockListWorkflowRuns as unknown as typeof GithubUtils.octokit.actions.listWorkflowRuns, - }, - }, - }; + const mockOctokit = new GitHub(); + jest.spyOn(mockOctokit.rest.actions, 'listWorkflowRuns').mockImplementation(listWorkflowRuns); - GithubUtils.internalOctokit = mockOctokit as InternalOctokit; + GithubUtils.internalOctokit = mockOctokit; }); beforeEach(() => { diff --git a/tests/unit/dismissModalAndOpenReportInInboxTabTest.ts b/tests/unit/dismissModalAndOpenReportInInboxTabTest.ts index 0cf9e662b2e0..06800964c755 100644 --- a/tests/unit/dismissModalAndOpenReportInInboxTabTest.ts +++ b/tests/unit/dismissModalAndOpenReportInInboxTabTest.ts @@ -1,19 +1,25 @@ import dismissModalAndOpenReportInInboxTab from '@libs/Navigation/helpers/dismissModalAndOpenReportInInboxTab'; +import type isReportOpenInRHP from '@libs/Navigation/helpers/isReportOpenInRHP'; import Navigation from '@libs/Navigation/Navigation'; -const mockIsSearchTopmostFullScreenRoute = jest.fn(); -const mockIsReportOpenInRHP = jest.fn(); -const mockGetTrackingState = jest.fn(); +const mockIsSearchTopmostFullScreenRoute = jest.fn(); +const mockIsReportOpenInRHP = jest.fn, Parameters>(); +const mockGetTrackingState = jest.fn(); -jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => () => mockIsSearchTopmostFullScreenRoute() as boolean); -jest.mock('@libs/Navigation/helpers/isReportOpenInRHP', () => () => mockIsReportOpenInRHP() as boolean); -jest.mock('@libs/Navigation/helpers/isReportOpenInSuperWideRHP', () => () => false as boolean); +jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => () => mockIsSearchTopmostFullScreenRoute()); +jest.mock( + '@libs/Navigation/helpers/isReportOpenInRHP', + () => + (...args: Parameters) => + mockIsReportOpenInRHP(...args), +); +jest.mock('@libs/Navigation/helpers/isReportOpenInSuperWideRHP', () => () => false); jest.mock('@libs/Navigation/helpers/setNavigationActionToMicrotaskQueue', () => (callback: () => void) => { callback(); }); -jest.mock('@libs/getIsNarrowLayout', () => () => false as boolean); +jest.mock('@libs/getIsNarrowLayout', () => () => false); jest.mock('@libs/telemetry/submitFollowUpAction', () => ({ - isTracking: (...args: unknown[]) => mockGetTrackingState(...args) as boolean, + isTracking: () => mockGetTrackingState(), endSubmitFollowUpActionSpan: jest.fn(), setPendingSubmitFollowUpAction: jest.fn(), })); @@ -36,7 +42,7 @@ jest.mock('@react-navigation/native'); describe('dismissModalAndOpenReportInInboxTab', () => { beforeEach(() => { jest.clearAllMocks(); - mockGetTrackingState.mockReturnValue(null); + mockGetTrackingState.mockReturnValue(false); mockIsReportOpenInRHP.mockReturnValue(false); mockIsSearchTopmostFullScreenRoute.mockReturnValue(false); }); diff --git a/tests/unit/findUnusedStylesTest.ts b/tests/unit/findUnusedStylesTest.ts index ac36082ec938..063abce58a33 100644 --- a/tests/unit/findUnusedStylesTest.ts +++ b/tests/unit/findUnusedStylesTest.ts @@ -1,29 +1,23 @@ import type {Stats} from 'fs'; +import type * as GlobModule from 'glob'; import {Str} from 'expensify-common'; import * as fs from 'fs'; import {globSync} from 'glob'; import {ComprehensiveStylesFinder} from '../../scripts/findUnusedStyles'; +import createMock from '../utils/createMock'; -jest.mock( - 'fs', - () => - ({ - ...jest.requireActual('fs'), - readFileSync: jest.fn(), - lstatSync: jest.fn(), - }) as typeof fs, -); - -jest.mock( - 'glob', - () => - ({ - ...jest.requireActual('glob'), - globSync: jest.fn(), - }) as unknown as typeof globSync, -); +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + readFileSync: jest.fn(), + lstatSync: jest.fn(), +})); + +jest.mock('glob', () => ({ + ...jest.requireActual('glob'), + globSync: jest.fn(), +})); const mockReadFileSync = jest.mocked(fs.readFileSync); const mockLstatSync = jest.mocked(fs.lstatSync); @@ -34,9 +28,11 @@ describe('findUnusedStyles', () => { beforeEach(() => { jest.clearAllMocks(); - mockLstatSync.mockReturnValue({ - isFile: () => true, - } as Stats); + mockLstatSync.mockReturnValue( + createMock({ + isFile: () => true, + }), + ); }); afterEach(() => { @@ -326,9 +322,11 @@ describe('findUnusedStyles', () => { mockReadFileSync.mockReturnValueOnce('const styles = () => ({});'); // Mock lstatSync to indicate it's a directory, not a file - mockLstatSync.mockReturnValueOnce({ - isFile: () => false, - } as Stats); + mockLstatSync.mockReturnValueOnce( + createMock({ + isFile: () => false, + }), + ); finder = new ComprehensiveStylesFinder('/test');