Files
Johannes Klein 1f17d7a23d Update jest.setup.js
Root cause
Integration tests that open ObsEdit (and related flows) render DraggableFlatList, which uses useAnimatedScrollHandler. In Jest, Reanimated 4.4’s useHandler compares worklet closures via Object.keys(next.__closure), but handlers from the prebuilt node_modules package sometimes have no __closure, causing:

TypeError: Cannot convert undefined or null to object
  at areWorkletClosuresEqual (useHandler.ts:69)
That showed up in 6 integration suites (ObsEdit, PhotoDeletion, MediaViewer, etc.). Some runs also timed out when the error left the UI stuck.

Fix
Added a Jest mock for react-native-draggable-flatlist in tests/jest.setup.js that uses a plain FlatList plus a no-op ScaleDecorator — enough for evidence-list tests without running real scroll worklets.
2026-09-09 01:01:29 +02:00

257 lines
9.0 KiB
JavaScript

// Recommendation from the uuid library is to import get-random-values before
// uuid, so we're importing it first thing in the entry point.
// https://www.npmjs.com/package/uuid#react-native--expo
// eslint-disable-next-line simple-import-sort/imports
import "react-native-get-random-values";
import "@shopify/flash-list/jestSetup";
import mockBottomSheet from "@gorhom/bottom-sheet/mock";
import mockRNCNetInfo from "@react-native-community/netinfo/jest/netinfo-mock";
import inatjs from "inaturalistjs";
import fetchMock from "jest-fetch-mock";
import mockRNDeviceInfo from "react-native-device-info/jest/react-native-device-info-mock";
import mockSafeAreaContext from "react-native-safe-area-context/jest/mock";
import factory, { makeResponse } from "./factory";
// tests seem to be slower without this global reanimated mock
global.ReanimatedDataMock = {
now: () => 0,
};
global.requestIdleCallback = callback => setTimeout( callback, 0 );
global.cancelIdleCallback = id => clearTimeout( id );
jest.mock( "react-native-volume-manager", () => ( {
VolumeManager: {
getVolume: jest.fn( () => Promise.resolve( 0.5 ) ),
setVolume: jest.fn( ),
addVolumeListener: jest.fn( () => ( { remove: jest.fn() } ) ),
showNativeVolumeUI: jest.fn(),
},
} ) );
// Mock the react-native-logs config because it has a dependency on AuthenticationService
// instead use console.logs for tests
jest.mock( "../react-native-logs.config", () => {
const log = {
extend: jest.fn( () => ( {
debug: msg => console.debug( msg ),
info: msg => console.info( msg ),
warn: msg => console.warn( msg ),
error: msg => console.error( msg ),
debugWithExtra: ( ...args ) => console.debug( ...args ),
infoWithExtra: ( ...args ) => console.info( ...args ),
warnWithExtra: ( ...args ) => console.warn( ...args ),
errorWithExtra: ( ...args ) => console.error( ...args ),
} ) ),
};
return {
log,
legacyLogfilePath: "inaturalist-rn-log.txt",
logFileNamePrefix: "inaturalist-rn-log",
logFileDirectory: "logs",
logWithoutRemote: log,
};
} );
jest.mock( "@gorhom/bottom-sheet", () => ( {
...mockBottomSheet,
__esModule: true,
} ) );
jest.mock( "@react-native-community/netinfo", () => mockRNCNetInfo );
jest.mock( "react-native-device-info", () => mockRNDeviceInfo );
jest.mock( "react-native-safe-area-context", () => mockSafeAreaContext );
// Reanimated 4.2 + Worklets 0.7: Jest loads native worklets which fails in Node. See:
// https://github.com/software-mansion/react-native-reanimated/discussions/8806
// we can remove this once the fix is released
jest.mock( "react-native-worklets", () => require( "react-native-worklets/src/mock" ) );
// Reanimated 4.4 + draggable-flatlist: prebuilt scroll handlers in node_modules
// lack __closure on some renders, crashing useHandler in Jest (Object.keys on
// undefined). FlatList stand-in is enough for evidence-list integration tests.
jest.mock( "react-native-draggable-flatlist", () => {
const React = jest.requireActual( "react" );
const { FlatList, View } = jest.requireActual( "react-native" );
const DraggableFlatList = React.forwardRef( ( props, ref ) => (
React.createElement( FlatList, { ...props, ref } )
) );
const ScaleDecorator = ( { children } ) => (
React.createElement( View, null, children )
);
return {
__esModule: true,
default: DraggableFlatList,
ScaleDecorator,
};
} );
require( "react-native-reanimated" ).setUpTests();
// Some test environments may need a little more time
jest.setTimeout( 50000 );
// Mock inaturalistjs so we can make some fake responses
jest.mock( "inaturalistjs" );
inatjs.observations.observers.mockResolvedValue( makeResponse( ) );
inatjs.observations.search.mockResolvedValue( makeResponse( ) );
inatjs.observations.speciesCounts.mockResolvedValue( makeResponse( ) );
inatjs.observations.updates.mockResolvedValue( makeResponse( ) );
inatjs.users.projects.mockResolvedValue( makeResponse( ) );
inatjs.project_observations.create.mockResolvedValue( makeResponse( ) );
inatjs.project_observations.update.mockResolvedValue( makeResponse( ) );
inatjs.project_observations.delete.mockResolvedValue( undefined );
inatjs.observation_field_values.create.mockResolvedValue( makeResponse( ) );
inatjs.observation_field_values.update.mockResolvedValue( makeResponse( ) );
inatjs.observation_field_values.delete.mockResolvedValue( undefined );
inatjs.computervision.score_image.mockResolvedValue( makeResponse( ) );
// Set up mocked fetch for testing (or disabling) fetch requests
fetchMock.enableMocks( );
fetchMock.dontMock( );
const mockIconicTaxon = factory( "RemoteTaxon", {
is_iconic: true,
name: "Mock iconic taxon",
} );
inatjs.taxa.search.mockResolvedValue( makeResponse( [mockIconicTaxon] ) );
inatjs.announcements.search.mockResolvedValue( makeResponse( ) );
inatjs.observations.updates.mockResolvedValue( makeResponse( ) );
// the following two mocks are both needed for react-native-keep-awake
jest.mock( "@sayem314/react-native-keep-awake" );
jest.mock( "react-native/Libraries/TurboModule/TurboModuleRegistry", () => {
const turboModuleRegistry = jest
.requireActual( "react-native/Libraries/TurboModule/TurboModuleRegistry" );
return {
...turboModuleRegistry,
getEnforcing: name => {
// List of TurboModules libraries to mock.
const modulesToMock = ["ReactNativeKCKeepAwake", "FileReaderModule"];
if ( modulesToMock.includes( name ) ) {
return null;
}
return turboModuleRegistry.getEnforcing( name );
},
};
} );
jest.mock( "react-native-restart", ( ) => ( {
restart: jest.fn( ),
} ) );
jest.mock( "@react-native-firebase/analytics", () => ( {
getAnalytics: jest.fn( ),
logEvent: jest.fn( ),
setAnalyticsCollectionEnabled: jest.fn( ),
} ) );
jest.mock( "@react-native-firebase/perf", () => ( {
getPerformance: jest.fn( ( ) => ( {
dataCollectionEnabled: true,
startTrace: jest.fn( async ( ) => ( {
stop: jest.fn( ),
putAttribute: jest.fn( ),
} ) ),
} ) ),
} ) );
jest.mock( "react-native-legal", () => ( {
launchLicenseListScreen: jest.fn(),
} ) );
// see https://stackoverflow.com/questions/42268673/jest-test-animated-view-for-react-native-app
// for more details about this withAnimatedTimeTravelEnabled approach. basically, this
// allows us to step through animation frames when a screen is first loading when we're using the
// FadeInView animation for navigation screen transitions
global.withAnimatedTimeTravelEnabled = ( options = {} ) => {
beforeEach( () => {
if ( !options.skipFakeTimers ) {
jest.useFakeTimers();
jest.setSystemTime( new Date( 0 ) );
}
} );
if ( !options.skipFakeTimers ) {
afterEach( () => {
jest.useRealTimers();
} );
}
};
const frameTime = 10;
global.timeTravel = ( time = frameTime ) => {
const tickTravel = () => {
const now = Date.now();
jest.setSystemTime( new Date( now + frameTime ) );
jest.advanceTimersByTime( frameTime );
};
// Step through each of the frames
const frames = time / frameTime;
for ( let i = 0; i < frames; i += 1 ) {
tickTravel();
}
};
jest.mock( "sharedHelpers/installData", ( ) => ( {
// For most tests it's just going to be burden to dismiss this thing that
// most users only ever see once. If we do want to test it, we can redefine
// this mock
useOnboardingShown: jest.fn( ( ) => [true, jest.fn()] ),
getInstallID: jest.fn( ( ) => "fake-installation-id" ),
getEnvironmentOverride: jest.fn( ( ) => undefined ),
setEnvironmentOverride: jest.fn( ( ) => { } ),
} ) );
jest.mock( "components/SharedComponents/Buttons/Button", () => {
const actualButton = jest.requireActual(
"components/SharedComponents/Buttons/Button",
).default;
// Use a very short debounce time (10ms) in tests to simulate the 300ms
// debounce time in the actual Button component
return jest.fn( props => actualButton( { ...props, debounceTime: 10 } ) );
} );
jest.mock( "navigation/FadeInView", () => {
// jest.requireActual instead of require: nativewind's babel plugin rewrites
// React.createElement from a plain require("react") to reference a hoisted
// import, which jest.mock factories aren't allowed to touch
const React = jest.requireActual( "react" );
const { View } = jest.requireActual( "react-native" );
return jest.fn( ( { children } ) => React.createElement( View, null, children ) );
} );
jest.mock( "components/Camera/FadeInOutView", () => {
const React = jest.requireActual( "react" );
const { View } = jest.requireActual( "react-native" );
return jest.fn( ( ) => React.createElement( View, null ) );
} );
// Disable bottom tab fade animation in tests to prevent act() warnings
jest.mock( "navigation/BottomTabNavigator/tabScreenOptions", () => ( {
__esModule: true,
default: {
lazy: true,
freezeOnBlur: true,
headerShown: false,
animation: "none",
},
} ) );
// this silences console methods in jest tests, to make them less noisy
// and easier to debug. uncomment if you want to silence them
global.console = {
...console,
// info: jest.fn(),
// error: jest.fn(),
// warn: jest.fn()
// log: jest.fn()
};