@samitouri / QOS-React-2 / commits / 9a52cc8bcd

Convert ReactLazy-test to waitFor pattern (#26304)

I'm in the process of codemodding our test suite to the waitFor pattern. See #26285 for full context. This module required a lot of manual changes so I'm doing it as its own PR. The reason is that most of the tests involved simulating an async import by wrapping them in `Promise.resolve()`, which means they would immediately resolve the next time the microtask queue was flushed. I rewrote the tests to resolve the simulated import explicitly. While converting these tests, I also realized that the `waitFor` helpers weren't properly waiting for the entire microtask queue to recursively finish — if a microtask schedules another microtask, the subsequent one wouldn't fire until after `waitFor` had resolved. To fix this, I used the same strategy as `act` — wait for a real task to finish before proceeding, such as a message event.

Andrew Clark committed Mar 4, 2023 at 11:44 UTC 9a52cc8bcd580238f4a8b12036e698823847fd65
6 files changed +305 -262
packages/internal-test-utils/ReactInternalTestUtils.js
+23 -10
@@ -10,6 +10,7 @@
10 import * as SchedulerMock from 'scheduler/unstable_mock';
11 import {diff} from 'jest-diff';
12 import {equals} from '@jest/expect-utils';
13 +import enqueueTask from './enqueueTask';
14
15 function assertYieldsWereCleared(Scheduler) {
16 const actualYields = Scheduler.unstable_clearYields();
@@ -22,6 +23,12 @@ function assertYieldsWereCleared(Scheduler) {
23 }
24 }
25
26 +async function waitForMicrotasks() {
27 + return new Promise(resolve => {
28 + enqueueTask(() => resolve());
29 + });
30 +}
31 +
32 export async function waitFor(expectedLog) {
33 assertYieldsWereCleared(SchedulerMock);
34
@@ -33,7 +40,7 @@ export async function waitFor(expectedLog) {
40 const actualLog = [];
41 do {
42 // Wait until end of current task/microtask.
36 - await null;
43 + await waitForMicrotasks();
44 if (SchedulerMock.unstable_hasPendingWork()) {
45 SchedulerMock.unstable_flushNumberOfYields(
46 expectedLog.length - actualLog.length,
@@ -44,7 +51,7 @@ export async function waitFor(expectedLog) {
51 } else {
52 // Once we've reached the expected sequence, wait one more microtask to
53 // flush any remaining synchronous work.
47 - await null;
54 + await waitForMicrotasks();
55 actualLog.push(...SchedulerMock.unstable_clearYields());
56 break;
57 }
@@ -72,11 +79,11 @@ export async function waitForAll(expectedLog) {
79 // Create the error object before doing any async work, to get a better
80 // stack trace.
81 const error = new Error();
75 - Error.captureStackTrace(error, waitFor);
82 + Error.captureStackTrace(error, waitForAll);
83
84 do {
85 // Wait until end of current task/microtask.
79 - await null;
86 + await waitForMicrotasks();
87 if (!SchedulerMock.unstable_hasPendingWork()) {
88 // There's no pending work, even after a microtask. Stop flushing.
89 break;
@@ -103,11 +110,11 @@ export async function waitForThrow(expectedError: mixed) {
110 // Create the error object before doing any async work, to get a better
111 // stack trace.
112 const error = new Error();
106 - Error.captureStackTrace(error, waitFor);
113 + Error.captureStackTrace(error, waitForThrow);
114
115 do {
116 // Wait until end of current task/microtask.
110 - await null;
117 + await waitForMicrotasks();
118 if (!SchedulerMock.unstable_hasPendingWork()) {
119 // There's no pending work, even after a microtask. Stop flushing.
120 error.message = 'Expected something to throw, but nothing did.';
@@ -119,7 +126,13 @@ export async function waitForThrow(expectedError: mixed) {
126 if (equals(x, expectedError)) {
127 return;
128 }
122 - if (typeof x === 'object' && x !== null && x.message === expectedError) {
129 + if (
130 + typeof expectedError === 'string' &&
131 + typeof x === 'object' &&
132 + x !== null &&
133 + typeof x.message === 'string' &&
134 + x.message.includes(expectedError)
135 + ) {
136 return;
137 }
138 error.message = `
@@ -142,15 +155,15 @@ export async function waitForPaint(expectedLog) {
155 // Create the error object before doing any async work, to get a better
156 // stack trace.
157 const error = new Error();
145 - Error.captureStackTrace(error, waitFor);
158 + Error.captureStackTrace(error, waitForPaint);
159
160 // Wait until end of current task/microtask.
148 - await null;
161 + await waitForMicrotasks();
162 if (SchedulerMock.unstable_hasPendingWork()) {
163 // Flush until React yields.
164 SchedulerMock.unstable_flushUntilNextPaint();
165 // Wait one more microtask to flush any remaining synchronous work.
153 - await null;
166 + await waitForMicrotasks();
167 }
168
169 const actualLog = SchedulerMock.unstable_clearYields();
packages/internal-test-utils/enqueueTask.js new
+50
@@ -0,0 +1,50 @@
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 + * @flow
8 + */
9 +
10 +let didWarnAboutMessageChannel = false;
11 +let enqueueTaskImpl = null;
12 +
13 +// Same as shared/enqeuueTask, but while that one used by the public
14 +// implementation of `act`, this is only used by our internal testing helpers.
15 +export default function enqueueTask(task: () => void): void {
16 + if (enqueueTaskImpl === null) {
17 + try {
18 + // read require off the module object to get around the bundlers.
19 + // we don't want them to detect a require and bundle a Node polyfill.
20 + const requireString = ('require' + Math.random()).slice(0, 7);
21 + const nodeRequire = module && module[requireString];
22 + // assuming we're in node, let's try to get node's
23 + // version of setImmediate, bypassing fake timers if any.
24 + enqueueTaskImpl = nodeRequire.call(module, 'timers').setImmediate;
25 + } catch (_err) {
26 + // we're in a browser
27 + // we can't use regular timers because they may still be faked
28 + // so we try MessageChannel+postMessage instead
29 + enqueueTaskImpl = function (callback: () => void) {
30 + if (__DEV__) {
31 + if (didWarnAboutMessageChannel === false) {
32 + didWarnAboutMessageChannel = true;
33 + if (typeof MessageChannel === 'undefined') {
34 + console['error'](
35 + 'This browser does not have a MessageChannel implementation, ' +
36 + 'so enqueuing tasks via await act(async () => ...) will fail. ' +
37 + 'Please file an issue at https://github.com/facebook/react/issues ' +
38 + 'if you encounter this warning.',
39 + );
40 + }
41 + }
42 + }
43 + const channel = new MessageChannel();
44 + channel.port1.onmessage = callback;
45 + channel.port2.postMessage(undefined);
46 + };
47 + }
48 + }
49 + return enqueueTaskImpl(task);
50 +}
packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js
+1 -1
@@ -183,7 +183,7 @@ describe('ReactCache', () => {
183 );
184
185 if (__DEV__) {
186 - expect(async () => {
186 + await expect(async () => {
187 await waitForAll(['App', 'Loading...']);
188 }).toErrorDev([
189 'Invalid key type. Expected a string, number, symbol, or ' +
packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js
+2 -2
@@ -93,7 +93,7 @@ describe('ReactIncrementalScheduling', () => {
93 expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
94 });
95
96 - it('works on deferred roots in the order they were scheduled', () => {
96 + it('works on deferred roots in the order they were scheduled', async () => {
97 const {useEffect} = React;
98 function Text({text}) {
99 useEffect(() => {
@@ -114,7 +114,7 @@ describe('ReactIncrementalScheduling', () => {
114 expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:1');
115
116 // Schedule deferred work in the reverse order
117 - act(async () => {
117 + await act(async () => {
118 React.startTransition(() => {
119 ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
120 ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+2 -2
@@ -518,7 +518,7 @@ describe('ReactIncrementalUpdates', () => {
518 expect(ReactNoop).toMatchRenderedOutput(<span prop="derived state" />);
519 });
520
521 - it('regression: does not expire soon due to layout effects in the last batch', () => {
521 + it('regression: does not expire soon due to layout effects in the last batch', async () => {
522 const {useState, useLayoutEffect} = React;
523
524 let setCount;
@@ -533,7 +533,7 @@ describe('ReactIncrementalUpdates', () => {
533 return null;
534 }
535
536 - act(async () => {
536 + await act(async () => {
537 React.startTransition(() => {
538 ReactNoop.render(<App />);
539 });
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+227 -247
@@ -5,6 +5,11 @@ let Scheduler;
5 let ReactFeatureFlags;
6 let Suspense;
7 let lazy;
8 +let waitForAll;
9 +let waitForThrow;
10 +let assertLog;
11 +
12 +let fakeModuleCache;
13
14 function normalizeCodeLocInfo(str) {
15 return (
@@ -27,6 +32,13 @@ describe('ReactLazy', () => {
32 lazy = React.lazy;
33 ReactTestRenderer = require('react-test-renderer');
34 Scheduler = require('scheduler');
35 +
36 + const InternalTestUtils = require('internal-test-utils');
37 + waitForAll = InternalTestUtils.waitForAll;
38 + waitForThrow = InternalTestUtils.waitForThrow;
39 + assertLog = InternalTestUtils.assertLog;
40 +
41 + fakeModuleCache = new Map();
42 });
43
44 function Text(props) {
@@ -34,12 +46,45 @@ describe('ReactLazy', () => {
46 return props.text;
47 }
48
37 - function delay(ms) {
38 - return new Promise(resolve => setTimeout(() => resolve(), ms));
49 + async function fakeImport(Component) {
50 + const record = fakeModuleCache.get(Component);
51 + if (record === undefined) {
52 + const newRecord = {
53 + status: 'pending',
54 + value: {default: Component},
55 + pings: [],
56 + then(ping) {
57 + switch (newRecord.status) {
58 + case 'pending': {
59 + newRecord.pings.push(ping);
60 + return;
61 + }
62 + case 'resolved': {
63 + ping(newRecord.value);
64 + return;
65 + }
66 + case 'rejected': {
67 + throw newRecord.value;
68 + }
69 + }
70 + },
71 + };
72 + fakeModuleCache.set(Component, newRecord);
73 + return newRecord;
74 + }
75 + return record;
76 }
77
41 - async function fakeImport(result) {
42 - return {default: result};
78 + function resolveFakeImport(moduleName) {
79 + const record = fakeModuleCache.get(moduleName);
80 + if (record === undefined) {
81 + throw new Error('Module not found');
82 + }
83 + if (record.status !== 'pending') {
84 + throw new Error('Module already resolved');
85 + }
86 + record.status = 'resolved';
87 + record.pings.forEach(ping => ping(record.value));
88 }
89
90 it('suspends until module has loaded', async () => {
@@ -54,12 +99,12 @@ describe('ReactLazy', () => {
99 },
100 );
101
57 - expect(Scheduler).toFlushAndYield(['Loading...']);
102 + await waitForAll(['Loading...']);
103 expect(root).not.toMatchRenderedOutput('Hi');
104
60 - await Promise.resolve();
105 + await resolveFakeImport(Text);
106
62 - expect(Scheduler).toFlushAndYield(['Hi']);
107 + await waitForAll(['Hi']);
108 expect(root).toMatchRenderedOutput('Hi');
109
110 // Should not suspend on update
@@ -68,7 +113,7 @@ describe('ReactLazy', () => {
113 <LazyText text="Hi again" />
114 </Suspense>,
115 );
71 - expect(Scheduler).toFlushAndYield(['Hi again']);
116 + await waitForAll(['Hi again']);
117 expect(root).toMatchRenderedOutput('Hi again');
118 });
119
@@ -85,7 +130,7 @@ describe('ReactLazy', () => {
130 </Suspense>,
131 );
132
88 - expect(Scheduler).toHaveYielded(['Hi']);
133 + assertLog(['Hi']);
134 expect(root).toMatchRenderedOutput('Hi');
135 });
136
@@ -115,7 +160,7 @@ describe('ReactLazy', () => {
160 </Suspense>
161 </ErrorBoundary>,
162 );
118 - expect(Scheduler).toHaveYielded([]);
163 + assertLog([]);
164 expect(root).toMatchRenderedOutput('Error: oh no');
165 });
166
@@ -128,11 +173,8 @@ describe('ReactLazy', () => {
173 return <Text text="Bar" />;
174 }
175
131 - const promiseForFoo = delay(100).then(() => fakeImport(Foo));
132 - const promiseForBar = delay(500).then(() => fakeImport(Bar));
133 -
134 - const LazyFoo = lazy(() => promiseForFoo);
135 - const LazyBar = lazy(() => promiseForBar);
176 + const LazyFoo = lazy(() => fakeImport(Foo));
177 + const LazyBar = lazy(() => fakeImport(Bar));
178
179 const root = ReactTestRenderer.create(
180 <Suspense fallback={<Text text="Loading..." />}>
@@ -144,19 +186,17 @@ describe('ReactLazy', () => {
186 },
187 );
188
147 - expect(Scheduler).toFlushAndYield(['Loading...']);
189 + await waitForAll(['Loading...']);
190 expect(root).not.toMatchRenderedOutput('FooBar');
191
150 - jest.advanceTimersByTime(100);
151 - await promiseForFoo;
192 + await resolveFakeImport(Foo);
193
153 - expect(Scheduler).toFlushAndYield(['Foo']);
194 + await waitForAll(['Foo']);
195 expect(root).not.toMatchRenderedOutput('FooBar');
196
156 - jest.advanceTimersByTime(500);
157 - await promiseForBar;
197 + await resolveFakeImport(Bar);
198
159 - expect(Scheduler).toFlushAndYield(['Foo', 'Bar']);
199 + await waitForAll(['Foo', 'Bar']);
200 expect(root).toMatchRenderedOutput('FooBar');
201 });
202
@@ -173,12 +213,9 @@ describe('ReactLazy', () => {
213 unstable_isConcurrent: true,
214 },
215 );
176 - expect(Scheduler).toFlushAndYield(['Loading...']);
216 + await waitForThrow('Element type is invalid');
217 + assertLog(['Loading...']);
218 expect(root).not.toMatchRenderedOutput('Hi');
178 -
179 - await Promise.resolve();
180 -
181 - expect(Scheduler).toFlushAndThrow('Element type is invalid');
219 if (__DEV__) {
220 expect(console.error).toHaveBeenCalledTimes(3);
221 expect(console.error.mock.calls[0][0]).toContain(
@@ -201,14 +238,9 @@ describe('ReactLazy', () => {
238 },
239 );
240
204 - expect(Scheduler).toFlushAndYield(['Loading...']);
241 + await waitForThrow('Bad network');
242 + assertLog(['Loading...']);
243 expect(root).not.toMatchRenderedOutput('Hi');
206 -
207 - try {
208 - await Promise.resolve();
209 - } catch (e) {}
210 -
211 - expect(Scheduler).toFlushAndThrow('Bad network');
244 });
245
246 it('mount and reorder', async () => {
@@ -247,28 +279,17 @@ describe('ReactLazy', () => {
279 unstable_isConcurrent: true,
280 });
281
250 - expect(Scheduler).toFlushAndYield(['Loading...']);
282 + await waitForAll(['Loading...']);
283 expect(root).not.toMatchRenderedOutput('AB');
284
253 - await LazyChildA;
254 - await LazyChildB;
285 + await resolveFakeImport(Child);
286
256 - expect(Scheduler).toFlushAndYield([
257 - 'A',
258 - 'B',
259 - 'Did mount: A',
260 - 'Did mount: B',
261 - ]);
287 + await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']);
288 expect(root).toMatchRenderedOutput('AB');
289
290 // Swap the position of A and B
291 root.update(<Parent swap={true} />);
266 - expect(Scheduler).toFlushAndYield([
267 - 'B',
268 - 'A',
269 - 'Did update: B',
270 - 'Did update: A',
271 - ]);
292 + await waitForAll(['B', 'A', 'Did update: B', 'Did update: A']);
293 expect(root).toMatchRenderedOutput('BA');
294 });
295
@@ -288,10 +309,10 @@ describe('ReactLazy', () => {
309 },
310 );
311
291 - expect(Scheduler).toFlushAndYield(['Loading...']);
312 + await waitForAll(['Loading...']);
313 expect(root).not.toMatchRenderedOutput('Hi');
314
294 - await Promise.resolve();
315 + await resolveFakeImport(T);
316
317 expect(() => expect(Scheduler).toFlushAndYield(['Hi'])).toErrorDev(
318 'Warning: T: Support for defaultProps ' +
@@ -307,7 +328,7 @@ describe('ReactLazy', () => {
328 <LazyText />
329 </Suspense>,
330 );
310 - expect(Scheduler).toFlushAndYield(['Hi again']);
331 + await waitForAll(['Hi again']);
332 expect(root).toMatchRenderedOutput('Hi again');
333 });
334
@@ -343,10 +364,10 @@ describe('ReactLazy', () => {
364 unstable_isConcurrent: true,
365 },
366 );
346 - expect(Scheduler).toFlushAndYield(['Loading...']);
367 + await waitForAll(['Loading...']);
368 expect(root).not.toMatchRenderedOutput('SiblingA');
369
349 - await Promise.resolve();
370 + await resolveFakeImport(LazyImpl);
371
372 expect(() =>
373 expect(Scheduler).toFlushAndYield(['Lazy', 'Sibling', 'A']),
@@ -360,7 +381,7 @@ describe('ReactLazy', () => {
381
382 // Lazy should not re-render
383 stateful.current.setState({text: 'B'});
363 - expect(Scheduler).toFlushAndYield(['B']);
384 + await waitForAll(['B']);
385 expect(root).toMatchRenderedOutput('SiblingB');
386 });
387
@@ -390,22 +411,22 @@ describe('ReactLazy', () => {
411 unstable_isConcurrent: true,
412 },
413 );
393 - expect(Scheduler).toFlushAndYield(['Not lazy: 0', 'Loading...']);
414 + await waitForAll(['Not lazy: 0', 'Loading...']);
415 expect(root).not.toMatchRenderedOutput('Not lazy: 0Lazy: 0');
416
396 - await Promise.resolve();
417 + await resolveFakeImport(LazyImpl);
418
398 - expect(Scheduler).toFlushAndYield(['Lazy: 0']);
419 + await waitForAll(['Lazy: 0']);
420 expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
421
422 // Should bailout due to unchanged props and state
423 instance1.current.setState(null);
403 - expect(Scheduler).toFlushAndYield([]);
424 + await waitForAll([]);
425 expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
426
427 // Should bailout due to unchanged props and state
428 instance2.current.setState(null);
408 - expect(Scheduler).toFlushAndYield([]);
429 + await waitForAll([]);
430 expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
431 });
432
@@ -436,22 +457,22 @@ describe('ReactLazy', () => {
457 unstable_isConcurrent: true,
458 },
459 );
439 - expect(Scheduler).toFlushAndYield(['Not lazy: 0', 'Loading...']);
460 + await waitForAll(['Not lazy: 0', 'Loading...']);
461 expect(root).not.toMatchRenderedOutput('Not lazy: 0Lazy: 0');
462
442 - await Promise.resolve();
463 + await resolveFakeImport(LazyImpl);
464
444 - expect(Scheduler).toFlushAndYield(['Lazy: 0']);
465 + await waitForAll(['Lazy: 0']);
466 expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
467
468 // Should bailout due to shallow equal props and state
469 instance1.current.setState({});
449 - expect(Scheduler).toFlushAndYield([]);
470 + await waitForAll([]);
471 expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
472
473 // Should bailout due to shallow equal props and state
474 instance2.current.setState({});
454 - expect(Scheduler).toFlushAndYield([]);
475 + await waitForAll([]);
476 expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
477 });
478
@@ -518,12 +539,12 @@ describe('ReactLazy', () => {
539 },
540 );
541
521 - expect(Scheduler).toFlushAndYield(['Loading...']);
542 + await waitForAll(['Loading...']);
543 expect(root).not.toMatchRenderedOutput('A1');
544
524 - await Promise.resolve();
545 + await resolveFakeImport(C);
546
526 - expect(Scheduler).toFlushAndYield([
547 + await waitForAll([
548 'constructor: A',
549 'getDerivedStateFromProps: A',
550 'A1',
@@ -535,7 +556,7 @@ describe('ReactLazy', () => {
556 <LazyClass num={2} />
557 </Suspense>,
558 );
538 - expect(Scheduler).toFlushAndYield([
559 + await waitForAll([
560 'getDerivedStateFromProps: A',
561 'shouldComponentUpdate: A -> A',
562 'A2',
@@ -549,7 +570,7 @@ describe('ReactLazy', () => {
570 <LazyClass num={3} />
571 </Suspense>,
572 );
552 - expect(Scheduler).toFlushAndYield([
573 + await waitForAll([
574 'getDerivedStateFromProps: A',
575 'shouldComponentUpdate: A -> A',
576 'A3',
@@ -595,13 +616,13 @@ describe('ReactLazy', () => {
616 </Suspense>,
617 );
618
598 - expect(Scheduler).toHaveYielded(['Loading...']);
599 - expect(Scheduler).toFlushAndYield([]);
619 + assertLog(['Loading...']);
620 + await waitForAll([]);
621 expect(root).toMatchRenderedOutput('Loading...');
622
602 - await Promise.resolve();
623 + await resolveFakeImport(C);
624
604 - expect(Scheduler).toHaveYielded([]);
625 + assertLog([]);
626
627 root.update(
628 <Suspense fallback={<Text text="Loading..." />}>
@@ -609,7 +630,7 @@ describe('ReactLazy', () => {
630 </Suspense>,
631 );
632
612 - expect(Scheduler).toHaveYielded(['UNSAFE_componentWillMount: A', 'A2']);
633 + assertLog(['UNSAFE_componentWillMount: A', 'A2']);
634 expect(root).toMatchRenderedOutput('A2');
635
636 root.update(
@@ -617,12 +638,12 @@ describe('ReactLazy', () => {
638 <LazyClass num={3} />
639 </Suspense>,
640 );
620 - expect(Scheduler).toHaveYielded([
641 + assertLog([
642 'UNSAFE_componentWillReceiveProps: A -> A',
643 'UNSAFE_componentWillUpdate: A -> A',
644 'A3',
645 ]);
625 - expect(Scheduler).toFlushAndYield([]);
646 + await waitForAll([]);
647 expect(root).toMatchRenderedOutput('A3');
648 });
649
@@ -651,10 +672,10 @@ describe('ReactLazy', () => {
672 },
673 );
674
654 - expect(Scheduler).toFlushAndYield(['Loading...']);
675 + await waitForAll(['Loading...']);
676 expect(root).not.toMatchRenderedOutput('Hi Bye');
677
657 - await Promise.resolve();
678 + await resolveFakeImport(T);
679 expect(() => expect(Scheduler).toFlushAndYield(['Hi Bye'])).toErrorDev(
680 'Warning: T: Support for defaultProps ' +
681 'will be removed from function components in a future major ' +
@@ -668,7 +689,7 @@ describe('ReactLazy', () => {
689 <LazyText outer="World" />
690 </Suspense>,
691 );
671 - expect(Scheduler).toFlushAndYield(['Hi World']);
692 + await waitForAll(['Hi World']);
693 expect(root).toMatchRenderedOutput('Hi World');
694
695 root.update(
@@ -676,7 +697,7 @@ describe('ReactLazy', () => {
697 <LazyText inner="Friends" />
698 </Suspense>,
699 );
679 - expect(Scheduler).toFlushAndYield(['Friends Bye']);
700 + await waitForAll(['Friends Bye']);
701 expect(root).toMatchRenderedOutput('Friends Bye');
702 });
703
@@ -692,9 +713,9 @@ describe('ReactLazy', () => {
713 },
714 );
715
695 - expect(Scheduler).toFlushAndYield(['Loading...']);
716 + await waitForAll(['Loading...']);
717
697 - await Promise.resolve();
718 + await resolveFakeImport(42);
719 root.update(
720 <Suspense fallback={<Text text="Loading..." />}>
721 <BadLazy />
@@ -719,10 +740,10 @@ describe('ReactLazy', () => {
740 },
741 );
742
722 - expect(Scheduler).toFlushAndYield(['Loading...']);
743 + await waitForAll(['Loading...']);
744 expect(root).not.toMatchRenderedOutput('Hello');
745
725 - await Promise.resolve();
746 + await resolveFakeImport(Lazy1);
747 root.update(
748 <Suspense fallback={<Text text="Loading..." />}>
749 <Lazy2 text="Hello" />
@@ -773,14 +794,14 @@ describe('ReactLazy', () => {
794 },
795 );
796
776 - expect(Scheduler).toFlushAndYield(['Loading...']);
797 + await waitForAll(['Loading...']);
798
799 expect(root).not.toMatchRenderedOutput('22');
800
801 // Mount
781 - await Promise.resolve();
782 - expect(() => {
783 - Scheduler.unstable_flushAll();
802 + await resolveFakeImport(Add);
803 + await expect(async () => {
804 + await waitForAll([]);
805 }).toErrorDev(
806 shouldWarnAboutFunctionDefaultProps
807 ? [
@@ -799,13 +820,13 @@ describe('ReactLazy', () => {
820 expect(root).toMatchRenderedOutput('22');
821
822 // Update
802 - expect(() => {
823 + await expect(async () => {
824 root.update(
825 <Suspense fallback={<Text text="Loading..." />}>
826 <LazyAdd inner={false} outer={false} />
827 </Suspense>,
828 );
808 - expect(Scheduler).toFlushWithoutYielding();
829 + await waitForAll([]);
830 }).toErrorDev(
831 'Invalid prop `inner` of type `boolean` supplied to `Add`, expected `number`.',
832 );
@@ -971,13 +992,13 @@ describe('ReactLazy', () => {
992 },
993 );
994
974 - expect(Scheduler).toFlushAndYield(['Loading...']);
995 + await waitForAll(['Loading...']);
996 expect(root).not.toMatchRenderedOutput('Inner default text');
997
998 // Mount
978 - await Promise.resolve();
979 - expect(() => {
980 - expect(Scheduler).toFlushAndYield(['Inner default text']);
999 + await resolveFakeImport(T);
1000 + await expect(async () => {
1001 + await waitForAll(['Inner default text']);
1002 }).toErrorDev([
1003 'T: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.',
1004 'The prop `text` is marked as required in `T`, but its value is `undefined`',
@@ -985,13 +1006,13 @@ describe('ReactLazy', () => {
1006 expect(root).toMatchRenderedOutput('Inner default text');
1007
1008 // Update
988 - expect(() => {
1009 + await expect(async () => {
1010 root.update(
1011 <Suspense fallback={<Text text="Loading..." />}>
1012 <LazyText text={null} />
1013 </Suspense>,
1014 );
994 - expect(Scheduler).toFlushAndYield([null]);
1015 + await waitForAll([null]);
1016 }).toErrorDev(
1017 'The prop `text` is marked as required in `T`, but its value is `null`',
1018 );
@@ -999,9 +1020,9 @@ describe('ReactLazy', () => {
1020 });
1021
1022 it('includes lazy-loaded component in warning stack', async () => {
1023 + const Foo = props => <div>{[<Text text="A" />, <Text text="B" />]}</div>;
1024 const LazyFoo = lazy(() => {
1025 Scheduler.unstable_yieldValue('Started loading');
1004 - const Foo = props => <div>{[<Text text="A" />, <Text text="B" />]}</div>;
1026 return fakeImport(Foo);
1027 });
1028
@@ -1014,39 +1035,39 @@ describe('ReactLazy', () => {
1035 },
1036 );
1037
1017 - expect(Scheduler).toFlushAndYield(['Started loading', 'Loading...']);
1038 + await waitForAll(['Started loading', 'Loading...']);
1039 expect(root).not.toMatchRenderedOutput(<div>AB</div>);
1040
1020 - await Promise.resolve();
1041 + await resolveFakeImport(Foo);
1042
1022 - expect(() => {
1023 - expect(Scheduler).toFlushAndYield(['A', 'B']);
1043 + await expect(async () => {
1044 + await waitForAll(['A', 'B']);
1045 }).toErrorDev(' in Text (at **)\n' + ' in Foo (at **)');
1046 expect(root).toMatchRenderedOutput(<div>AB</div>);
1047 });
1048
1049 it('supports class and forwardRef components', async () => {
1029 - const LazyClass = lazy(() => {
1030 - class Foo extends React.Component {
1031 - render() {
1032 - return <Text text="Foo" />;
1033 - }
1050 + class Foo extends React.Component {
1051 + render() {
1052 + return <Text text="Foo" />;
1053 }
1054 + }
1055 + const LazyClass = lazy(() => {
1056 return fakeImport(Foo);
1057 });
1058
1038 - const LazyForwardRef = lazy(() => {
1039 - class Bar extends React.Component {
1040 - render() {
1041 - return <Text text="Bar" />;
1042 - }
1059 + class Bar extends React.Component {
1060 + render() {
1061 + return <Text text="Bar" />;
1062 }
1044 - return fakeImport(
1045 - React.forwardRef((props, ref) => {
1046 - Scheduler.unstable_yieldValue('forwardRef');
1047 - return <Bar ref={ref} />;
1048 - }),
1049 - );
1063 + }
1064 + const ForwardRefBar = React.forwardRef((props, ref) => {
1065 + Scheduler.unstable_yieldValue('forwardRef');
1066 + return <Bar ref={ref} />;
1067 + });
1068 +
1069 + const LazyForwardRef = lazy(() => {
1070 + return fakeImport(ForwardRefBar);
1071 });
1072
1073 const ref = React.createRef();
@@ -1060,13 +1081,16 @@ describe('ReactLazy', () => {
1081 },
1082 );
1083
1063 - expect(Scheduler).toFlushAndYield(['Loading...']);
1084 + await waitForAll(['Loading...']);
1085 expect(root).not.toMatchRenderedOutput('FooBar');
1086 expect(ref.current).toBe(null);
1087
1067 - await Promise.resolve();
1088 + await resolveFakeImport(Foo);
1089 + await waitForAll(['Foo']);
1090
1069 - expect(Scheduler).toFlushAndYield(['Foo', 'forwardRef', 'Bar']);
1091 + await resolveFakeImport(ForwardRefBar);
1092 +
1093 + await waitForAll(['Foo', 'forwardRef', 'Bar']);
1094 expect(root).toMatchRenderedOutput('FooBar');
1095 expect(ref.current).not.toBe(null);
1096 });
@@ -1088,13 +1112,13 @@ describe('ReactLazy', () => {
1112 unstable_isConcurrent: true,
1113 },
1114 );
1091 - expect(Scheduler).toFlushAndYield(['Loading...']);
1115 + await waitForAll(['Loading...']);
1116 expect(root).not.toMatchRenderedOutput('4');
1117
1118 // Mount
1095 - await Promise.resolve();
1096 - expect(() => {
1097 - expect(Scheduler).toFlushWithoutYielding();
1119 + await resolveFakeImport(Add);
1120 + await expect(async () => {
1121 + await waitForAll([]);
1122 }).toErrorDev(
1123 'Unknown: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.',
1124 );
@@ -1106,7 +1130,7 @@ describe('ReactLazy', () => {
1130 <LazyAdd outer={2} />
1131 </Suspense>,
1132 );
1109 - expect(Scheduler).toFlushWithoutYielding();
1133 + await waitForAll([]);
1134 expect(root).toMatchRenderedOutput('4');
1135
1136 // Update
@@ -1115,7 +1139,7 @@ describe('ReactLazy', () => {
1139 <LazyAdd outer={3} />
1140 </Suspense>,
1141 );
1118 - expect(Scheduler).toFlushWithoutYielding();
1142 + await waitForAll([]);
1143 expect(root).toMatchRenderedOutput('5');
1144
1145 // Update (shallowly equal)
@@ -1124,7 +1148,7 @@ describe('ReactLazy', () => {
1148 <LazyAdd outer={3} />
1149 </Suspense>,
1150 );
1127 - expect(Scheduler).toFlushWithoutYielding();
1151 + await waitForAll([]);
1152 expect(root).toMatchRenderedOutput('5');
1153
1154 // Update (explicit props)
@@ -1133,7 +1157,7 @@ describe('ReactLazy', () => {
1157 <LazyAdd outer={1} inner={1} />
1158 </Suspense>,
1159 );
1136 - expect(Scheduler).toFlushWithoutYielding();
1160 + await waitForAll([]);
1161 expect(root).toMatchRenderedOutput('2');
1162
1163 // Update (explicit props, shallowly equal)
@@ -1142,7 +1166,7 @@ describe('ReactLazy', () => {
1166 <LazyAdd outer={1} inner={1} />
1167 </Suspense>,
1168 );
1145 - expect(Scheduler).toFlushWithoutYielding();
1169 + await waitForAll([]);
1170 expect(root).toMatchRenderedOutput('2');
1171
1172 // Update
@@ -1151,7 +1175,7 @@ describe('ReactLazy', () => {
1175 <LazyAdd outer={1} />
1176 </Suspense>,
1177 );
1154 - expect(Scheduler).toFlushWithoutYielding();
1178 + await waitForAll([]);
1179 expect(root).toMatchRenderedOutput('3');
1180 });
1181
@@ -1176,13 +1200,13 @@ describe('ReactLazy', () => {
1200 unstable_isConcurrent: true,
1201 },
1202 );
1179 - expect(Scheduler).toFlushAndYield(['Loading...']);
1203 + await waitForAll(['Loading...']);
1204 expect(root).not.toMatchRenderedOutput('4');
1205
1206 // Mount
1183 - await Promise.resolve();
1184 - expect(() => {
1185 - expect(Scheduler).toFlushWithoutYielding();
1207 + await resolveFakeImport(Add);
1208 + await expect(async () => {
1209 + await waitForAll([]);
1210 }).toErrorDev([
1211 'Memo: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.',
1212 'Unknown: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.',
@@ -1195,7 +1219,7 @@ describe('ReactLazy', () => {
1219 <LazyAdd outer={3} />
1220 </Suspense>,
1221 );
1198 - expect(Scheduler).toFlushWithoutYielding();
1222 + await waitForAll([]);
1223 expect(root).toMatchRenderedOutput('5');
1224
1225 // Update
@@ -1204,13 +1228,13 @@ describe('ReactLazy', () => {
1228 <LazyAdd />
1229 </Suspense>,
1230 );
1207 - expect(Scheduler).toFlushWithoutYielding();
1231 + await waitForAll([]);
1232 expect(root).toMatchRenderedOutput('2');
1233 });
1234
1235 it('warns about ref on functions for lazy-loaded components', async () => {
1236 + const Foo = props => <div />;
1237 const LazyFoo = lazy(() => {
1213 - const Foo = props => <div />;
1238 return fakeImport(Foo);
1239 });
1240
@@ -1224,21 +1248,20 @@ describe('ReactLazy', () => {
1248 },
1249 );
1250
1227 - expect(Scheduler).toFlushAndYield(['Loading...']);
1228 - await Promise.resolve();
1229 - expect(() => {
1230 - expect(Scheduler).toFlushAndYield([]);
1251 + await waitForAll(['Loading...']);
1252 + await resolveFakeImport(Foo);
1253 + await expect(async () => {
1254 + await waitForAll([]);
1255 }).toErrorDev('Function components cannot be given refs');
1256 });
1257
1258 it('should error with a component stack naming the resolved component', async () => {
1259 let componentStackMessage;
1260
1237 - const LazyText = lazy(() =>
1238 - fakeImport(function ResolvedText() {
1239 - throw new Error('oh no');
1240 - }),
1241 - );
1261 + function ResolvedText() {
1262 + throw new Error('oh no');
1263 + }
1264 + const LazyText = lazy(() => fakeImport(ResolvedText));
1265
1266 class ErrorBoundary extends React.Component {
1267 state = {error: null};
@@ -1264,13 +1287,10 @@ describe('ReactLazy', () => {
1287 {unstable_isConcurrent: true},
1288 );
1289
1267 - expect(Scheduler).toFlushAndYield(['Loading...']);
1268 -
1269 - try {
1270 - await Promise.resolve();
1271 - } catch (e) {}
1290 + await waitForAll(['Loading...']);
1291
1273 - expect(Scheduler).toFlushAndYield([]);
1292 + await resolveFakeImport(ResolvedText);
1293 + await waitForAll([]);
1294
1295 expect(componentStackMessage).toContain('in ResolvedText');
1296 });
@@ -1307,7 +1327,7 @@ describe('ReactLazy', () => {
1327 </ErrorBoundary>,
1328 );
1329
1310 - expect(Scheduler).toHaveYielded([]);
1330 + assertLog([]);
1331
1332 expect(componentStackMessage).toContain('in Lazy');
1333 });
@@ -1375,41 +1395,28 @@ describe('ReactLazy', () => {
1395 unstable_isConcurrent: true,
1396 });
1397
1378 - expect(Scheduler).toFlushAndYield(['Init A', 'Init B', 'Loading...']);
1398 + await waitForAll(['Init A', 'Init B', 'Loading...']);
1399 expect(root).not.toMatchRenderedOutput('AB');
1400
1381 - await LazyChildA;
1382 - await LazyChildB;
1401 + await resolveFakeImport(ChildA);
1402 + await resolveFakeImport(ChildB);
1403
1384 - expect(Scheduler).toFlushAndYield([
1385 - 'A',
1386 - 'B',
1387 - 'Did mount: A',
1388 - 'Did mount: B',
1389 - ]);
1404 + await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']);
1405 expect(root).toMatchRenderedOutput('AB');
1406
1407 // Swap the position of A and B
1408 root.update(<Parent swap={true} />);
1394 - expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
1409 + await waitForAll(['Init B2', 'Loading...']);
1410 jest.runAllTimers();
1411
1397 - expect(Scheduler).toHaveYielded(['Did unmount: A', 'Did unmount: B']);
1412 + assertLog(['Did unmount: A', 'Did unmount: B']);
1413
1414 // The suspense boundary should've triggered now.
1415 expect(root).toMatchRenderedOutput('Loading...');
1416 await resolveB2({default: ChildB});
1417
1418 // We need to flush to trigger the second one to load.
1404 - expect(Scheduler).toFlushAndYield(['Init A2']);
1405 - await LazyChildA2;
1406 -
1407 - expect(Scheduler).toFlushAndYield([
1408 - 'b',
1409 - 'a',
1410 - 'Did mount: b',
1411 - 'Did mount: a',
1412 - ]);
1419 + await waitForAll(['Init A2', 'b', 'a', 'Did mount: b', 'Did mount: a']);
1420 expect(root).toMatchRenderedOutput('ba');
1421 });
1422
@@ -1470,34 +1477,19 @@ describe('ReactLazy', () => {
1477 unstable_isConcurrent: false,
1478 });
1479
1473 - expect(Scheduler).toHaveYielded(['Init A', 'Init B', 'Loading...']);
1480 + assertLog(['Init A', 'Init B', 'Loading...']);
1481 expect(root).not.toMatchRenderedOutput('AB');
1482
1476 - await LazyChildA;
1477 - await LazyChildB;
1483 + await resolveFakeImport(ChildA);
1484 + await resolveFakeImport(ChildB);
1485
1479 - expect(Scheduler).toFlushAndYield([
1480 - 'A',
1481 - 'B',
1482 - 'Did mount: A',
1483 - 'Did mount: B',
1484 - ]);
1486 + await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']);
1487 expect(root).toMatchRenderedOutput('AB');
1488
1489 // Swap the position of A and B
1490 root.update(<Parent swap={true} />);
1489 - expect(Scheduler).toHaveYielded(['Init B2', 'Loading...']);
1490 - await LazyChildB2;
1491 - // We need to flush to trigger the second one to load.
1492 - expect(Scheduler).toFlushAndYield(['Init A2']);
1493 - await LazyChildA2;
1494 -
1495 - expect(Scheduler).toFlushAndYield([
1496 - 'b',
1497 - 'a',
1498 - 'Did update: b',
1499 - 'Did update: a',
1500 - ]);
1491 + assertLog(['Init B2', 'Loading...']);
1492 + await waitForAll(['Init A2', 'b', 'a', 'Did update: b', 'Did update: a']);
1493 expect(root).toMatchRenderedOutput('ba');
1494 });
1495
@@ -1514,21 +1506,25 @@ describe('ReactLazy', () => {
1506 }
1507 }
1508
1509 + const ChildA = <Child key="A" label="A" />;
1510 const lazyChildA = lazy(() => {
1511 Scheduler.unstable_yieldValue('Init A');
1519 - return fakeImport(<Child key="A" label="A" />);
1512 + return fakeImport(ChildA);
1513 });
1514 + const ChildB = <Child key="B" label="B" />;
1515 const lazyChildB = lazy(() => {
1516 Scheduler.unstable_yieldValue('Init B');
1523 - return fakeImport(<Child key="B" label="B" />);
1517 + return fakeImport(ChildB);
1518 });
1519 + const ChildA2 = <Child key="A" label="a" />;
1520 const lazyChildA2 = lazy(() => {
1521 Scheduler.unstable_yieldValue('Init A2');
1527 - return fakeImport(<Child key="A" label="a" />);
1522 + return fakeImport(ChildA2);
1523 });
1524 + const ChildB2 = <Child key="B" label="b" />;
1525 const lazyChildB2 = lazy(() => {
1526 Scheduler.unstable_yieldValue('Init B2');
1531 - return fakeImport(<Child key="B" label="b" />);
1527 + return fakeImport(ChildB2);
1528 });
1529
1530 function Parent({swap}) {
@@ -1543,38 +1539,28 @@ describe('ReactLazy', () => {
1539 unstable_isConcurrent: true,
1540 });
1541
1546 - expect(Scheduler).toFlushAndYield(['Init A', 'Loading...']);
1542 + await waitForAll(['Init A', 'Loading...']);
1543 expect(root).not.toMatchRenderedOutput('AB');
1544
1549 - await lazyChildA;
1545 + await resolveFakeImport(ChildA);
1546 // We need to flush to trigger the B to load.
1551 - expect(Scheduler).toFlushAndYield(['Init B']);
1552 - await lazyChildB;
1553 -
1554 - expect(Scheduler).toFlushAndYield([
1555 - 'A',
1556 - 'B',
1557 - 'Did mount: A',
1558 - 'Did mount: B',
1559 - ]);
1547 + await waitForAll(['Init B']);
1548 + await resolveFakeImport(ChildB);
1549 +
1550 + await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']);
1551 expect(root).toMatchRenderedOutput('AB');
1552
1553 // Swap the position of A and B
1554 React.startTransition(() => {
1555 root.update(<Parent swap={true} />);
1556 });
1566 - expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
1567 - await lazyChildB2;
1557 + await waitForAll(['Init B2', 'Loading...']);
1558 + await resolveFakeImport(ChildB2);
1559 // We need to flush to trigger the second one to load.
1569 - expect(Scheduler).toFlushAndYield(['Init A2', 'Loading...']);
1570 - await lazyChildA2;
1571 -
1572 - expect(Scheduler).toFlushAndYield([
1573 - 'b',
1574 - 'a',
1575 - 'Did update: b',
1576 - 'Did update: a',
1577 - ]);
1560 + await waitForAll(['Init A2', 'Loading...']);
1561 + await resolveFakeImport(ChildA2);
1562 +
1563 + await waitForAll(['b', 'a', 'Did update: b', 'Did update: a']);
1564 expect(root).toMatchRenderedOutput('ba');
1565 });
1566
@@ -1591,21 +1577,25 @@ describe('ReactLazy', () => {
1577 }
1578 }
1579
1580 + const ChildA = <Child key="A" label="A" />;
1581 const lazyChildA = lazy(() => {
1582 Scheduler.unstable_yieldValue('Init A');
1596 - return fakeImport(<Child key="A" label="A" />);
1583 + return fakeImport(ChildA);
1584 });
1585 + const ChildB = <Child key="B" label="B" />;
1586 const lazyChildB = lazy(() => {
1587 Scheduler.unstable_yieldValue('Init B');
1600 - return fakeImport(<Child key="B" label="B" />);
1588 + return fakeImport(ChildB);
1589 });
1590 + const ChildA2 = <Child key="A" label="a" />;
1591 const lazyChildA2 = lazy(() => {
1592 Scheduler.unstable_yieldValue('Init A2');
1604 - return fakeImport(<Child key="A" label="a" />);
1593 + return fakeImport(ChildA2);
1594 });
1595 + const ChildB2 = <Child key="B" label="b" />;
1596 const lazyChildB2 = lazy(() => {
1597 Scheduler.unstable_yieldValue('Init B2');
1608 - return fakeImport(<Child key="B" label="b" />);
1598 + return fakeImport(ChildB2);
1599 });
1600
1601 function Parent({swap}) {
@@ -1620,36 +1610,26 @@ describe('ReactLazy', () => {
1610 unstable_isConcurrent: false,
1611 });
1612
1623 - expect(Scheduler).toHaveYielded(['Init A', 'Loading...']);
1613 + assertLog(['Init A', 'Loading...']);
1614 expect(root).not.toMatchRenderedOutput('AB');
1615
1626 - await lazyChildA;
1616 + await resolveFakeImport(ChildA);
1617 // We need to flush to trigger the B to load.
1628 - expect(Scheduler).toFlushAndYield(['Init B']);
1629 - await lazyChildB;
1630 -
1631 - expect(Scheduler).toFlushAndYield([
1632 - 'A',
1633 - 'B',
1634 - 'Did mount: A',
1635 - 'Did mount: B',
1636 - ]);
1618 + await waitForAll(['Init B']);
1619 + await resolveFakeImport(ChildB);
1620 +
1621 + await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']);
1622 expect(root).toMatchRenderedOutput('AB');
1623
1624 // Swap the position of A and B
1625 root.update(<Parent swap={true} />);
1641 - expect(Scheduler).toHaveYielded(['Init B2', 'Loading...']);
1642 - await lazyChildB2;
1626 + assertLog(['Init B2', 'Loading...']);
1627 + await resolveFakeImport(ChildB2);
1628 // We need to flush to trigger the second one to load.
1644 - expect(Scheduler).toFlushAndYield(['Init A2']);
1645 - await lazyChildA2;
1646 -
1647 - expect(Scheduler).toFlushAndYield([
1648 - 'b',
1649 - 'a',
1650 - 'Did update: b',
1651 - 'Did update: a',
1652 - ]);
1629 + await waitForAll(['Init A2']);
1630 + await resolveFakeImport(ChildA2);
1631 +
1632 + await waitForAll(['b', 'a', 'Did update: b', 'Did update: a']);
1633 expect(root).toMatchRenderedOutput('ba');
1634 });
1635 });