Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/__tests__/JestMock-test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
73 changes: 73 additions & 0 deletions src/__tests__/JestMockProvider-test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<mockSafeAreaContext.SafeAreaProvider initialMetrics={metrics}>
<Probe />
</mockSafeAreaContext.SafeAreaProvider>,
);

expect(onRender).toHaveBeenLastCalledWith(metrics);
});

it('keeps SafeAreaView props available to tests', () => {
render(<mockSafeAreaContext.SafeAreaView testID="safe-area-view" />);

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(
<mockSafeAreaContext.SafeAreaListener
testID="safe-area-listener"
onChange={onChange}
/>,
);

screen.getByTestId('safe-area-listener').props.onInsetsChange({
nativeEvent: metrics,
});

expect(onChange).toHaveBeenCalledWith(metrics);
});
116 changes: 82 additions & 34 deletions src/jest/mock.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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<EdgeInsets | null>(null);
const SafeAreaFrameContext = React.createContext<Rect | null>(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 (
<SafeAreaFrameContext.Provider
value={initialMetrics?.frame ?? MOCK_INITIAL_METRICS.frame}
>
<SafeAreaInsetsContext.Provider
value={initialMetrics?.insets ?? MOCK_INITIAL_METRICS.insets}
>
{children}
</SafeAreaInsetsContext.Provider>
</SafeAreaFrameContext.Provider>
);
};

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 = <T,>(
WrappedComponent: React.ComponentType<
(React.PropsWithoutRef<T> | T) & WithSafeAreaInsetsProps
>,
) =>
React.forwardRef<unknown, T>((props, ref) => {
const insets = useSafeAreaInsets();
return <WrappedComponent {...props} insets={insets} ref={ref} />;
});

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 (
<RNSafeAreaContext.SafeAreaFrameContext.Provider
value={initialMetrics?.frame ?? MOCK_INITIAL_METRICS.frame}
>
<RNSafeAreaContext.SafeAreaInsetsContext.Provider
value={initialMetrics?.insets ?? MOCK_INITIAL_METRICS.insets}
>
{children}
</RNSafeAreaContext.SafeAreaInsetsContext.Provider>
</RNSafeAreaContext.SafeAreaFrameContext.Provider>
);
},
initialWindowSafeAreaInsets: null,
useSafeAreaInsets,
useSafeAreaFrame,
useSafeArea: useSafeAreaInsets,
withSafeAreaInsets,
};