Skip to content

Native modules

Standalone Hermes has no React Native runtime. There is no bridge, no TurboModule infrastructure, and nothing behind NativeModules.

Argus aliases the react-native specifier to an in-realm shim at bundle time, so code that imports from it resolves to something real — and you decide what each native module returns.

import { NativeModules, TurboModuleRegistry } from 'react-native';
describe('BiometricsGate', () => {
beforeEach(() => {
argus.resetNativeModules();
argus.mockNativeModule('Biometrics', () => ({
isAvailable: argus.fn().mockReturnValue(true),
authenticate: argus.fn().mockResolvedValue({ ok: true }),
}));
});
test('authenticates when hardware is available', async () => {
const gate = new BiometricsGate();
await expect(gate.unlock()).resolves.toBe(true);
const native = TurboModuleRegistry.getEnforcing('Biometrics');
expect(native.authenticate).toHaveBeenCalled();
});
});

mockNativeModule(name, factory) calls the factory immediately and stores the result. The same object is then returned by all three access paths:

Access Returns
NativeModules.Biometrics The registered object
TurboModuleRegistry.get('Biometrics') The registered object, or null if unregistered
TurboModuleRegistry.getEnforcing('Biometrics') The registered object, or throws if unregistered

getEnforcing on an unregistered name throws TurboModuleRegistry.getEnforcing('X'): module not registered — which is the same failure shape production code sees when a native module is genuinely missing, so error paths are testable.

The registry is global to the file’s realm. argus.resetNativeModules() empties it.

beforeEach(() => {
argus.resetNativeModules();
});

Do it in beforeEach rather than afterEach: a test that fails midway then still starts the next one from a clean registry.

The same react-native alias exports a minimal set of host components, which is what component testing renders against:

import { Pressable, Text, TextInput, View } from 'react-native';
Component Props the queries and events understand
View testID, accessibilityRole, children, anything else
Text as View; its text content is what getByText matches
Pressable onPress, disabled
TextInput value, defaultValue, placeholder, editable, onChangeText

UIManager is exported as an empty object so imports of it resolve without a crash.

These are host-component names, not implementations. Nothing lays out, measures, or paints — see Limitations.

This shim covers native-module infrastructure, not the React Native JavaScript library. AppState, Dimensions, Animated, Linking, AsyncStorage and friends are not implemented.

When your code depends on one of those, take it as a dependency instead of importing it deep in the call stack, and pass a fake in tests. Code written that way is testable on every runner, not just this one.