From 97c0a021f3f21ad46753db7cacfa4a55ac49cef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20=C4=90=C3=ACnh=20Huy?= Date: Mon, 24 Aug 2026 11:17:33 +0700 Subject: [PATCH] fix(jest): avoid loading native runtime in mock --- src/__tests__/JestMock-test.tsx | 9 ++ src/__tests__/JestMockProvider-test.tsx | 73 +++++++++++++++ src/jest/mock.tsx | 116 +++++++++++++++++------- 3 files changed, 164 insertions(+), 34 deletions(-) create mode 100644 src/__tests__/JestMock-test.tsx create mode 100644 src/__tests__/JestMockProvider-test.tsx diff --git a/src/__tests__/JestMock-test.tsx b/src/__tests__/JestMock-test.tsx new file mode 100644 index 00000000..15b74b54 --- /dev/null +++ b/src/__tests__/JestMock-test.tsx @@ -0,0 +1,9 @@ +import { expect, it, jest } from '@jest/globals'; + +jest.mock('react-native', () => { + throw new Error('The React Native runtime should not be evaluated'); +}); + +it('loads without evaluating the React Native runtime', () => { + expect(() => require('../jest/mock')).not.toThrow(); +}); diff --git a/src/__tests__/JestMockProvider-test.tsx b/src/__tests__/JestMockProvider-test.tsx new file mode 100644 index 00000000..0a249bb5 --- /dev/null +++ b/src/__tests__/JestMockProvider-test.tsx @@ -0,0 +1,73 @@ +import { expect, it, jest } from '@jest/globals'; +import { render, screen } from '@testing-library/react-native'; +import React from 'react'; +import mockSafeAreaContext from '../jest/mock'; +import type { Metrics } from '../SafeArea.types'; + +it('preserves the package runtime export surface', () => { + expect(Object.keys(mockSafeAreaContext).sort()).toEqual([ + 'SafeAreaConsumer', + 'SafeAreaContext', + 'SafeAreaFrameContext', + 'SafeAreaInsetsContext', + 'SafeAreaListener', + 'SafeAreaProvider', + 'SafeAreaView', + 'initialWindowMetrics', + 'initialWindowSafeAreaInsets', + 'useSafeArea', + 'useSafeAreaFrame', + 'useSafeAreaInsets', + 'withSafeAreaInsets', + ]); +}); + +it('provides custom metrics without loading native components', () => { + const metrics: Metrics = { + frame: { x: 1, y: 2, width: 300, height: 600 }, + insets: { top: 10, right: 20, bottom: 30, left: 40 }, + }; + const onRender = jest.fn(); + + const Probe = () => { + onRender({ + frame: mockSafeAreaContext.useSafeAreaFrame(), + insets: mockSafeAreaContext.useSafeAreaInsets(), + }); + return null; + }; + + render( + + + , + ); + + expect(onRender).toHaveBeenLastCalledWith(metrics); +}); + +it('keeps SafeAreaView props available to tests', () => { + render(); + + expect(screen.getByTestId('safe-area-view')).toBeTruthy(); +}); + +it('maps native inset events to SafeAreaListener callbacks', () => { + const onChange = jest.fn(); + const metrics: Metrics = { + frame: { x: 1, y: 2, width: 300, height: 600 }, + insets: { top: 10, right: 20, bottom: 30, left: 40 }, + }; + render( + , + ); + + screen.getByTestId('safe-area-listener').props.onInsetsChange({ + nativeEvent: metrics, + }); + + expect(onChange).toHaveBeenCalledWith(metrics); +}); diff --git a/src/jest/mock.tsx b/src/jest/mock.tsx index 3bb05fa8..798c5497 100644 --- a/src/jest/mock.tsx +++ b/src/jest/mock.tsx @@ -1,10 +1,17 @@ import { jest } from '@jest/globals'; import React, { useContext } from 'react'; -import type { Metrics } from '../SafeArea.types'; import type { + EdgeInsets, + InsetChangedEvent, + Metrics, + NativeSafeAreaViewInstance, + NativeSafeAreaViewProps, + Rect, +} from '../SafeArea.types'; +import type { + SafeAreaListenerProps, SafeAreaProviderProps, - SafeAreaInsetsContext, - SafeAreaFrameContext, + WithSafeAreaInsetsProps, } from '../SafeAreaContext'; const MOCK_INITIAL_METRICS: Metrics = { @@ -22,38 +29,79 @@ const MOCK_INITIAL_METRICS: Metrics = { }, }; -const RNSafeAreaContext = jest.requireActual<{ - SafeAreaInsetsContext: typeof SafeAreaInsetsContext; - SafeAreaFrameContext: typeof SafeAreaFrameContext; -}>('react-native-safe-area-context'); +const SafeAreaInsetsContext = React.createContext(null); +const SafeAreaFrameContext = React.createContext(null); + +const useSafeAreaInsets = jest.fn(() => { + return useContext(SafeAreaInsetsContext) ?? MOCK_INITIAL_METRICS.insets; +}); + +const useSafeAreaFrame = jest.fn(() => { + return useContext(SafeAreaFrameContext) ?? MOCK_INITIAL_METRICS.frame; +}); + +const SafeAreaProvider = ({ + children, + initialMetrics, +}: SafeAreaProviderProps) => { + return ( + + + {children} + + + ); +}; + +const SafeAreaListener = ({ + children, + onChange, + ...props +}: SafeAreaListenerProps) => { + return React.createElement( + 'RNCSafeAreaProvider', + { + ...props, + onInsetsChange: ({ nativeEvent }: InsetChangedEvent) => + onChange(nativeEvent), + }, + children, + ); +}; + +const SafeAreaView = React.forwardRef< + NativeSafeAreaViewInstance, + NativeSafeAreaViewProps +>((props, ref) => { + return React.createElement('RNCSafeAreaView', { ...props, ref }); +}); + +const withSafeAreaInsets = ( + WrappedComponent: React.ComponentType< + (React.PropsWithoutRef | T) & WithSafeAreaInsetsProps + >, +) => + React.forwardRef((props, ref) => { + const insets = useSafeAreaInsets(); + return ; + }); export default { - ...RNSafeAreaContext, + SafeAreaInsetsContext, + SafeAreaFrameContext, + SafeAreaProvider, + SafeAreaListener, + SafeAreaView, + SafeAreaConsumer: SafeAreaInsetsContext.Consumer, + SafeAreaContext: SafeAreaInsetsContext, initialWindowMetrics: MOCK_INITIAL_METRICS, - useSafeAreaInsets: jest.fn(() => { - return ( - useContext(RNSafeAreaContext.SafeAreaInsetsContext) ?? - MOCK_INITIAL_METRICS.insets - ); - }), - useSafeAreaFrame: jest.fn(() => { - return ( - useContext(RNSafeAreaContext.SafeAreaFrameContext) ?? - MOCK_INITIAL_METRICS.frame - ); - }), - // Provide a simpler implementation with default values. - SafeAreaProvider: ({ children, initialMetrics }: SafeAreaProviderProps) => { - return ( - - - {children} - - - ); - }, + initialWindowSafeAreaInsets: null, + useSafeAreaInsets, + useSafeAreaFrame, + useSafeArea: useSafeAreaInsets, + withSafeAreaInsets, };