Skip to content

Commit 647fb69

Browse files
committed
Add full test coverage and testing setup
Introduces comprehensive tests for useAutoHeight and SizedWebView, ensuring 100% coverage enforced by Jest. Updates package.json with testing dependencies and coverage thresholds, documents testing in README, and excludes test files from build output in tsconfig.build.json.
1 parent bb05480 commit 647fb69

File tree

8 files changed

+560
-6
lines changed

8 files changed

+560
-6
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ The example showcases:
6868
- Auto-sizing dynamic HTML with toggled sections.
6969
- Live external sites (Marvel, NFL, Google, Wikipedia, The Verge) embedded without layout thrash.
7070
- Real-time height readouts so you can verify your own endpoints quickly.
71+
- One code path that works the same on iOS, Android, and Expo Go.
7172

7273
> [!NOTE]
7374
> 🧪 The demo is built with Expo; swap the `uri` to test your own pages instantly.
@@ -101,6 +102,14 @@ The example showcases:
101102

102103
Benchmarks were captured on CMS articles up to 3k words in a 60 fps RN dev build. The bridge batches DOM mutations so even long documents resize without thrashing the JS thread.
103104

105+
## ✅ Testing
106+
107+
```sh
108+
yarn test
109+
```
110+
111+
Jest runs with full coverage collection and enforces 100% statements, branches, functions, and lines across the TypeScript source.
112+
104113
## 🛠️ Local Development
105114

