import type {RenderInfo} from '@components/FlatList/RenderTaskQueue'; import '@shopify/flash-list/jestSetup'; import type {ReactNode} from 'react'; import type React from 'react'; import type * as RNAppLogs from 'react-native-app-logs'; import type {ReadDirItem} from 'react-native-fs'; import type * as RNKeyboardController from 'react-native-keyboard-controller'; import 'react-native-gesture-handler/jestSetup'; import type Animated from 'react-native-reanimated'; import {useMemo} from 'react'; import 'setimmediate'; import mockStorage from 'react-native-onyx/dist/storage/__mocks__'; import {TextDecoder, TextEncoder} from 'util'; import '@src/polyfills/PromiseWithResolvers'; import '@src/polyfills/requestIdleCallback'; import mockFSLibrary from './setupMockFullstoryLib'; import setupMockImages from './setupMockImages'; import setupMockLegendList from './setupMockLegendList'; // Needed for tests to have the necessary environment variables set if (!('GITHUB_REPOSITORY' in process.env)) { (process.env as NodeJS.ProcessEnv).GITHUB_REPOSITORY_OWNER = 'Expensify'; (process.env as NodeJS.ProcessEnv).GITHUB_REPOSITORY = 'Expensify/App'; } setupMockImages(); mockFSLibrary(); setupMockLegendList(); // Polyfill necessary for Onyx.init in jest/setupAfterEnv.ts Object.assign(global, {TextDecoder, TextEncoder}); // This mock is required as per setup instructions for react-navigation testing // https://reactnavigation.org/docs/testing/#mocking-native-modules jest.mock('react-native/src/private/animated/NativeAnimatedHelper'); // Gesture Handler 3 throws in __DEV__ when a GestureDetector has no GestureHandlerRootView above it. In the app every screen // renders under the root view in App.tsx, but tests render screens on their own, so the context pretends a root view is always present. // Jest resolves the package through `main` (lib/module), so the compiled file is the one to mock. jest.mock('react-native-gesture-handler/lib/module/GestureHandlerRootViewContext', () => ({ __esModule: true, default: jest.requireActual('react').createContext(true), })); // Mock react-native-onyx storage layer because the SQLite storage layer doesn't work in jest. // Mocking this file in __mocks__ does not work because jest doesn't support mocking files that are not directly used in the testing project, // and we only want to mock the storage layer, not the whole Onyx module. jest.mock('react-native-onyx/dist/storage', () => mockStorage); // Mock NativeEventEmitter as it is needed to provide mocks of libraries which include it jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter'); // Mock expo-task-manager jest.mock('expo-task-manager', () => ({ defineTask: jest.fn(), // Add other methods here if you use them })); // Mock expo-location — the jest-expo preset replaces all native module methods with jest.fn(async () => {}), // which returns undefined instead of a proper PermissionResponse. This causes crashes when code reads .status // from the result of requestForegroundPermissionsAsync(). jest.mock('expo-location', () => ({ requestForegroundPermissionsAsync: jest.fn(() => Promise.resolve({status: 'granted', granted: true, canAskAgain: true, expires: 0})), requestBackgroundPermissionsAsync: jest.fn(() => Promise.resolve({status: 'granted', granted: true, canAskAgain: true, expires: 0})), getForegroundPermissionsAsync: jest.fn(() => Promise.resolve({status: 'granted', granted: true, canAskAgain: true, expires: 0})), getBackgroundPermissionsAsync: jest.fn(() => Promise.resolve({status: 'granted', granted: true, canAskAgain: true, expires: 0})), getCurrentPositionAsync: jest.fn(() => Promise.resolve({coords: {latitude: 0, longitude: 0, altitude: 0, accuracy: 0, altitudeAccuracy: 0, heading: 0, speed: 0}, timestamp: 0})), hasStartedLocationUpdatesAsync: jest.fn(() => Promise.resolve(false)), startLocationUpdatesAsync: jest.fn(() => Promise.resolve()), stopLocationUpdatesAsync: jest.fn(() => Promise.resolve()), reverseGeocodeAsync: jest.fn(() => Promise.resolve([])), hasServicesEnabledAsync: jest.fn(() => Promise.resolve(true)), PermissionStatus: { GRANTED: 'granted', DENIED: 'denied', UNDETERMINED: 'undetermined', }, Accuracy: { Lowest: 1, Low: 2, Balanced: 3, High: 4, Highest: 5, BestForNavigation: 6, }, ActivityType: { Other: 1, AutomotiveNavigation: 2, Fitness: 3, OtherNavigation: 4, Airborne: 5, }, })); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ __esModule: true, default: { ignoreLogs: jest.fn(), ignoreAllLogs: jest.fn(), }, })); const isVerbose = process.env.JEST_VERBOSE === 'true'; if (!isVerbose) { // Override console methods globally so module-level console calls are silenced too global.console = { ...console, log: jest.fn(), info: jest.fn(), debug: jest.fn(), warn: jest.fn(), } as Console; } // This mock is required for mocking file systems when running tests jest.mock('react-native-fs', () => ({ unlink: jest.fn( () => new Promise((res) => { res(); }), ), readDir: jest.fn( () => new Promise((res) => { res([]); }), ), exists: jest.fn(() => Promise.resolve(false)), mkdir: jest.fn(() => Promise.resolve()), moveFile: jest.fn(() => Promise.resolve()), copyFile: jest.fn(() => Promise.resolve()), writeFile: jest.fn(() => Promise.resolve()), DocumentDirectoryPath: '/mock/documents', CachesDirectoryPath: '/mock/caches', LibraryDirectoryPath: '/mock/library', })); jest.mock('react-native-share', () => ({ default: jest.fn(), })); jest.mock('react-native-reanimated', () => ({ ...jest.requireActual('react-native-reanimated/mock'), createAnimatedPropAdapter: jest.fn, // react-native-reanimated/mock leaves dispatchCommand out (see its own "ADD ME IF NEEDED" comment). forceClearInput // (src/libs/ComponentUtils) dispatches it from a UI-thread worklet, so any test exercising that path needs it mocked. dispatchCommand: jest.fn(), useReducedMotion: jest.fn, useScrollViewOffset: jest.fn(() => 0), useAnimatedRef: jest.fn(() => jest.fn()), LayoutAnimationConfig: ({children}: {children: React.ReactNode}) => children, makeShareableCloneRecursive: jest.fn, })); // eslint-disable-next-line @typescript-eslint/no-unsafe-return jest.mock('react-native-worklets', () => require('react-native-worklets/src/mock')); jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')); jest.mock('react-native-app-logs', () => require('react-native-app-logs/jest')); jest.mock('@src/setup/telemetry', () => ({ __esModule: true, default: jest.fn(), navigationIntegration: { registerNavigationContainer: jest.fn(), }, })); jest.mock('../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ defineTask: jest.fn(), onBackgroundTaskExecution: jest.fn(), })); jest.mock('../modules/hybrid-app/src/NativeReactNativeHybridApp', () => ({ isHybridApp: jest.fn(), closeReactNativeApp: jest.fn(), completeOnboarding: jest.fn(), switchAccount: jest.fn(), clearOldDotAfterSignOut: jest.fn(), })); const mockUseMemo = useMemo; // Mock lazy asset loading to be synchronous in tests jest.mock('../src/hooks/useLazyAsset.ts', () => ({ useMemoizedLazyAsset: jest.fn((importFn) => mockUseMemo(() => { // Return a mock asset immediately to avoid async loading in tests const mockAsset = { src: 'mock-icon', testID: 'mock-asset', // Add common icon properties that tests might expect height: 20, width: 20, }; return { asset: mockAsset, isLoaded: true, isLoading: false, hasError: false, }; }, [importFn]), ), useMemoizedLazyIllustrations: jest.fn((names: readonly string[]) => mockUseMemo(() => { // Return a Record with all requested illustration names const mockIllustrations: Record = {}; for (const name of names) { mockIllustrations[name] = { src: `mock-${name}`, testID: `mock-illustration-${name}`, height: 20, width: 20, }; } return mockIllustrations; // Use a value-based dependency to avoid returning a new object caused by reference changes on names }, [names.join(',')]), ), useMemoizedLazyExpensifyIcons: jest.fn((names: readonly string[]) => mockUseMemo(() => { // Return a Record with all requested icon names const mockIcons: Record = {}; for (const name of names) { mockIcons[name] = { src: `mock-${name}`, testID: `mock-expensify-icon-${name}`, height: 20, width: 20, }; } return mockIcons; // Use a value-based dependency to avoid returning a new object caused by reference changes on names }, [names.join(',')]), ), default: jest.fn((importFn) => mockUseMemo(() => { const mockAsset = {src: 'mock-icon', testID: 'mock-asset'}; return { asset: mockAsset, isLoaded: true, isLoading: false, hasError: false, }; }, [importFn]), ), })); // Mock icon loading functions to resolve immediately jest.mock('../src/components/Icon/ExpensifyIconLoader.ts', () => ({ loadExpensifyIcon: jest.fn((iconName) => { const mockIcon = { src: `mock-${iconName}`, testID: `mock-icon-${iconName}`, height: 20, width: 20, }; return Promise.resolve({default: mockIcon}); }), loadExpensifyIconsChunk: jest.fn(() => Promise.resolve({})), getExpensifyIconsChunk: jest.fn(() => ({})), })); jest.mock('../src/components/Icon/IllustrationLoader.ts', () => ({ loadIllustration: jest.fn(() => Promise.resolve({default: {src: 'mock-illustration', height: 20, width: 20}})), loadIllustrationsChunk: jest.fn(() => Promise.resolve({})), getIllustrationsChunk: jest.fn(() => ({})), })); jest.mock( '@components/FlatList/RenderTaskQueue', () => class SyncRenderTaskQueue { private handler: (info: unknown) => void = () => {}; add(info: RenderInfo) { this.handler(info); } setHandler(handler: () => void) { this.handler = handler; } cancel() {} }, ); jest.mock('@libs/prepareRequestPayload/index.native.ts', () => ({ __esModule: true, default: jest.fn((command: string, data: Record) => { const formData = new FormData(); for (const key of Object.keys(data)) { const value = data[key]; if (value === undefined || value === null) { continue; } formData.append(key, value as string | Blob); } return Promise.resolve(formData); }), })); // This keeps the error "@rnmapbox/maps native code not available." from causing the tests to fail jest.mock('@components/ConfirmedRoute.tsx'); jest.mock('@src/hooks/useWorkletStateMachine/runOnUISync', () => ({ __esModule: true, default: jest.fn(() => jest.fn()), // Return a function that returns a function })); jest.mock('react-native-nitro-sqlite', () => ({ open: jest.fn(), })); jest.mock('react-native-nitro-fetch', () => ({ __esModule: true, fetch: (...args: Parameters) => globalThis.fetch(...args), prefetchOnAppStart: jest.fn(() => Promise.resolve()), registerTokenRefresh: jest.fn(), clearTokenRefresh: jest.fn(), removeFromAutoPrefetch: jest.fn(() => Promise.resolve()), removeAllFromAutoprefetch: jest.fn(() => Promise.resolve()), })); jest.mock('@shopify/react-native-skia', () => ({ useFont: jest.fn(() => null), matchFont: jest.fn(() => null), listFontFamilies: jest.fn(() => []), })); // The native tab bar icons are drawn with Skia's imperative API, which the mock above leaves out, so the tabs get no icons. jest.mock('@libs/Navigation/AppNavigator/Navigators/NativeTabNavigator/tabIconRasterizer', () => jest.fn(() => undefined)); jest.mock('@sbaiahmed1/react-native-biometrics', () => ({ isSensorAvailable: jest.fn(() => Promise.resolve({available: false})), createKeys: jest.fn(() => Promise.resolve({publicKey: ''})), deleteKeys: jest.fn(() => Promise.resolve({success: true})), getAllKeys: jest.fn(() => Promise.resolve({keys: []})), signWithOptions: jest.fn(() => Promise.resolve({success: false})), sha256: jest.fn(() => Promise.resolve({hash: ''})), InputEncoding: {Base64: 'base64', Utf8: 'utf8'}, })); jest.mock('victory-native', () => ({ Bar: jest.fn(() => null), CartesianChart: jest.fn( ({children}: {children?: (args: Record) => ReactNode}) => children?.({points: {y: []}, chartBounds: {left: 0, right: 0, top: 0, bottom: 0}}) ?? null, ), Pie: {Chart: jest.fn(() => null)}, PolarChart: jest.fn(() => null), })); // Provide a default global fetch mock for tests that do not explicitly set it up // This avoids ReferenceError: fetch is not defined in CI when coverage is enabled const globalWithOptionalFetch: typeof globalThis & {fetch?: unknown} = globalThis as typeof globalThis & {fetch?: unknown}; if (typeof globalWithOptionalFetch.fetch !== 'function') { const mockResponse = { ok: true, status: 200, statusText: 'OK', headers: {get: () => null}, // Return a minimal shape our code expects json: () => Promise.resolve({jsonCode: 200}), }; Object.defineProperty(globalWithOptionalFetch, 'fetch', { value: jest.fn(() => Promise.resolve(mockResponse)), writable: true, configurable: true, }); } jest.mock('@components/ActionSheetAwareScrollView/index'); jest.mock('@components/ActionSheetAwareScrollView/index.ios'); jest.mock('@components/ActionSheetAwareScrollView/index.android'); jest.mock('@components/ActionSheetAwareScrollView/ActionSheetAwareScrollViewContext'); jest.mock('@src/components/KeyboardDismissibleFlatList/KeyboardDismissibleFlatListContext'); // Mock document title hooks as no-ops in tests. The web implementation of setPageTitle uses // setTimeout(fn, 0) which accumulates in the fake timer queue. Combined with lodash debounce // in triggerUnreadUpdate (also timer-based), this creates excessive timer churn that causes // heavy integration tests like SessionTest to exceed their timeout. jest.mock('@src/hooks/useDocumentTitle', () => ({ __esModule: true, default: jest.fn(), })); jest.mock('@src/hooks/useWorkspaceDocumentTitle', () => ({ __esModule: true, default: jest.fn(), })); jest.mock('@src/hooks/useDomainDocumentTitle', () => ({ __esModule: true, default: jest.fn(), }));