From a2597db0a79b52b3766575c69468b3969fc3f7dc Mon Sep 17 00:00:00 2001 From: Bao Nguyen Date: Thu, 13 Aug 2026 16:21:09 +0700 Subject: [PATCH] fix(jest): keep mocked hooks working after jest.resetAllMocks() The jest mock wraps useSafeAreaInsets and useSafeAreaFrame in jest.fn() so tests can override them. jest.resetAllMocks(), which apps commonly call from a global beforeEach/afterEach, strips the implementation off every mock function. The hooks then return undefined and any component reading insets.top or frame.width throws. Wrap the two hooks in a Proxy that reinstalls the default implementation when getMockImplementation() reports it has been stripped. Overrides made with mockReturnValue/mockImplementation set an implementation, so they still take precedence, and call tracking is untouched. The generated jest/mock.d.ts is byte identical, so this is not a type change. Fixes #551 --- src/jest/__tests__/mock-test.tsx | 64 ++++++++++++++++++++++++++++++++ src/jest/mock.tsx | 22 ++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 src/jest/__tests__/mock-test.tsx 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