106115
```sh

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ export default defineConfig([
2424
},
2525
},
2626
{
27-
ignores: ['node_modules/', 'lib/'],
27+
ignores: ['node_modules/', 'lib/', 'coverage/'],
2828
},
2929
]);

package.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,10 @@
7171
"@react-native/babel-preset": "0.82.1",
7272
"@react-native/eslint-config": "^0.82.1",
7373
"@release-it/conventional-changelog": "^10.0.1",
74+
"@testing-library/react-native": "^13.3.3",
7475
"@types/jest": "^30.0.0",
7576
"@types/react": "^19.2.2",
77+
"@types/react-dom": "^19.2.2",
7678
"babel-plugin-react-compiler": "^1.0.0",
7779
"commitlint": "^20.1.0",
7880
"del-cli": "^7.0.0",
@@ -85,6 +87,7 @@
8587
"react-native": "0.82.1",
8688
"react-native-builder-bob": "^0.40.14",
8789
"react-native-webview": "^13.16.0",
90+
"react-test-renderer": "19.1.0",
8891
"release-it": "^19.0.5",
8992
"typescript": "^5.9.3"
9093
},
@@ -103,7 +106,21 @@
103106
"modulePathIgnorePatterns": [
104107
"<rootDir>/example/node_modules",
105108
"<rootDir>/lib/"
106-
]
109+
],
110+
"collectCoverage": true,
111+
"collectCoverageFrom": [
112+
"src/**/*.{ts,tsx}",
113+
"!src/index.ts",
114+
"!src/constants/autoHeightBridge.ts"
115+
],
116+
"coverageThreshold": {
117+
"global": {
118+
"branches": 100,
119+
"functions": 100,
120+
"lines": 100,
121+
"statements": 100
122+
}
123+
}
107124
},
108125
"commitlint": {
109126
"extends": [

src/__tests__/composeInjectedScript.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,10 @@ describe('composeInjectedScript', () => {
1616
expect(script).toContain('const b = 2;');
1717
expect(script?.trim().endsWith('true;')).toBe(true);
1818
});
19+
20+
it('joins chunks with newlines to keep snippets readable', () => {
21+
const script = composeInjectedScript('a();', 'b();');
22+
23+
expect(script?.split('\n')).toHaveLength(3);
24+
});
1925
});

src/__tests__/index.test.tsx

Lines changed: 204 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,204 @@
1-
it.todo('write a test');
1+
import { render, act } from '@testing-library/react-native';
2+
import { View } from 'react-native';
3+
4+
import { SizedWebView } from '../components/SizedWebView';
5+
import { AUTO_HEIGHT_BRIDGE } from '../constants/autoHeightBridge';
6+
import { composeInjectedScript } from '../utils/composeInjectedScript';
7+
8+
jest.mock('../hooks/useAutoHeight', () => {
9+
const setHeightFromPayload = jest.fn();
10+
return {
11+
__esModule: true,
12+
useAutoHeight: jest.fn(() => ({
13+
height: 240,
14+
setHeightFromPayload,
15+
})),
16+
__setHeightFromPayload: setHeightFromPayload,
17+
};
18+
});
19+
20+
const capturedWebViewProps: Array<Record<string, unknown>> = [];
21+
22+
jest.mock('react-native-webview', () => {
23+
const MockWebView = (props: any) => {
24+
capturedWebViewProps.push(props);
25+
return null;
26+
};
27+
28+
(MockWebView as { displayName?: string }).displayName = 'MockWebView';
29+
30+
return {
31+
__esModule: true,
32+
WebView: MockWebView,
33+
};
34+
});
35+
36+
describe('SizedWebView', () => {
37+
beforeEach(() => {
38+
capturedWebViewProps.length = 0;
39+
40+
const { useAutoHeight } = jest.requireMock('../hooks/useAutoHeight');
41+
const { __setHeightFromPayload } = jest.requireMock(
42+
'../hooks/useAutoHeight'
43+
);
44+
(useAutoHeight as jest.Mock).mockReturnValue({
45+
height: 240,
46+
setHeightFromPayload: __setHeightFromPayload,
47+
});
48+
__setHeightFromPayload.mockClear();
49+
});
50+
51+
it('renders a container view that reflects the measured height', () => {
52+
const onMessage = jest.fn();
53+
54+
const renderResult = render(
55+
<SizedWebView
56+
minHeight={120}
57+
containerStyle={{ backgroundColor: 'red' }}
58+
style={{ opacity: 0.5 }}
59+
source={{ html: '<p>Hello</p>' }}
60+
injectedJavaScriptBeforeContentLoaded="console.log('before');"
61+
injectedJavaScript="console.log('after');"
62+
onMessage={onMessage}
63+
/>
64+
);
65+
66+
const container = renderResult.UNSAFE_getByType(View);
67+
expect(container.props.style).toEqual([
68+
{ height: 240 },
69+
{ backgroundColor: 'red' },
70+
]);
71+
72+
const props = capturedWebViewProps.at(-1) ?? {};
73+
74+
expect(props.style).toEqual([
75+
{ backgroundColor: 'transparent' },
76+
{ opacity: 0.5 },
77+
]);
78+
expect(props.originWhitelist).toEqual(['*']);
79+
expect(props.scrollEnabled).toBe(false);
80+
expect(props.showsVerticalScrollIndicator).toBe(false);
81+
expect(props.javaScriptEnabled).toBe(true);
82+
83+
const bridgeScript = composeInjectedScript(
84+
AUTO_HEIGHT_BRIDGE,
85+
"console.log('before');"
86+
);
87+
expect(props.injectedJavaScriptBeforeContentLoaded).toBe(bridgeScript);
88+
expect(props.injectedJavaScript).toBe(
89+
composeInjectedScript("console.log('after');")
90+
);
91+
92+
act(() => {
93+
renderResult.unmount();
94+
});
95+
});
96+
97+
it('delegates WebView message events to the auto-height hook and user callback', () => {
98+
const { __setHeightFromPayload } = jest.requireMock(
99+
'../hooks/useAutoHeight'
100+
);
101+
const onMessage = jest.fn();
102+
103+
const renderResult = render(
104+
<SizedWebView source={{ html: '<p>Hi</p>' }} onMessage={onMessage} />
105+
);
106+
107+
const webViewProps = capturedWebViewProps.at(-1) ?? {};
108+
const event = { nativeEvent: { data: '360' } } as any;
109+
110+
act(() => {
111+
(webViewProps.onMessage as (evt: unknown) => void)?.(event);
112+
});
113+
114+
expect(__setHeightFromPayload).toHaveBeenCalledWith('360');
115+
expect(onMessage).toHaveBeenCalledWith(event);
116+
117+
act(() => {
118+
renderResult.unmount();
119+
});
120+
});
121+
122+
it('still updates the hook when no onMessage callback is provided', () => {
123+
const { __setHeightFromPayload } = jest.requireMock(
124+
'../hooks/useAutoHeight'
125+
);
126+
127+
const renderResult = render(
128+
<SizedWebView source={{ html: '<p>Hi</p>' }} />
129+
);
130+
131+
const webViewProps = capturedWebViewProps.at(-1) ?? {};
132+
133+
act(() => {
134+
(webViewProps.onMessage as (evt: unknown) => void)?.({
135+
nativeEvent: { data: '480' },
136+
});
137+
});
138+
139+
expect(__setHeightFromPayload).toHaveBeenCalledWith('480');
140+
141+
act(() => {
142+
renderResult.unmount();
143+
});
144+
});
145+
146+
it('forwards custom origin whitelist and scroll props', () => {
147+
const renderResult = render(
148+
<SizedWebView
149+
source={{ html: '<p>Hi</p>' }}
150+
originWhitelist={['https://example.com']}
151+
scrollEnabled
152+
showsVerticalScrollIndicator
153+
/>
154+
);
155+
156+
const props = capturedWebViewProps.at(-1) ?? {};
157+
158+
expect(props.originWhitelist).toEqual(['https://example.com']);
159+
expect(props.scrollEnabled).toBe(true);
160+
expect(props.showsVerticalScrollIndicator).toBe(true);
161+
162+
act(() => {
163+
renderResult.unmount();
164+
});
165+
});
166+
167+
it('passes minHeight and onHeightChange to the auto-height hook', () => {
168+
const onHeightChange = jest.fn();
169+
const hookModule = jest.requireMock('../hooks/useAutoHeight');
170+
171+
const renderResult = render(
172+
<SizedWebView
173+
minHeight={77}
174+
source={{ html: '<p>hook</p>' }}
175+
onHeightChange={onHeightChange}
176+
/>
177+
);
178+
179+
expect(hookModule.useAutoHeight).toHaveBeenLastCalledWith({
180+
minHeight: 77,
181+
onHeightChange,
182+
});
183+
184+
act(() => {
185+
renderResult.unmount();
186+
});
187+
});
188+
189+
it('allows opting out of automatic inset adjustments', () => {
190+
const renderResult = render(
191+
<SizedWebView
192+
source={{ html: '<p>Insets</p>' }}
193+
automaticallyAdjustContentInsets={false}
194+
/>
195+
);
196+
197+
const props = capturedWebViewProps.at(-1) ?? {};
198+
expect(props.automaticallyAdjustContentInsets).toBe(false);
199+
200+
act(() => {
201+
renderResult.unmount();
202+
});
203+
});
204+
});

0 commit comments

Comments
 (0)