Skip to content

Commit 32a7bc3

Browse files
committed
tests
1 parent 79482be commit 32a7bc3

File tree

3 files changed

+280
-3
lines changed

3 files changed

+280
-3
lines changed

src/__tests__/render-async.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ test('renderAsync with wrapper option', async () => {
5454
expect(screen.getByTestId('inner')).toBeTruthy();
5555
});
5656

57-
test('renderAsync supports concurrent rendering option', async () => {
58-
await renderAsync(<View testID="test" />, { concurrentRoot: true });
57+
test('renderAsync supports legacy rendering option', async () => {
58+
await renderAsync(<View testID="test" />, { concurrentRoot: false });
5959
expect(screen.root).toBeOnTheScreen();
6060
});
6161

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import type { ReactNode } from 'react';
2+
import * as React from 'react';
3+
4+
import { act, renderHookAsync } from '..';
5+
import { excludeConsoleMessage } from '../test-utils/console';
6+
7+
const testGateReact19 = React.version.startsWith('19.') ? test : test.skip;
8+
9+
// eslint-disable-next-line no-console
10+
const originalConsoleError = console.error;
11+
afterEach(() => {
12+
// eslint-disable-next-line no-console
13+
console.error = originalConsoleError;
14+
});
15+
16+
test('renderHookAsync renders hook asynchronously', async () => {
17+
const { result } = await renderHookAsync(() => {
18+
const [state, setState] = React.useState(1);
19+
20+
React.useEffect(() => {
21+
setState(2);
22+
}, []);
23+
24+
return state;
25+
});
26+
27+
expect(result.current).toEqual(2);
28+
});
29+
30+
test('renderHookAsync with wrapper option', async () => {
31+
const Context = React.createContext('default');
32+
33+
function useTestHook() {
34+
return React.useContext(Context);
35+
}
36+
37+
function Wrapper({ children }: { children: ReactNode }) {
38+
return <Context.Provider value="provided">{children}</Context.Provider>;
39+
}
40+
41+
const { result } = await renderHookAsync(useTestHook, { wrapper: Wrapper });
42+
expect(result.current).toEqual('provided');
43+
});
44+
45+
test('renderHookAsync supports legacy rendering option', async () => {
46+
function useTestHook() {
47+
return React.useState(42)[0];
48+
}
49+
50+
const { result } = await renderHookAsync(useTestHook, { concurrentRoot: false });
51+
expect(result.current).toEqual(42);
52+
});
53+
54+
test('rerender function updates hook asynchronously', async () => {
55+
function useTestHook(props: { value: number }) {
56+
const [state, setState] = React.useState(props.value);
57+
58+
React.useEffect(() => {
59+
setState(props.value * 2);
60+
}, [props.value]);
61+
62+
return state;
63+
}
64+
65+
const { result, rerender } = await renderHookAsync(useTestHook, { initialProps: { value: 5 } });
66+
expect(result.current).toEqual(10);
67+
68+
await rerender({ value: 10 });
69+
expect(result.current).toEqual(20);
70+
});
71+
72+
test('unmount function unmounts hook asynchronously', async () => {
73+
let cleanupCalled = false;
74+
75+
function useTestHook() {
76+
React.useEffect(() => {
77+
return () => {
78+
cleanupCalled = true;
79+
};
80+
}, []);
81+
82+
return 'test';
83+
}
84+
85+
const { unmount } = await renderHookAsync(useTestHook);
86+
expect(cleanupCalled).toBe(false);
87+
88+
await unmount();
89+
expect(cleanupCalled).toBe(true);
90+
});
91+
92+
test('handles hook with state updates during effects', async () => {
93+
function useTestHook() {
94+
const [count, setCount] = React.useState(0);
95+
96+
React.useEffect(() => {
97+
setCount((prev) => prev + 1);
98+
}, []);
99+
100+
return count;
101+
}
102+
103+
const { result } = await renderHookAsync(useTestHook);
104+
expect(result.current).toBe(1);
105+
});
106+
107+
test('handles multiple state updates in effects', async () => {
108+
function useTestHook() {
109+
const [first, setFirst] = React.useState(1);
110+
const [second, setSecond] = React.useState(2);
111+
112+
React.useEffect(() => {
113+
setFirst(10);
114+
setSecond(20);
115+
}, []);
116+
117+
return { first, second };
118+
}
119+
120+
const { result } = await renderHookAsync(useTestHook);
121+
expect(result.current).toEqual({ first: 10, second: 20 });
122+
});
123+
124+
testGateReact19('handles hook with suspense', async () => {
125+
function useSuspendingHook(promise: Promise<string>) {
126+
return React.use(promise);
127+
}
128+
129+
let resolvePromise: (value: string) => void;
130+
const promise = new Promise<string>((resolve) => {
131+
resolvePromise = resolve;
132+
});
133+
134+
const { result } = await renderHookAsync(useSuspendingHook, {
135+
initialProps: promise,
136+
wrapper: ({ children }) => <React.Suspense fallback="loading">{children}</React.Suspense>,
137+
});
138+
139+
// Initially suspended, result should not be available
140+
expect(result.current).toBeNull();
141+
142+
// eslint-disable-next-line require-await
143+
await act(async () => resolvePromise('resolved'));
144+
expect(result.current).toBe('resolved');
145+
});
146+
147+
class ErrorBoundary extends React.Component<
148+
{ children: React.ReactNode; fallback: string },
149+
{ hasError: boolean }
150+
> {
151+
constructor(props: { children: React.ReactNode; fallback: string }) {
152+
super(props);
153+
this.state = { hasError: false };
154+
}
155+
156+
static getDerivedStateFromError() {
157+
return { hasError: true };
158+
}
159+
160+
render() {
161+
return this.state.hasError ? this.props.fallback : this.props.children;
162+
}
163+
}
164+
165+
testGateReact19('handles hook suspense with error boundary', async () => {
166+
const ERROR_MESSAGE = 'Hook Promise Rejected In Test';
167+
// eslint-disable-next-line no-console
168+
console.error = excludeConsoleMessage(console.error, ERROR_MESSAGE);
169+
170+
function useSuspendingHook(promise: Promise<string>) {
171+
return React.use(promise);
172+
}
173+
174+
let rejectPromise: (error: Error) => void;
175+
const promise = new Promise<string>((_resolve, reject) => {
176+
rejectPromise = reject;
177+
});
178+
179+
const { result } = await renderHookAsync(useSuspendingHook, {
180+
initialProps: promise,
181+
wrapper: ({ children }) => (
182+
<ErrorBoundary fallback="error-fallback">
183+
<React.Suspense fallback="loading">{children}</React.Suspense>
184+
</ErrorBoundary>
185+
),
186+
});
187+
188+
// Initially suspended
189+
expect(result.current).toBeNull();
190+
191+
// eslint-disable-next-line require-await
192+
await act(async () => rejectPromise(new Error(ERROR_MESSAGE)));
193+
194+
// After error, result should still be null (error boundary caught it)
195+
expect(result.current).toBeNull();
196+
});
197+
198+
test('handles custom hooks with complex logic', async () => {
199+
function useCounter(initialValue: number) {
200+
const [count, setCount] = React.useState(initialValue);
201+
202+
const increment = React.useCallback(() => {
203+
setCount((prev) => prev + 1);
204+
}, []);
205+
206+
const decrement = React.useCallback(() => {
207+
setCount((prev) => prev - 1);
208+
}, []);
209+
210+
const reset = React.useCallback(() => {
211+
setCount(initialValue);
212+
}, [initialValue]);
213+
214+
return { count, increment, decrement, reset };
215+
}
216+
217+
const { result, rerender } = await renderHookAsync(useCounter, { initialProps: 5 });
218+
219+
expect(result.current.count).toBe(5);
220+
221+
// Test increment
222+
result.current.increment();
223+
await rerender(5);
224+
expect(result.current.count).toBe(6);
225+
226+
// Test decrement
227+
result.current.decrement();
228+
await rerender(5);
229+
expect(result.current.count).toBe(5);
230+
231+
// Test reset
232+
result.current.increment();
233+
result.current.increment();
234+
await rerender(5);
235+
expect(result.current.count).toBe(7);
236+
237+
result.current.reset();
238+
await rerender(5);
239+
expect(result.current.count).toBe(5);
240+
});
241+
242+
test('handles hook with cleanup and re-initialization', async () => {
243+
let effectCount = 0;
244+
let cleanupCount = 0;
245+
246+
function useTestHook(props: { key: string }) {
247+
const [value, setValue] = React.useState(props.key);
248+
249+
React.useEffect(() => {
250+
effectCount++;
251+
setValue(`${props.key}-effect`);
252+
253+
return () => {
254+
cleanupCount++;
255+
};
256+
}, [props.key]);
257+
258+
return value;
259+
}
260+
261+
const { result, rerender, unmount } = await renderHookAsync(useTestHook, {
262+
initialProps: { key: 'initial' },
263+
});
264+
265+
expect(result.current).toBe('initial-effect');
266+
expect(effectCount).toBe(1);
267+
expect(cleanupCount).toBe(0);
268+
269+
await rerender({ key: 'updated' });
270+
expect(result.current).toBe('updated-effect');
271+
expect(effectCount).toBe(2);
272+
expect(cleanupCount).toBe(1);
273+
274+
await unmount();
275+
expect(effectCount).toBe(2);
276+
expect(cleanupCount).toBe(2);
277+
});

src/pure.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export { within, getQueriesForElement } from './within';
1010
export { configure, resetToDefaults } from './config';
1111
export { isHiddenFromAccessibility, isInaccessible } from './helpers/accessibility';
1212
export { getDefaultNormalizer } from './matches';
13-
export { renderHook } from './render-hook';
13+
export { renderHook, renderHookAsync } from './render-hook';
1414
export { screen } from './screen';
1515
export { userEvent } from './user-event';
1616

0 commit comments

Comments
 (0)