diff --git a/src/jest/__tests__/mock-test.tsx b/src/jest/__tests__/mock-test.tsx
new file mode 100644
index 0000000..2bd709c
--- /dev/null
+++ b/src/jest/__tests__/mock-test.tsx
@@ -0,0 +1,64 @@
+import { beforeEach, describe, expect, it, jest } from '@jest/globals';
+import { render, screen } from '@testing-library/react-native';
+import * as React from 'react';
+import { Text } from 'react-native';
+import type { Metrics } from '../../SafeArea.types';
+import mockSafeAreaContext from '../mock';
+
+// This is the setup consuming apps are told to use in the README.
+jest.mock('react-native-safe-area-context', () => mockSafeAreaContext);
+
+const { SafeAreaProvider, useSafeAreaFrame, useSafeAreaInsets } =
+ require('react-native-safe-area-context') as typeof import('../../index');
+
+const TEST_METRICS: Metrics = {
+ insets: { top: 1, left: 2, right: 3, bottom: 4 },
+ frame: { x: 0, y: 0, width: 100, height: 200 },
+};
+
+const PrintMetricsTestView = () => {
+ const insets = useSafeAreaInsets();
+ const frame = useSafeAreaFrame();
+ return {`${insets.top} ${frame.width}`};
+};
+
+const renderTestView = () =>
+ render(
+
+
+ ,
+ );
+
+describe('jest mock', () => {
+ beforeEach(() => {
+ // Consuming apps commonly do this in a global setup file, which used to
+ // strip the mock implementations and make the hooks return undefined.
+ jest.resetAllMocks();
+ });
+
+ it('keeps returning metrics after jest.resetAllMocks()', () => {
+ renderTestView();
+
+ expect(screen.getByText('1 100')).toBeDefined();
+ });
+
+ it('still lets tests override the hooks after jest.resetAllMocks()', () => {
+ jest.mocked(useSafeAreaInsets).mockReturnValue({
+ top: 42,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ });
+
+ renderTestView();
+
+ expect(screen.getByText('42 100')).toBeDefined();
+ });
+
+ it('still records calls after jest.resetAllMocks()', () => {
+ renderTestView();
+
+ expect(jest.mocked(useSafeAreaInsets)).toHaveBeenCalledTimes(1);
+ expect(jest.mocked(useSafeAreaFrame)).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/jest/mock.tsx b/src/jest/mock.tsx
index 3bb05fa..2747cf5 100644
--- a/src/jest/mock.tsx
+++ b/src/jest/mock.tsx
@@ -27,16 +27,34 @@ const RNSafeAreaContext = jest.requireActual<{
SafeAreaFrameContext: typeof SafeAreaFrameContext;
}>('react-native-safe-area-context');
+// `jest.resetAllMocks()`, which apps commonly call from a global `beforeEach`,
+// strips the implementation off every mock function. That would make these
+// hooks return `undefined` and break every component reading insets or frame.
+// Reinstall the default implementation when it has been stripped, so the hooks
+// keep working while tests can still override them with `mockReturnValue` and
+// friends.
+function mockHook unknown>(implementation: T) {
+ const hook = jest.fn(implementation);
+ return new Proxy(hook, {
+ apply(target, thisArg, args) {
+ if (target.getMockImplementation() == null) {
+ target.mockImplementation(implementation);
+ }
+ return Reflect.apply(target, thisArg, args);
+ },
+ });
+}
+
export default {
...RNSafeAreaContext,
initialWindowMetrics: MOCK_INITIAL_METRICS,
- useSafeAreaInsets: jest.fn(() => {
+ useSafeAreaInsets: mockHook(() => {
return (
useContext(RNSafeAreaContext.SafeAreaInsetsContext) ??
MOCK_INITIAL_METRICS.insets
);
}),
- useSafeAreaFrame: jest.fn(() => {
+ useSafeAreaFrame: mockHook(() => {
return (
useContext(RNSafeAreaContext.SafeAreaFrameContext) ??
MOCK_INITIAL_METRICS.frame