jest/setup.ts
Rama main · réplica de lectura del repositorio original.
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<typeof React>('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<void>((res) => {
res();
}),
),
readDir: jest.fn(
() =>
new Promise<ReadDirItem[]>((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<typeof Animated>('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<typeof RNKeyboardController>('react-native-keyboard-controller/jest'));
jest.mock('react-native-app-logs', () => require<typeof RNAppLogs>('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<string, unknown> = {};
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<string, unknown> = {};
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<string, unknown>) => {
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<typeof fetch>) => 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<string, unknown>) => 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(),
}));