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,
};