main
js 473 lines 13.6 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 */
9
10 'use strict';
11
12 let React;
13 let ReactFeatureFlags;
14 let ReactDOM;
15 let ReactDOMClient;
16 let Scheduler;
17 let mockDevToolsHook;
18 let allSchedulerTags;
19 let allSchedulerTypes;
20 let onCommitRootShouldYield;
21 let act;
22 let waitFor;
23 let waitForAll;
24 let assertLog;
25
26 describe('updaters', () => {
27 beforeEach(() => {
28 jest.resetModules();
29
30 allSchedulerTags = [];
31 allSchedulerTypes = [];
32
33 onCommitRootShouldYield = true;
34
35 ReactFeatureFlags = require('shared/ReactFeatureFlags');
36 ReactFeatureFlags.enableUpdaterTracking = true;
37
38 mockDevToolsHook = {
39 injectInternals: jest.fn(() => {}),
40 isDevToolsPresent: true,
41 onCommitRoot: jest.fn(fiberRoot => {
42 if (onCommitRootShouldYield) {
43 Scheduler.log('onCommitRoot');
44 }
45 const schedulerTags = [];
46 const schedulerTypes = [];
47 fiberRoot.memoizedUpdaters.forEach(fiber => {
48 schedulerTags.push(fiber.tag);
49 schedulerTypes.push(fiber.elementType);
50 });
51 allSchedulerTags.push(schedulerTags);
52 allSchedulerTypes.push(schedulerTypes);
53 }),
54 onCommitUnmount: jest.fn(() => {}),
55 onPostCommitRoot: jest.fn(() => {}),
56 onScheduleRoot: jest.fn(() => {}),
57
58 // Profiling APIs
59 markCommitStarted: jest.fn(() => {}),
60 markCommitStopped: jest.fn(() => {}),
61 markComponentRenderStarted: jest.fn(() => {}),
62 markComponentRenderStopped: jest.fn(() => {}),
63 markComponentPassiveEffectMountStarted: jest.fn(() => {}),
64 markComponentPassiveEffectMountStopped: jest.fn(() => {}),
65 markComponentPassiveEffectUnmountStarted: jest.fn(() => {}),
66 markComponentPassiveEffectUnmountStopped: jest.fn(() => {}),
67 markComponentLayoutEffectMountStarted: jest.fn(() => {}),
68 markComponentLayoutEffectMountStopped: jest.fn(() => {}),
69 markComponentLayoutEffectUnmountStarted: jest.fn(() => {}),
70 markComponentLayoutEffectUnmountStopped: jest.fn(() => {}),
71 markComponentErrored: jest.fn(() => {}),
72 markComponentSuspended: jest.fn(() => {}),
73 markLayoutEffectsStarted: jest.fn(() => {}),
74 markLayoutEffectsStopped: jest.fn(() => {}),
75 markPassiveEffectsStarted: jest.fn(() => {}),
76 markPassiveEffectsStopped: jest.fn(() => {}),
77 markRenderStarted: jest.fn(() => {}),
78 markRenderYielded: jest.fn(() => {}),
79 markRenderStopped: jest.fn(() => {}),
80 markRenderScheduled: jest.fn(() => {}),
81 markForceUpdateScheduled: jest.fn(() => {}),
82 markStateUpdateScheduled: jest.fn(() => {}),
83 };
84
85 jest.mock(
86 'react-reconciler/src/ReactFiberDevToolsHook',
87 () => mockDevToolsHook,
88 );
89
90 React = require('react');
91 ReactDOM = require('react-dom');
92 ReactDOMClient = require('react-dom/client');
93 Scheduler = require('scheduler');
94
95 act = require('internal-test-utils').act;
96
97 const InternalTestUtils = require('internal-test-utils');
98 waitFor = InternalTestUtils.waitFor;
99 waitForAll = InternalTestUtils.waitForAll;
100 assertLog = InternalTestUtils.assertLog;
101 });
102
103 it('should report the (host) root as the scheduler for root-level render', async () => {
104 const {HostRoot} = require('react-reconciler/src/ReactWorkTags');
105
106 const Parent = () => <Child />;
107 const Child = () => null;
108 const root = ReactDOMClient.createRoot(document.createElement('div'));
109
110 await act(() => {
111 root.render(<Parent />);
112 });
113 expect(allSchedulerTags).toEqual([[HostRoot]]);
114 assertLog(['onCommitRoot']);
115
116 await act(() => {
117 root.render(<Parent />);
118 });
119 expect(allSchedulerTags).toEqual([[HostRoot], [HostRoot]]);
120 assertLog(['onCommitRoot']);
121 });
122
123 it('should report a function component as the scheduler for a hooks update', async () => {
124 let scheduleForA = null;
125 let scheduleForB = null;
126
127 const Parent = () => (
128 <React.Fragment>
129 <SchedulingComponentA />
130 <SchedulingComponentB />
131 </React.Fragment>
132 );
133 const SchedulingComponentA = () => {
134 const [count, setCount] = React.useState(0);
135 scheduleForA = () => setCount(prevCount => prevCount + 1);
136 return <Child count={count} />;
137 };
138 const SchedulingComponentB = () => {
139 const [count, setCount] = React.useState(0);
140 scheduleForB = () => setCount(prevCount => prevCount + 1);
141 return <Child count={count} />;
142 };
143 const Child = () => null;
144
145 const root = ReactDOMClient.createRoot(document.createElement('div'));
146 await act(() => {
147 root.render(<Parent />);
148 });
149 expect(scheduleForA).not.toBeNull();
150 expect(scheduleForB).not.toBeNull();
151 expect(allSchedulerTypes).toEqual([[null]]);
152 assertLog(['onCommitRoot']);
153
154 await act(() => {
155 scheduleForA();
156 });
157 expect(allSchedulerTypes).toEqual([[null], [SchedulingComponentA]]);
158 assertLog(['onCommitRoot']);
159 await act(() => {
160 scheduleForB();
161 });
162 expect(allSchedulerTypes).toEqual([
163 [null],
164 [SchedulingComponentA],
165 [SchedulingComponentB],
166 ]);
167 assertLog(['onCommitRoot']);
168 });
169
170 it('should report a class component as the scheduler for a setState update', async () => {
171 const Parent = () => <SchedulingComponent />;
172 class SchedulingComponent extends React.Component {
173 state = {};
174 render() {
175 instance = this;
176 return <Child />;
177 }
178 }
179 const Child = () => null;
180 let instance;
181 const root = ReactDOMClient.createRoot(document.createElement('div'));
182 await act(() => {
183 root.render(<Parent />);
184 });
185 expect(allSchedulerTypes).toEqual([[null]]);
186 assertLog(['onCommitRoot']);
187 expect(instance).not.toBeNull();
188 await act(() => {
189 instance.setState({});
190 });
191 expect(allSchedulerTypes).toEqual([[null], [SchedulingComponent]]);
192 });
193
194 it('should cover cascading updates', async () => {
195 let triggerActiveCascade = null;
196 let triggerPassiveCascade = null;
197
198 const Parent = () => <SchedulingComponent />;
199 const SchedulingComponent = () => {
200 const [cascade, setCascade] = React.useState(null);
201 triggerActiveCascade = () => setCascade('active');
202 triggerPassiveCascade = () => setCascade('passive');
203 return <CascadingChild cascade={cascade} />;
204 };
205 const CascadingChild = ({cascade}) => {
206 const [count, setCount] = React.useState(0);
207 Scheduler.log(`CascadingChild ${count}`);
208 React.useLayoutEffect(() => {
209 if (cascade === 'active') {
210 setCount(prevCount => prevCount + 1);
211 }
212 return () => {};
213 }, [cascade]);
214 React.useEffect(() => {
215 if (cascade === 'passive') {
216 setCount(prevCount => prevCount + 1);
217 }
218 return () => {};
219 }, [cascade]);
220 return count;
221 };
222
223 const root = ReactDOMClient.createRoot(document.createElement('div'));
224 await act(async () => {
225 root.render(<Parent />);
226 await waitFor(['CascadingChild 0', 'onCommitRoot']);
227 });
228 expect(triggerActiveCascade).not.toBeNull();
229 expect(triggerPassiveCascade).not.toBeNull();
230 expect(allSchedulerTypes).toEqual([[null]]);
231
232 await act(async () => {
233 triggerActiveCascade();
234 await waitFor([
235 'CascadingChild 0',
236 'onCommitRoot',
237 'CascadingChild 1',
238 'onCommitRoot',
239 ]);
240 });
241 expect(allSchedulerTypes).toEqual([
242 [null],
243 [SchedulingComponent],
244 [CascadingChild],
245 ]);
246
247 await act(async () => {
248 triggerPassiveCascade();
249 await waitFor([
250 'CascadingChild 1',
251 'onCommitRoot',
252 'CascadingChild 2',
253 'onCommitRoot',
254 ]);
255 });
256 expect(allSchedulerTypes).toEqual([
257 [null],
258 [SchedulingComponent],
259 [CascadingChild],
260 [SchedulingComponent],
261 [CascadingChild],
262 ]);
263
264 // Verify no outstanding flushes
265 await waitForAll([]);
266 });
267
268 // This test should be convertable to createRoot but the allScheduledTypes assertions are no longer the same
269 // So I'm leaving it in legacy mode for now and just disabling if legacy mode is turned off
270 // @gate !disableLegacyMode
271 it('should cover suspense pings', async () => {
272 let data = null;
273 let resolver = null;
274 let promise = null;
275 const fakeCacheRead = () => {
276 if (data === null) {
277 promise = new Promise(resolve => {
278 resolver = resolvedData => {
279 data = resolvedData;
280 resolve(resolvedData);
281 };
282 });
283 throw promise;
284 } else {
285 return data;
286 }
287 };
288 const Parent = () => (
289 <React.Suspense fallback={<Fallback />}>
290 <Suspender />
291 </React.Suspense>
292 );
293 const Fallback = () => null;
294 let setShouldSuspend = null;
295 const Suspender = ({suspend}) => {
296 const tuple = React.useState(false);
297 setShouldSuspend = tuple[1];
298 if (tuple[0] === true) {
299 return fakeCacheRead();
300 } else {
301 return null;
302 }
303 };
304
305 await act(() => {
306 ReactDOM.render(<Parent />, document.createElement('div'));
307 assertLog(['onCommitRoot']);
308 });
309 expect(setShouldSuspend).not.toBeNull();
310 expect(allSchedulerTypes).toEqual([[null]]);
311
312 await act(() => {
313 setShouldSuspend(true);
314 });
315 assertLog(['onCommitRoot']);
316 expect(allSchedulerTypes).toEqual([[null], [Suspender]]);
317
318 expect(resolver).not.toBeNull();
319 await act(() => {
320 resolver('abc');
321 return promise;
322 });
323 assertLog(['onCommitRoot']);
324 expect(allSchedulerTypes).toEqual([[null], [Suspender], [Suspender]]);
325
326 // Verify no outstanding flushes
327 await waitForAll([]);
328 });
329
330 it('should cover error handling', async () => {
331 let triggerError = null;
332
333 const Parent = () => {
334 const [shouldError, setShouldError] = React.useState(false);
335 triggerError = () => setShouldError(true);
336 return shouldError ? (
337 <ErrorBoundary>
338 <BrokenRender />
339 </ErrorBoundary>
340 ) : (
341 <ErrorBoundary>
342 <Yield value="initial" />
343 </ErrorBoundary>
344 );
345 };
346 class ErrorBoundary extends React.Component {
347 state = {error: null};
348 componentDidCatch(error) {
349 this.setState({error});
350 }
351 render() {
352 if (this.state.error) {
353 return <Yield value="error" />;
354 }
355 return this.props.children;
356 }
357 }
358 const Yield = ({value}) => {
359 Scheduler.log(value);
360 return null;
361 };
362 const BrokenRender = () => {
363 throw new Error('Hello');
364 };
365
366 const root = ReactDOMClient.createRoot(document.createElement('div'));
367 await act(() => {
368 root.render(<Parent shouldError={false} />);
369 });
370 assertLog(['initial', 'onCommitRoot']);
371 expect(triggerError).not.toBeNull();
372
373 allSchedulerTypes.splice(0);
374 onCommitRootShouldYield = true;
375
376 await act(() => {
377 triggerError();
378 });
379 assertLog(['onCommitRoot', 'error', 'onCommitRoot']);
380 expect(allSchedulerTypes).toEqual([[Parent], [ErrorBoundary]]);
381
382 // Verify no outstanding flushes
383 await waitForAll([]);
384 });
385
386 it('should distinguish between updaters in the case of interleaved work', async () => {
387 const {
388 FunctionComponent,
389 HostRoot,
390 } = require('react-reconciler/src/ReactWorkTags');
391
392 let triggerLowPriorityUpdate = null;
393 let triggerSyncPriorityUpdate = null;
394
395 const SyncPriorityUpdater = () => {
396 const [count, setCount] = React.useState(0);
397 triggerSyncPriorityUpdate = () => setCount(prevCount => prevCount + 1);
398 Scheduler.log(`SyncPriorityUpdater ${count}`);
399 return <Yield value={`HighPriority ${count}`} />;
400 };
401 const LowPriorityUpdater = () => {
402 const [count, setCount] = React.useState(0);
403 triggerLowPriorityUpdate = () => {
404 React.startTransition(() => {
405 setCount(prevCount => prevCount + 1);
406 });
407 };
408 Scheduler.log(`LowPriorityUpdater ${count}`);
409 return <Yield value={`LowPriority ${count}`} />;
410 };
411 const Yield = ({value}) => {
412 Scheduler.log(`Yield ${value}`);
413 return null;
414 };
415
416 const root = ReactDOMClient.createRoot(document.createElement('div'));
417 root.render(
418 <React.Fragment>
419 <SyncPriorityUpdater />
420 <LowPriorityUpdater />
421 </React.Fragment>,
422 );
423
424 // Render everything initially.
425 await waitForAll([
426 'SyncPriorityUpdater 0',
427 'Yield HighPriority 0',
428 'LowPriorityUpdater 0',
429 'Yield LowPriority 0',
430 'onCommitRoot',
431 ]);
432 expect(triggerLowPriorityUpdate).not.toBeNull();
433 expect(triggerSyncPriorityUpdate).not.toBeNull();
434 expect(allSchedulerTags).toEqual([[HostRoot]]);
435
436 // Render a partial update, but don't finish.
437 await act(async () => {
438 triggerLowPriorityUpdate();
439 await waitFor(['LowPriorityUpdater 1']);
440 expect(allSchedulerTags).toEqual([[HostRoot]]);
441
442 // Interrupt with higher priority work.
443 ReactDOM.flushSync(triggerSyncPriorityUpdate);
444 assertLog([
445 'SyncPriorityUpdater 1',
446 'Yield HighPriority 1',
447 'onCommitRoot',
448 ]);
449 expect(allSchedulerTypes).toEqual([[null], [SyncPriorityUpdater]]);
450
451 // Finish the initial partial update
452 triggerLowPriorityUpdate();
453 await waitForAll([
454 'LowPriorityUpdater 2',
455 'Yield LowPriority 2',
456 'onCommitRoot',
457 ]);
458 });
459 expect(allSchedulerTags).toEqual([
460 [HostRoot],
461 [FunctionComponent],
462 [FunctionComponent],
463 ]);
464 expect(allSchedulerTypes).toEqual([
465 [null],
466 [SyncPriorityUpdater],
467 [LowPriorityUpdater],
468 ]);
469
470 // Verify no outstanding flushes
471 await waitForAll([]);
472 });
473 });