main
js 330 lines 9.86 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 * @jest-environment node
9 */
10
11 'use strict';
12
13 describe('React hooks DevTools integration', () => {
14 let React;
15 let ReactDebugTools;
16 let ReactTestRenderer;
17 let act;
18 let overrideHookState;
19 let scheduleUpdate;
20 let scheduleRetry;
21 let setSuspenseHandler;
22 let waitForAll;
23
24 global.IS_REACT_ACT_ENVIRONMENT = true;
25
26 beforeEach(() => {
27 global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
28 inject: injected => {
29 overrideHookState = injected.overrideHookState;
30 scheduleUpdate = injected.scheduleUpdate;
31 scheduleRetry = injected.scheduleRetry;
32 setSuspenseHandler = injected.setSuspenseHandler;
33 },
34 supportsFiber: true,
35 onCommitFiberRoot: () => {},
36 onCommitFiberUnmount: () => {},
37 };
38
39 jest.resetModules();
40
41 React = require('react');
42 ReactDebugTools = require('react-debug-tools');
43 ReactTestRenderer = require('react-test-renderer');
44
45 const InternalTestUtils = require('internal-test-utils');
46 waitForAll = InternalTestUtils.waitForAll;
47
48 act = require('internal-test-utils').act;
49 });
50
51 it('should support editing useState hooks', async () => {
52 let setCountFn;
53
54 function MyComponent() {
55 const [count, setCount] = React.useState(0);
56 setCountFn = setCount;
57 return <div>count:{count}</div>;
58 }
59
60 let renderer;
61 await act(() => {
62 renderer = ReactTestRenderer.create(<MyComponent />, {
63 unstable_isConcurrent: true,
64 });
65 });
66 expect(renderer.toJSON()).toEqual({
67 type: 'div',
68 props: {},
69 children: ['count:', '0'],
70 });
71
72 const fiber = renderer.root.findByType(MyComponent)._currentFiber();
73 const tree = ReactDebugTools.inspectHooksOfFiber(fiber);
74 const stateHook = tree[0];
75 expect(stateHook.isStateEditable).toBe(true);
76
77 if (__DEV__) {
78 await act(() => overrideHookState(fiber, stateHook.id, [], 10));
79 expect(renderer.toJSON()).toEqual({
80 type: 'div',
81 props: {},
82 children: ['count:', '10'],
83 });
84
85 await act(() => setCountFn(count => count + 1));
86 expect(renderer.toJSON()).toEqual({
87 type: 'div',
88 props: {},
89 children: ['count:', '11'],
90 });
91 }
92 });
93
94 it('should support editable useReducer hooks', async () => {
95 const initialData = {foo: 'abc', bar: 123};
96
97 function reducer(state, action) {
98 switch (action.type) {
99 case 'swap':
100 return {foo: state.bar, bar: state.foo};
101 default:
102 throw new Error();
103 }
104 }
105
106 let dispatchFn;
107 function MyComponent() {
108 const [state, dispatch] = React.useReducer(reducer, initialData);
109 dispatchFn = dispatch;
110 return (
111 <div>
112 foo:{state.foo}, bar:{state.bar}
113 </div>
114 );
115 }
116
117 let renderer;
118 await act(() => {
119 renderer = ReactTestRenderer.create(<MyComponent />, {
120 unstable_isConcurrent: true,
121 });
122 });
123 expect(renderer.toJSON()).toEqual({
124 type: 'div',
125 props: {},
126 children: ['foo:', 'abc', ', bar:', '123'],
127 });
128
129 const fiber = renderer.root.findByType(MyComponent)._currentFiber();
130 const tree = ReactDebugTools.inspectHooksOfFiber(fiber);
131 const reducerHook = tree[0];
132 expect(reducerHook.isStateEditable).toBe(true);
133
134 if (__DEV__) {
135 await act(() => overrideHookState(fiber, reducerHook.id, ['foo'], 'def'));
136 expect(renderer.toJSON()).toEqual({
137 type: 'div',
138 props: {},
139 children: ['foo:', 'def', ', bar:', '123'],
140 });
141
142 await act(() => dispatchFn({type: 'swap'}));
143 expect(renderer.toJSON()).toEqual({
144 type: 'div',
145 props: {},
146 children: ['foo:', '123', ', bar:', 'def'],
147 });
148 }
149 });
150
151 // This test case is based on an open source bug report:
152 // https://github.com/facebookincubator/redux-react-hook/issues/34#issuecomment-466693787
153 it('should handle interleaved stateful hooks (e.g. useState) and non-stateful hooks (e.g. useContext)', async () => {
154 const MyContext = React.createContext(1);
155
156 let setStateFn;
157 function useCustomHook() {
158 const context = React.useContext(MyContext);
159 const [state, setState] = React.useState({count: context});
160 React.useDebugValue(state.count);
161 setStateFn = setState;
162 return state.count;
163 }
164
165 function MyComponent() {
166 const count = useCustomHook();
167 return <div>count:{count}</div>;
168 }
169
170 let renderer;
171 await act(() => {
172 renderer = ReactTestRenderer.create(<MyComponent />, {
173 unstable_isConcurrent: true,
174 });
175 });
176 expect(renderer.toJSON()).toEqual({
177 type: 'div',
178 props: {},
179 children: ['count:', '1'],
180 });
181
182 const fiber = renderer.root.findByType(MyComponent)._currentFiber();
183 const tree = ReactDebugTools.inspectHooksOfFiber(fiber);
184 const stateHook = tree[0].subHooks[1];
185 expect(stateHook.isStateEditable).toBe(true);
186
187 if (__DEV__) {
188 await act(() => overrideHookState(fiber, stateHook.id, ['count'], 10));
189 expect(renderer.toJSON()).toEqual({
190 type: 'div',
191 props: {},
192 children: ['count:', '10'],
193 });
194 await act(() => setStateFn(state => ({count: state.count + 1})));
195 expect(renderer.toJSON()).toEqual({
196 type: 'div',
197 props: {},
198 children: ['count:', '11'],
199 });
200 }
201 });
202
203 it('should support overriding suspense in legacy mode', async () => {
204 if (__DEV__) {
205 // Lock the first render
206 setSuspenseHandler(() => true);
207 }
208
209 function MyComponent() {
210 return 'Done';
211 }
212 let renderer;
213 await act(() => {
214 renderer = ReactTestRenderer.create(
215 <div>
216 <React.Suspense fallback={'Loading'}>
217 <MyComponent />
218 </React.Suspense>
219 </div>,
220 {unstable_isConcurrent: true},
221 );
222 });
223 const fiber = renderer.root._currentFiber().child;
224 if (__DEV__) {
225 // First render was locked
226 expect(renderer.toJSON().children).toEqual(['Loading']);
227 await act(() => scheduleUpdate(fiber)); // Re-render
228 expect(renderer.toJSON().children).toEqual(['Loading']);
229
230 // Release the lock
231 setSuspenseHandler(() => false);
232 await act(() => scheduleUpdate(fiber)); // Re-render
233 expect(renderer.toJSON().children).toEqual(['Done']);
234 await act(() => scheduleUpdate(fiber)); // Re-render
235 expect(renderer.toJSON().children).toEqual(['Done']);
236
237 // Lock again
238 setSuspenseHandler(() => true);
239 await act(() => scheduleUpdate(fiber)); // Re-render
240 expect(renderer.toJSON().children).toEqual(['Loading']);
241
242 // Release the lock again
243 setSuspenseHandler(() => false);
244 await act(() => scheduleUpdate(fiber)); // Re-render
245 expect(renderer.toJSON().children).toEqual(['Done']);
246
247 // Ensure it checks specific fibers.
248 setSuspenseHandler(f => f === fiber || f === fiber.alternate);
249 await act(() => scheduleUpdate(fiber)); // Re-render
250 expect(renderer.toJSON().children).toEqual(['Loading']);
251 setSuspenseHandler(f => f !== fiber && f !== fiber.alternate);
252 await act(() => scheduleUpdate(fiber)); // Re-render
253 expect(renderer.toJSON().children).toEqual(['Done']);
254 } else {
255 expect(renderer.toJSON().children).toEqual(['Done']);
256 }
257 });
258
259 it('should support overriding suspense in concurrent mode', async () => {
260 if (__DEV__) {
261 // Lock the first render
262 setSuspenseHandler(() => true);
263 }
264
265 function MyComponent() {
266 return 'Done';
267 }
268
269 const renderer = await act(() =>
270 ReactTestRenderer.create(
271 <div>
272 <React.Suspense fallback={'Loading'}>
273 <MyComponent />
274 </React.Suspense>
275 </div>,
276 {unstable_isConcurrent: true},
277 ),
278 );
279
280 await waitForAll([]);
281 // Ensure we timeout any suspense time.
282 jest.advanceTimersByTime(1000);
283 const fiber = renderer.root._currentFiber().child;
284 if (__DEV__) {
285 // First render was locked
286 expect(renderer.toJSON().children).toEqual(['Loading']);
287 await act(() => scheduleUpdate(fiber)); // Re-render
288 expect(renderer.toJSON().children).toEqual(['Loading']);
289
290 // Release the lock
291 setSuspenseHandler(() => false);
292 await act(() => scheduleUpdate(fiber)); // Re-render
293 expect(renderer.toJSON().children).toEqual(['Done']);
294 await act(() => scheduleUpdate(fiber)); // Re-render
295 expect(renderer.toJSON().children).toEqual(['Done']);
296
297 // Lock again
298 setSuspenseHandler(() => true);
299 await act(() => scheduleUpdate(fiber)); // Re-render
300 expect(renderer.toJSON().children).toEqual(['Loading']);
301
302 // Release the lock again
303 setSuspenseHandler(() => false);
304 await act(() => scheduleUpdate(fiber)); // Re-render
305 expect(renderer.toJSON().children).toEqual(['Done']);
306
307 // Ensure it checks specific fibers.
308 setSuspenseHandler(f => f === fiber || f === fiber.alternate);
309 await act(() => scheduleUpdate(fiber)); // Re-render
310 expect(renderer.toJSON().children).toEqual(['Loading']);
311 setSuspenseHandler(f => f !== fiber && f !== fiber.alternate);
312 await act(() => scheduleUpdate(fiber)); // Re-render
313 expect(renderer.toJSON().children).toEqual(['Done']);
314 } else {
315 expect(renderer.toJSON().children).toEqual(['Done']);
316 }
317
318 if (scheduleRetry) {
319 // Lock again, synchronously
320 setSuspenseHandler(() => true);
321 await act(() => scheduleUpdate(fiber)); // Re-render
322 expect(renderer.toJSON().children).toEqual(['Loading']);
323
324 // Release the lock again but this time using retry lane
325 setSuspenseHandler(() => false);
326 await act(() => scheduleRetry(fiber)); // Re-render
327 expect(renderer.toJSON().children).toEqual(['Done']);
328 }
329 });
330 });