Skip to content

Commit 23ac3dc

Browse files
chore: rewrite render tests (#1859)
1 parent d4c6303 commit 23ac3dc

File tree

3 files changed

+266
-56
lines changed

3 files changed

+266
-56
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"clean": "del build",
2727
"test": "jest",
2828
"test:ci": "jest --maxWorkers=2",
29-
"test:ci:coverage": "jest --maxWorkers=2 --collectCoverage=true --coverage-provider=v8",
29+
"test:ci:coverage": "jest --maxWorkers=2 --collectCoverage=true",
3030
"test:codemods": "node scripts/test-codemods.mjs",
3131
"typecheck": "tsc",
3232
"lint": "eslint src --cache",

src/__tests__/render.test.tsx

Lines changed: 262 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,82 +3,290 @@ import { Text, View } from 'react-native';
33

44
import { render, screen } from '..';
55

6-
class Banana extends React.Component<any, { fresh: boolean }> {
7-
state = {
8-
fresh: false,
9-
};
10-
11-
componentDidUpdate() {
12-
if (this.props.onUpdate) {
13-
this.props.onUpdate();
6+
test('renders a simple component', async () => {
7+
const TestComponent = () => (
8+
<View testID="container">
9+
<Text>Hello World</Text>
10+
</View>
11+
);
12+
13+
await render(<TestComponent />);
14+
15+
expect(screen.getByTestId('container')).toBeOnTheScreen();
16+
expect(screen.getByText('Hello World')).toBeOnTheScreen();
17+
});
18+
19+
describe('render options', () => {
20+
test('renders component with wrapper option', async () => {
21+
const TestComponent = () => <Text testID="inner">Inner Content</Text>;
22+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
23+
<View testID="wrapper">{children}</View>
24+
);
25+
26+
await render(<TestComponent />, { wrapper: Wrapper });
27+
28+
expect(screen.getByTestId('wrapper')).toBeOnTheScreen();
29+
expect(screen.getByTestId('inner')).toBeOnTheScreen();
30+
expect(screen.getByText('Inner Content')).toBeOnTheScreen();
31+
});
32+
33+
test('createNodeMock option is passed to renderer', async () => {
34+
const TestComponent = () => <View testID="test" />;
35+
const mockNode = { testProperty: 'testValue' };
36+
const createNodeMock = jest.fn(() => mockNode);
37+
38+
await render(<TestComponent />, { createNodeMock });
39+
40+
expect(screen.getByTestId('test')).toBeOnTheScreen();
41+
});
42+
});
43+
44+
describe('component rendering', () => {
45+
test('render accepts RCTText component', async () => {
46+
await render(React.createElement('RCTText', { testID: 'text' }, 'Hello'));
47+
expect(screen.getByTestId('text')).toBeOnTheScreen();
48+
expect(screen.getByText('Hello')).toBeOnTheScreen();
49+
});
50+
51+
test('render throws when text string is rendered without Text component', async () => {
52+
await expect(render(<View>Hello</View>)).rejects.toThrowErrorMatchingInlineSnapshot(
53+
`"Invariant Violation: Text strings must be rendered within a <Text> component. Detected attempt to render "Hello" string within a <View> component."`,
54+
);
55+
});
56+
});
57+
58+
describe('rerender', () => {
59+
test('rerender updates component with new props', async () => {
60+
interface TestComponentProps {
61+
message: string;
62+
}
63+
const TestComponent = ({ message }: TestComponentProps) => (
64+
<Text testID="message">{message}</Text>
65+
);
66+
67+
await render(<TestComponent message="Initial" />);
68+
69+
expect(screen.getByText('Initial')).toBeOnTheScreen();
70+
71+
await screen.rerender(<TestComponent message="Updated" />);
72+
73+
expect(screen.getByText('Updated')).toBeOnTheScreen();
74+
expect(screen.queryByText('Initial')).not.toBeOnTheScreen();
75+
});
76+
77+
test('rerender works with wrapper option', async () => {
78+
interface TestComponentProps {
79+
value: number;
1480
}
81+
const TestComponent = ({ value }: TestComponentProps) => <Text testID="value">{value}</Text>;
82+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
83+
<View testID="wrapper">{children}</View>
84+
);
85+
86+
await render(<TestComponent value={1} />, {
87+
wrapper: Wrapper,
88+
});
89+
90+
expect(screen.getByText('1')).toBeOnTheScreen();
91+
92+
await screen.rerender(<TestComponent value={2} />);
93+
94+
expect(screen.getByText('2')).toBeOnTheScreen();
95+
expect(screen.getByTestId('wrapper')).toBeOnTheScreen();
96+
});
97+
});
98+
99+
test('unmount removes component from tree', async () => {
100+
const TestComponent = () => <Text testID="content">Content</Text>;
101+
102+
await render(<TestComponent />);
103+
104+
expect(screen.getByTestId('content')).toBeOnTheScreen();
105+
106+
await screen.unmount();
107+
108+
expect(screen.queryByTestId('content')).not.toBeOnTheScreen();
109+
});
110+
111+
test('rerender calls componentDidUpdate and unmount calls componentWillUnmount', async () => {
112+
interface ClassComponentProps {
113+
onUpdate?: () => void;
114+
onUnmount?: () => void;
15115
}
116+
class ClassComponent extends React.Component<ClassComponentProps> {
117+
componentDidUpdate() {
118+
if (this.props.onUpdate) {
119+
this.props.onUpdate();
120+
}
121+
}
122+
123+
componentWillUnmount() {
124+
if (this.props.onUnmount) {
125+
this.props.onUnmount();
126+
}
127+
}
16128

17-
componentWillUnmount() {
18-
if (this.props.onUnmount) {
19-
this.props.onUnmount();
129+
render() {
130+
return <Text>Class Component</Text>;
20131
}
21132
}
22133

23-
changeFresh = () => {
24-
this.setState((state) => ({
25-
fresh: !state.fresh,
26-
}));
27-
};
134+
const onUpdate = jest.fn();
135+
const onUnmount = jest.fn();
136+
await render(<ClassComponent onUpdate={onUpdate} onUnmount={onUnmount} />);
137+
expect(onUpdate).toHaveBeenCalledTimes(0);
138+
expect(onUnmount).toHaveBeenCalledTimes(0);
139+
140+
await screen.rerender(<ClassComponent onUpdate={onUpdate} onUnmount={onUnmount} />);
141+
expect(onUpdate).toHaveBeenCalledTimes(1);
142+
expect(onUnmount).toHaveBeenCalledTimes(0);
143+
144+
await screen.unmount();
145+
expect(onUpdate).toHaveBeenCalledTimes(1);
146+
expect(onUnmount).toHaveBeenCalledTimes(1);
147+
});
148+
149+
describe('toJSON', () => {
150+
test('toJSON returns null for empty children', async () => {
151+
const TestComponent = () => null;
28152

29-
render() {
30-
return (
153+
await render(<TestComponent />);
154+
155+
expect(screen.toJSON()).toMatchInlineSnapshot(`null`);
156+
});
157+
158+
test('toJSON returns single child when only one child exists', async () => {
159+
const TestComponent = () => (
31160
<View>
32-
<Text>Is the banana fresh?</Text>
33-
<Text testID="bananaFresh">{this.state.fresh ? 'fresh' : 'not fresh'}</Text>
161+
<Text testID="single">Single Child</Text>
34162
</View>
35163
);
36-
}
37-
}
38164

39-
test('render renders component asynchronously', async () => {
40-
await render(<View testID="test" />);
41-
expect(screen.getByTestId('test')).toBeOnTheScreen();
165+
await render(<TestComponent />);
166+
167+
expect(screen.toJSON()).toMatchInlineSnapshot(`
168+
<View>
169+
<Text
170+
testID="single"
171+
>
172+
Single Child
173+
</Text>
174+
</View>
175+
`);
176+
});
177+
178+
test('toJSON returns full tree for React fragment with multiple children', async () => {
179+
const TestComponent = () => (
180+
<>
181+
<Text testID="first">First</Text>
182+
<Text testID="second">Second</Text>
183+
</>
184+
);
185+
186+
await render(<TestComponent />);
187+
188+
expect(screen.toJSON()).toMatchInlineSnapshot(`
189+
<>
190+
<Text
191+
testID="first"
192+
>
193+
First
194+
</Text>
195+
<Text
196+
testID="second"
197+
>
198+
Second
199+
</Text>
200+
</>
201+
`);
202+
});
42203
});
43204

44-
test('render with wrapper option', async () => {
45-
const WrapperComponent = ({ children }: { children: React.ReactNode }) => (
46-
<View testID="wrapper">{children}</View>
47-
);
205+
describe('debug', () => {
206+
test('debug outputs formatted component tree', async () => {
207+
const TestComponent = () => (
208+
<View testID="container">
209+
<Text>Debug Test</Text>
210+
</View>
211+
);
212+
213+
await render(<TestComponent />);
48214

49-
await render(<View testID="inner" />, {
50-
wrapper: WrapperComponent,
215+
expect(() => {
216+
screen.debug();
217+
}).not.toThrow();
51218
});
52219

53-
expect(screen.getByTestId('wrapper')).toBeTruthy();
54-
expect(screen.getByTestId('inner')).toBeTruthy();
55-
});
220+
test('debug accepts options with message', async () => {
221+
const TestComponent = () => <Text>Test</Text>;
56222

57-
test('rerender function updates component asynchronously', async () => {
58-
const fn = jest.fn();
59-
await render(<Banana onUpdate={fn} />);
60-
expect(fn).toHaveBeenCalledTimes(0);
223+
await render(<TestComponent />);
61224

62-
await screen.rerender(<Banana onUpdate={fn} />);
63-
expect(fn).toHaveBeenCalledTimes(1);
225+
expect(() => {
226+
screen.debug({ message: 'Custom message' });
227+
}).not.toThrow();
228+
});
64229
});
65230

66-
test('unmount function unmounts component asynchronously', async () => {
67-
const fn = jest.fn();
68-
await render(<Banana onUnmount={fn} />);
231+
describe('result getters', () => {
232+
test('container getter returns renderer container', async () => {
233+
const TestComponent = () => <Text testID="content">Content</Text>;
69234

70-
await screen.unmount();
71-
expect(fn).toHaveBeenCalled();
72-
});
235+
const result = await render(<TestComponent />);
236+
237+
expect(result.container).toMatchInlineSnapshot(`
238+
<>
239+
<Text
240+
testID="content"
241+
>
242+
Content
243+
</Text>
244+
</>
245+
`);
246+
});
73247

74-
test('render accepts RCTText component', async () => {
75-
await render(React.createElement('RCTText', { testID: 'text' }, 'Hello'));
76-
expect(screen.getByTestId('text')).toBeOnTheScreen();
77-
expect(screen.getByText('Hello')).toBeOnTheScreen();
248+
test('root getter works correctly', async () => {
249+
const TestComponent = () => <View testID="test" />;
250+
251+
const result = await render(<TestComponent />);
252+
253+
expect(result.root).toMatchInlineSnapshot(`
254+
<View
255+
testID="test"
256+
/>
257+
`);
258+
});
78259
});
79260

80-
test('render throws when text string is rendered without Text component', async () => {
81-
await expect(render(<View>Hello</View>)).rejects.toThrowErrorMatchingInlineSnapshot(
82-
`"Invariant Violation: Text strings must be rendered within a <Text> component. Detected attempt to render "Hello" string within a <View> component."`,
83-
);
261+
describe('screen integration', () => {
262+
test('render sets screen queries', async () => {
263+
const TestComponent = () => (
264+
<View>
265+
<Text testID="text1">First Text</Text>
266+
<Text testID="text2">Second Text</Text>
267+
</View>
268+
);
269+
270+
await render(<TestComponent />);
271+
272+
expect(screen.getByTestId('text1')).toBeOnTheScreen();
273+
expect(screen.getByTestId('text2')).toBeOnTheScreen();
274+
expect(screen.getByText('First Text')).toBeOnTheScreen();
275+
expect(screen.getByText('Second Text')).toBeOnTheScreen();
276+
});
277+
278+
test('screen queries work after rerender', async () => {
279+
interface TestComponentProps {
280+
label: string;
281+
}
282+
const TestComponent = ({ label }: TestComponentProps) => <Text testID="label">{label}</Text>;
283+
284+
await render(<TestComponent label="Initial" />);
285+
286+
expect(screen.getByText('Initial')).toBeOnTheScreen();
287+
288+
await screen.rerender(<TestComponent label="Updated" />);
289+
290+
expect(screen.getByText('Updated')).toBeOnTheScreen();
291+
});
84292
});

src/render.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ export async function render<T>(element: React.ReactElement<T>, options: RenderO
6565

6666
const toJSON = (): JsonElement | null => {
6767
const json = renderer.container.toJSON();
68-
if (json?.children?.length === 0) {
68+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
69+
if (json?.children!.length === 0) {
6970
return null;
7071
}
7172

@@ -90,6 +91,7 @@ export async function render<T>(element: React.ReactElement<T>, options: RenderO
9091
get root(): HostElement | null {
9192
const firstChild = container.children[0];
9293
if (typeof firstChild === 'string') {
94+
/* istanbul ignore next */
9395
throw new Error(
9496
'Invariant Violation: Root element must be a host element. Detected attempt to render a string within the root element.',
9597
);

0 commit comments

Comments
 (0)