main
js 1,764 lines 48 KB
Raw
1 let React;
2 let ReactTestRenderer;
3 let Scheduler;
4 let Suspense;
5 let lazy;
6 let waitFor;
7 let waitForAll;
8 let waitForThrow;
9 let assertLog;
10 let assertConsoleErrorDev;
11 let act;
12
13 let fakeModuleCache;
14
15 function normalizeCodeLocInfo(str) {
16 return (
17 str &&
18 str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
19 return '\n in ' + name + ' (at **)';
20 })
21 );
22 }
23
24 describe('ReactLazy', () => {
25 beforeEach(() => {
26 jest.resetModules();
27 React = require('react');
28 Suspense = React.Suspense;
29 lazy = React.lazy;
30 ReactTestRenderer = require('react-test-renderer');
31 Scheduler = require('scheduler');
32
33 const InternalTestUtils = require('internal-test-utils');
34 waitFor = InternalTestUtils.waitFor;
35 waitForAll = InternalTestUtils.waitForAll;
36 waitForThrow = InternalTestUtils.waitForThrow;
37 assertLog = InternalTestUtils.assertLog;
38 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
39 act = InternalTestUtils.act;
40
41 fakeModuleCache = new Map();
42 });
43
44 function Text(props) {
45 Scheduler.log(props.text);
46 return props.text;
47 }
48
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
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 () => {
91 const LazyText = lazy(() => fakeImport(Text));
92
93 const root = ReactTestRenderer.create(
94 <Suspense fallback={<Text text="Loading..." />}>
95 <LazyText text="Hi" />
96 </Suspense>,
97 {
98 unstable_isConcurrent: true,
99 },
100 );
101
102 await waitForAll(['Loading...']);
103 expect(root).not.toMatchRenderedOutput('Hi');
104
105 await act(() => resolveFakeImport(Text));
106 assertLog(['Hi']);
107 expect(root).toMatchRenderedOutput('Hi');
108
109 // Should not suspend on update
110 root.update(
111 <Suspense fallback={<Text text="Loading..." />}>
112 <LazyText text="Hi again" />
113 </Suspense>,
114 );
115 await waitForAll(['Hi again']);
116 expect(root).toMatchRenderedOutput('Hi again');
117 });
118
119 it('renders a lazy context provider', async () => {
120 const Context = React.createContext('default');
121 function ConsumerText() {
122 return <Text text={React.useContext(Context)} />;
123 }
124 // Context.Provider === Context, so we can lazy-load the context itself
125 const LazyProvider = lazy(() => fakeImport(Context));
126
127 const root = ReactTestRenderer.create(
128 <Suspense fallback={<Text text="Loading..." />}>
129 <LazyProvider value="Hi">
130 <ConsumerText />
131 </LazyProvider>
132 </Suspense>,
133 {
134 unstable_isConcurrent: true,
135 },
136 );
137
138 await waitForAll(['Loading...']);
139 expect(root).not.toMatchRenderedOutput('Hi');
140
141 await act(() => resolveFakeImport(Context));
142 assertLog(['Hi']);
143 expect(root).toMatchRenderedOutput('Hi');
144
145 // Should not suspend on update
146 root.update(
147 <Suspense fallback={<Text text="Loading..." />}>
148 <LazyProvider value="Hi again">
149 <ConsumerText />
150 </LazyProvider>
151 </Suspense>,
152 );
153 await waitForAll(['Hi again']);
154 expect(root).toMatchRenderedOutput('Hi again');
155 });
156
157 it('can resolve synchronously without suspending', async () => {
158 const LazyText = lazy(() => ({
159 then(cb) {
160 cb({default: Text});
161 },
162 }));
163
164 let root;
165 await act(() => {
166 root = ReactTestRenderer.create(
167 <Suspense fallback={<Text text="Loading..." />}>
168 <LazyText text="Hi" />
169 </Suspense>,
170 {unstable_isConcurrent: true},
171 );
172 });
173
174 assertLog(['Hi']);
175 expect(root).toMatchRenderedOutput('Hi');
176 });
177
178 it('can reject synchronously without suspending', async () => {
179 const LazyText = lazy(() => ({
180 then(resolve, reject) {
181 reject(new Error('oh no'));
182 },
183 }));
184
185 class ErrorBoundary extends React.Component {
186 state = {};
187 static getDerivedStateFromError(error) {
188 return {message: error.message};
189 }
190 render() {
191 return this.state.message
192 ? `Error: ${this.state.message}`
193 : this.props.children;
194 }
195 }
196
197 let root;
198 await act(() => {
199 root = ReactTestRenderer.create(
200 <ErrorBoundary>
201 <Suspense fallback={<Text text="Loading..." />}>
202 <LazyText text="Hi" />
203 </Suspense>
204 </ErrorBoundary>,
205 {unstable_isConcurrent: true},
206 );
207 });
208 assertLog([]);
209 expect(root).toMatchRenderedOutput('Error: oh no');
210 });
211
212 it('multiple lazy components', async () => {
213 function Foo() {
214 return <Text text="Foo" />;
215 }
216
217 function Bar() {
218 return <Text text="Bar" />;
219 }
220
221 const LazyFoo = lazy(() => fakeImport(Foo));
222 const LazyBar = lazy(() => fakeImport(Bar));
223
224 const root = ReactTestRenderer.create(
225 <Suspense fallback={<Text text="Loading..." />}>
226 <LazyFoo />
227 <LazyBar />
228 </Suspense>,
229 {
230 unstable_isConcurrent: true,
231 },
232 );
233
234 await waitForAll(['Loading...']);
235 expect(root).not.toMatchRenderedOutput('FooBar');
236
237 await resolveFakeImport(Foo);
238
239 await waitForAll(['Foo']);
240 expect(root).not.toMatchRenderedOutput('FooBar');
241
242 await act(() => resolveFakeImport(Bar));
243 assertLog(['Foo', 'Bar']);
244 expect(root).toMatchRenderedOutput('FooBar');
245 });
246
247 it('does not support arbitrary promises, only module objects', async () => {
248 const LazyText = lazy(async () => Text);
249
250 const root = ReactTestRenderer.create(null, {
251 unstable_isConcurrent: true,
252 });
253
254 function App() {
255 return (
256 <Suspense fallback={<Text text="Loading..." />}>
257 <LazyText text="Hi" />
258 </Suspense>
259 );
260 }
261
262 let error;
263 try {
264 await act(() => {
265 root.update(<App />);
266 });
267 } catch (e) {
268 error = e;
269 }
270
271 expect(error.message).toMatch('Element type is invalid');
272 assertLog(['Loading...']);
273 assertConsoleErrorDev([
274 'lazy: Expected the result of a dynamic import() call. ' +
275 'Instead received: function Text(props) {\n' +
276 ' Scheduler.log(props.text);\n' +
277 ' return props.text;\n' +
278 ' }\n\n' +
279 'Your code should look like: \n ' +
280 "const MyComponent = lazy(() => import('./MyComponent'))\n" +
281 ' in App (at **)',
282 'lazy: Expected the result of a dynamic import() call. ' +
283 'Instead received: function Text(props) {\n' +
284 ' Scheduler.log(props.text);\n' +
285 ' return props.text;\n' +
286 ' }\n\n' +
287 'Your code should look like: \n ' +
288 "const MyComponent = lazy(() => import('./MyComponent'))\n" +
289 ' in App (at **)',
290 ]);
291 expect(root).not.toMatchRenderedOutput('Hi');
292 });
293
294 it('throws if promise rejects', async () => {
295 const networkError = new Error('Bad network');
296 const LazyText = lazy(async () => {
297 throw networkError;
298 });
299
300 const root = ReactTestRenderer.create(null, {
301 unstable_isConcurrent: true,
302 });
303
304 let error;
305 try {
306 await act(() => {
307 root.update(
308 <Suspense fallback={<Text text="Loading..." />}>
309 <LazyText text="Hi" />
310 </Suspense>,
311 );
312 });
313 } catch (e) {
314 error = e;
315 }
316
317 expect(error).toBe(networkError);
318 assertLog(['Loading...']);
319 expect(root).not.toMatchRenderedOutput('Hi');
320 });
321
322 it('mount and reorder', async () => {
323 class Child extends React.Component {
324 componentDidMount() {
325 Scheduler.log('Did mount: ' + this.props.label);
326 }
327 componentDidUpdate() {
328 Scheduler.log('Did update: ' + this.props.label);
329 }
330 render() {
331 return <Text text={this.props.label} />;
332 }
333 }
334
335 const LazyChildA = lazy(() => {
336 Scheduler.log('Suspend! [LazyChildA]');
337 return fakeImport(Child);
338 });
339 const LazyChildB = lazy(() => {
340 Scheduler.log('Suspend! [LazyChildB]');
341 return fakeImport(Child);
342 });
343
344 function Parent({swap}) {
345 return (
346 <Suspense fallback={<Text text="Loading..." />}>
347 {swap
348 ? [
349 <LazyChildB key="B" label="B" />,
350 <LazyChildA key="A" label="A" />,
351 ]
352 : [
353 <LazyChildA key="A" label="A" />,
354 <LazyChildB key="B" label="B" />,
355 ]}
356 </Suspense>
357 );
358 }
359
360 const root = ReactTestRenderer.create(<Parent swap={false} />, {
361 unstable_isConcurrent: true,
362 });
363
364 await waitForAll([
365 'Suspend! [LazyChildA]',
366 'Loading...',
367 // pre-warming
368 'Suspend! [LazyChildB]',
369 ]);
370 expect(root).not.toMatchRenderedOutput('AB');
371
372 await act(async () => {
373 await resolveFakeImport(Child);
374
375 // B suspends even though it happens to share the same import as A.
376 // TODO: React.lazy should implement the `status` and `value` fields, so
377 // we can unwrap the result synchronously if it already loaded. Like `use`.
378 await waitFor([
379 'A',
380 // pre-warming: LazyChildB was already initialized. So it also already resolved
381 // when we called resolveFakeImport above. So it doesn't suspend again.
382 'B',
383 ]);
384 });
385 assertLog(['Did mount: A', 'Did mount: B']);
386 expect(root).toMatchRenderedOutput('AB');
387
388 // Swap the position of A and B
389 root.update(<Parent swap={true} />);
390 await waitForAll(['B', 'A', 'Did update: B', 'Did update: A']);
391 expect(root).toMatchRenderedOutput('BA');
392 });
393
394 it('resolves defaultProps, on mount and update', async () => {
395 class T extends React.Component {
396 render() {
397 return <Text {...this.props} />;
398 }
399 }
400 T.defaultProps = {text: 'Hi'};
401 const LazyText = lazy(() => fakeImport(T));
402
403 const root = ReactTestRenderer.create(
404 <Suspense fallback={<Text text="Loading..." />}>
405 <LazyText />
406 </Suspense>,
407 {
408 unstable_isConcurrent: true,
409 },
410 );
411
412 await waitForAll(['Loading...']);
413 expect(root).not.toMatchRenderedOutput('Hi');
414
415 await act(() => resolveFakeImport(T));
416 assertLog(['Hi']);
417
418 expect(root).toMatchRenderedOutput('Hi');
419
420 T.defaultProps = {text: 'Hi again'};
421 root.update(
422 <Suspense fallback={<Text text="Loading..." />}>
423 <LazyText />
424 </Suspense>,
425 );
426 await waitForAll(['Hi again']);
427 expect(root).toMatchRenderedOutput('Hi again');
428 });
429
430 it('resolves defaultProps without breaking memoization', async () => {
431 class LazyImpl extends React.Component {
432 render() {
433 Scheduler.log('Lazy');
434 return (
435 <>
436 <Text text={this.props.siblingText} />
437 {this.props.children}
438 </>
439 );
440 }
441 }
442 LazyImpl.defaultProps = {siblingText: 'Sibling'};
443 const Lazy = lazy(() => fakeImport(LazyImpl));
444
445 class Stateful extends React.Component {
446 state = {text: 'A'};
447 render() {
448 return <Text text={this.state.text} />;
449 }
450 }
451
452 const stateful = React.createRef(null);
453
454 const root = ReactTestRenderer.create(
455 <Suspense fallback={<Text text="Loading..." />}>
456 <Lazy>
457 <Stateful ref={stateful} />
458 </Lazy>
459 </Suspense>,
460 {
461 unstable_isConcurrent: true,
462 },
463 );
464 await waitForAll(['Loading...']);
465 expect(root).not.toMatchRenderedOutput('SiblingA');
466
467 await act(() => resolveFakeImport(LazyImpl));
468 assertLog(['Lazy', 'Sibling', 'A']);
469
470 expect(root).toMatchRenderedOutput('SiblingA');
471
472 // Lazy should not re-render
473 stateful.current.setState({text: 'B'});
474 await waitForAll(['B']);
475 expect(root).toMatchRenderedOutput('SiblingB');
476 });
477
478 it('resolves defaultProps without breaking bailout due to unchanged props and state, #17151', async () => {
479 class LazyImpl extends React.Component {
480 static defaultProps = {value: 0};
481
482 render() {
483 const text = `${this.props.label}: ${this.props.value}`;
484 return <Text text={text} />;
485 }
486 }
487
488 const Lazy = lazy(() => fakeImport(LazyImpl));
489
490 const instance1 = React.createRef(null);
491 const instance2 = React.createRef(null);
492
493 const root = ReactTestRenderer.create(
494 <>
495 <LazyImpl ref={instance1} label="Not lazy" />
496 <Suspense fallback={<Text text="Loading..." />}>
497 <Lazy ref={instance2} label="Lazy" />
498 </Suspense>
499 </>,
500 {
501 unstable_isConcurrent: true,
502 },
503 );
504 await waitForAll(['Not lazy: 0', 'Loading...']);
505 expect(root).not.toMatchRenderedOutput('Not lazy: 0Lazy: 0');
506
507 await act(() => resolveFakeImport(LazyImpl));
508 assertLog(['Lazy: 0']);
509 expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
510
511 // Should bailout due to unchanged props and state
512 instance1.current.setState(null);
513 await waitForAll([]);
514 expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
515
516 // Should bailout due to unchanged props and state
517 instance2.current.setState(null);
518 await waitForAll([]);
519 expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
520 });
521
522 it('resolves defaultProps without breaking bailout in PureComponent, #17151', async () => {
523 class LazyImpl extends React.PureComponent {
524 static defaultProps = {value: 0};
525 state = {};
526
527 render() {
528 const text = `${this.props.label}: ${this.props.value}`;
529 return <Text text={text} />;
530 }
531 }
532
533 const Lazy = lazy(() => fakeImport(LazyImpl));
534
535 const instance1 = React.createRef(null);
536 const instance2 = React.createRef(null);
537
538 const root = ReactTestRenderer.create(
539 <>
540 <LazyImpl ref={instance1} label="Not lazy" />
541 <Suspense fallback={<Text text="Loading..." />}>
542 <Lazy ref={instance2} label="Lazy" />
543 </Suspense>
544 </>,
545 {
546 unstable_isConcurrent: true,
547 },
548 );
549 await waitForAll(['Not lazy: 0', 'Loading...']);
550 expect(root).not.toMatchRenderedOutput('Not lazy: 0Lazy: 0');
551
552 await act(() => resolveFakeImport(LazyImpl));
553 assertLog(['Lazy: 0']);
554 expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
555
556 // Should bailout due to shallow equal props and state
557 instance1.current.setState({});
558 await waitForAll([]);
559 expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
560
561 // Should bailout due to shallow equal props and state
562 instance2.current.setState({});
563 await waitForAll([]);
564 expect(root).toMatchRenderedOutput('Not lazy: 0Lazy: 0');
565 });
566
567 it('sets defaultProps for modern lifecycles', async () => {
568 class C extends React.Component {
569 static defaultProps = {text: 'A'};
570 state = {};
571
572 static getDerivedStateFromProps(props) {
573 Scheduler.log(`getDerivedStateFromProps: ${props.text}`);
574 return null;
575 }
576
577 constructor(props) {
578 super(props);
579 Scheduler.log(`constructor: ${this.props.text}`);
580 }
581
582 componentDidMount() {
583 Scheduler.log(`componentDidMount: ${this.props.text}`);
584 }
585
586 componentDidUpdate(prevProps) {
587 Scheduler.log(
588 `componentDidUpdate: ${prevProps.text} -> ${this.props.text}`,
589 );
590 }
591
592 componentWillUnmount() {
593 Scheduler.log(`componentWillUnmount: ${this.props.text}`);
594 }
595
596 shouldComponentUpdate(nextProps) {
597 Scheduler.log(
598 `shouldComponentUpdate: ${this.props.text} -> ${nextProps.text}`,
599 );
600 return true;
601 }
602
603 getSnapshotBeforeUpdate(prevProps) {
604 Scheduler.log(
605 `getSnapshotBeforeUpdate: ${prevProps.text} -> ${this.props.text}`,
606 );
607 return null;
608 }
609
610 render() {
611 return <Text text={this.props.text + this.props.num} />;
612 }
613 }
614
615 const LazyClass = lazy(() => fakeImport(C));
616
617 const root = ReactTestRenderer.create(
618 <Suspense fallback={<Text text="Loading..." />}>
619 <LazyClass num={1} />
620 </Suspense>,
621 {
622 unstable_isConcurrent: true,
623 },
624 );
625
626 await waitForAll(['Loading...']);
627 expect(root).not.toMatchRenderedOutput('A1');
628
629 await act(() => resolveFakeImport(C));
630 assertLog([
631 'constructor: A',
632 'getDerivedStateFromProps: A',
633 'A1',
634 'componentDidMount: A',
635 ]);
636
637 root.update(
638 <Suspense fallback={<Text text="Loading..." />}>
639 <LazyClass num={2} />
640 </Suspense>,
641 );
642 await waitForAll([
643 'getDerivedStateFromProps: A',
644 'shouldComponentUpdate: A -> A',
645 'A2',
646 'getSnapshotBeforeUpdate: A -> A',
647 'componentDidUpdate: A -> A',
648 ]);
649 expect(root).toMatchRenderedOutput('A2');
650
651 root.update(
652 <Suspense fallback={<Text text="Loading..." />}>
653 <LazyClass num={3} />
654 </Suspense>,
655 );
656 await waitForAll([
657 'getDerivedStateFromProps: A',
658 'shouldComponentUpdate: A -> A',
659 'A3',
660 'getSnapshotBeforeUpdate: A -> A',
661 'componentDidUpdate: A -> A',
662 ]);
663 expect(root).toMatchRenderedOutput('A3');
664 });
665
666 it('sets defaultProps for legacy lifecycles', async () => {
667 class C extends React.Component {
668 static defaultProps = {text: 'A'};
669 state = {};
670
671 UNSAFE_componentWillMount() {
672 Scheduler.log(`UNSAFE_componentWillMount: ${this.props.text}`);
673 }
674
675 UNSAFE_componentWillUpdate(nextProps) {
676 Scheduler.log(
677 `UNSAFE_componentWillUpdate: ${this.props.text} -> ${nextProps.text}`,
678 );
679 }
680
681 UNSAFE_componentWillReceiveProps(nextProps) {
682 Scheduler.log(
683 `UNSAFE_componentWillReceiveProps: ${this.props.text} -> ${nextProps.text}`,
684 );
685 }
686
687 render() {
688 return <Text text={this.props.text + this.props.num} />;
689 }
690 }
691
692 const LazyClass = lazy(() => fakeImport(C));
693
694 let root;
695 await act(() => {
696 root = ReactTestRenderer.create(
697 <Suspense fallback={<Text text="Loading..." />}>
698 <LazyClass num={1} />
699 </Suspense>,
700 {unstable_isConcurrent: true},
701 );
702 });
703
704 assertLog(['Loading...']);
705 await waitForAll([]);
706 expect(root).toMatchRenderedOutput('Loading...');
707
708 await resolveFakeImport(C);
709
710 assertLog([]);
711
712 await act(() => {
713 root.update(
714 <Suspense fallback={<Text text="Loading..." />}>
715 <LazyClass num={2} />
716 </Suspense>,
717 );
718 });
719
720 assertLog(['UNSAFE_componentWillMount: A', 'A2']);
721 expect(root).toMatchRenderedOutput('A2');
722
723 await act(() => {
724 root.update(
725 <Suspense fallback={<Text text="Loading..." />}>
726 <LazyClass num={3} />
727 </Suspense>,
728 );
729 });
730 assertLog([
731 'UNSAFE_componentWillReceiveProps: A -> A',
732 'UNSAFE_componentWillUpdate: A -> A',
733 'A3',
734 ]);
735 await waitForAll([]);
736 expect(root).toMatchRenderedOutput('A3');
737 });
738
739 it('throws with a useful error when wrapping invalid type with lazy()', async () => {
740 const BadLazy = lazy(() => fakeImport(42));
741
742 const root = ReactTestRenderer.create(
743 <Suspense fallback={<Text text="Loading..." />}>
744 <BadLazy />
745 </Suspense>,
746 {
747 unstable_isConcurrent: true,
748 },
749 );
750
751 await waitForAll(['Loading...']);
752
753 await resolveFakeImport(42);
754 root.update(
755 <Suspense fallback={<Text text="Loading..." />}>
756 <BadLazy />
757 </Suspense>,
758 );
759 await waitForThrow(
760 'Element type is invalid. Received a promise that resolves to: 42. ' +
761 'Lazy element type must resolve to a class or function.',
762 );
763 });
764
765 it('throws with a useful error when wrapping Fragment with lazy()', async () => {
766 const BadLazy = lazy(() => fakeImport(React.Fragment));
767
768 const root = ReactTestRenderer.create(
769 <Suspense fallback={<Text text="Loading..." />}>
770 <BadLazy />
771 </Suspense>,
772 {
773 unstable_isConcurrent: true,
774 },
775 );
776
777 await waitForAll(['Loading...']);
778
779 await resolveFakeImport(React.Fragment);
780 root.update(
781 <Suspense fallback={<Text text="Loading..." />}>
782 <BadLazy />
783 </Suspense>,
784 );
785 await waitForThrow(
786 'Element type is invalid. Received a promise that resolves to: Fragment. ' +
787 'Lazy element type must resolve to a class or function.',
788 );
789 });
790
791 // @gate !fb
792 it('throws with a useful error when wrapping createPortal with lazy()', async () => {
793 const ReactDOM = require('react-dom');
794 const container = document.createElement('div');
795 const portal = ReactDOM.createPortal(<div />, container);
796 const BadLazy = lazy(() => fakeImport(portal));
797
798 const root = ReactTestRenderer.create(
799 <Suspense fallback={<Text text="Loading..." />}>
800 <BadLazy />
801 </Suspense>,
802 {
803 unstable_isConcurrent: true,
804 },
805 );
806
807 await waitForAll(['Loading...']);
808
809 await resolveFakeImport(portal);
810 root.update(
811 <Suspense fallback={<Text text="Loading..." />}>
812 <BadLazy />
813 </Suspense>,
814 );
815 await waitForThrow(
816 'Element type is invalid. Received a promise that resolves to: Portal. ' +
817 'Lazy element type must resolve to a class or function.',
818 );
819 });
820
821 it('throws with a useful error when wrapping Profiler with lazy()', async () => {
822 const BadLazy = lazy(() => fakeImport(React.Profiler));
823
824 const root = ReactTestRenderer.create(
825 <Suspense fallback={<Text text="Loading..." />}>
826 <BadLazy />
827 </Suspense>,
828 {
829 unstable_isConcurrent: true,
830 },
831 );
832
833 await waitForAll(['Loading...']);
834
835 await resolveFakeImport(React.Profiler);
836 root.update(
837 <Suspense fallback={<Text text="Loading..." />}>
838 <BadLazy />
839 </Suspense>,
840 );
841 await waitForThrow(
842 'Element type is invalid. Received a promise that resolves to: Profiler. ' +
843 'Lazy element type must resolve to a class or function.',
844 );
845 });
846
847 it('throws with a useful error when wrapping StrictMode with lazy()', async () => {
848 const BadLazy = lazy(() => fakeImport(React.StrictMode));
849
850 const root = ReactTestRenderer.create(
851 <Suspense fallback={<Text text="Loading..." />}>
852 <BadLazy />
853 </Suspense>,
854 {
855 unstable_isConcurrent: true,
856 },
857 );
858
859 await waitForAll(['Loading...']);
860
861 await resolveFakeImport(React.StrictMode);
862 root.update(
863 <Suspense fallback={<Text text="Loading..." />}>
864 <BadLazy />
865 </Suspense>,
866 );
867 await waitForThrow(
868 'Element type is invalid. Received a promise that resolves to: StrictMode. ' +
869 'Lazy element type must resolve to a class or function.',
870 );
871 });
872
873 it('throws with a useful error when wrapping Suspense with lazy()', async () => {
874 const BadLazy = lazy(() => fakeImport(React.Suspense));
875
876 const root = ReactTestRenderer.create(
877 <Suspense fallback={<Text text="Loading..." />}>
878 <BadLazy />
879 </Suspense>,
880 {
881 unstable_isConcurrent: true,
882 },
883 );
884
885 await waitForAll(['Loading...']);
886
887 await resolveFakeImport(React.Suspense);
888 root.update(
889 <Suspense fallback={<Text text="Loading..." />}>
890 <BadLazy />
891 </Suspense>,
892 );
893 await waitForThrow(
894 'Element type is invalid. Received a promise that resolves to: Suspense. ' +
895 'Lazy element type must resolve to a class or function.',
896 );
897 });
898
899 it('renders a lazy context provider without value prop', async () => {
900 // Context providers work when wrapped in lazy()
901 const Context = React.createContext('default');
902 const LazyProvider = lazy(() => fakeImport(Context));
903
904 function ConsumerText() {
905 return <Text text={React.useContext(Context)} />;
906 }
907
908 const root = ReactTestRenderer.create(
909 <Suspense fallback={<Text text="Loading..." />}>
910 <LazyProvider value="provided">
911 <ConsumerText />
912 </LazyProvider>
913 </Suspense>,
914 {
915 unstable_isConcurrent: true,
916 },
917 );
918
919 await waitForAll(['Loading...']);
920
921 await act(() => resolveFakeImport(Context));
922 assertLog(['provided']);
923 expect(root).toMatchRenderedOutput('provided');
924 });
925
926 it('throws with a useful error when wrapping Context.Consumer with lazy()', async () => {
927 const Context = React.createContext(null);
928 const BadLazy = lazy(() => fakeImport(Context.Consumer));
929
930 const root = ReactTestRenderer.create(
931 <Suspense fallback={<Text text="Loading..." />}>
932 <BadLazy />
933 </Suspense>,
934 {
935 unstable_isConcurrent: true,
936 },
937 );
938
939 await waitForAll(['Loading...']);
940
941 await resolveFakeImport(Context.Consumer);
942 root.update(
943 <Suspense fallback={<Text text="Loading..." />}>
944 <BadLazy />
945 </Suspense>,
946 );
947 await waitForThrow(
948 'Element type is invalid. Received a promise that resolves to: Context.Consumer. ' +
949 'Lazy element type must resolve to a class or function.',
950 );
951 });
952
953 // @gate enableSuspenseList
954 it('throws with a useful error when wrapping SuspenseList with lazy()', async () => {
955 const BadLazy = lazy(() => fakeImport(React.unstable_SuspenseList));
956
957 const root = ReactTestRenderer.create(
958 <Suspense fallback={<Text text="Loading..." />}>
959 <BadLazy />
960 </Suspense>,
961 {
962 unstable_isConcurrent: true,
963 },
964 );
965
966 await waitForAll(['Loading...']);
967
968 await resolveFakeImport(React.unstable_SuspenseList);
969 root.update(
970 <Suspense fallback={<Text text="Loading..." />}>
971 <BadLazy />
972 </Suspense>,
973 );
974 await waitForThrow(
975 'Element type is invalid. Received a promise that resolves to: SuspenseList. ' +
976 'Lazy element type must resolve to a class or function.',
977 );
978 });
979
980 // @gate enableViewTransition
981 it('throws with a useful error when wrapping ViewTransition with lazy()', async () => {
982 const BadLazy = lazy(() => fakeImport(React.ViewTransition));
983
984 const root = ReactTestRenderer.create(
985 <Suspense fallback={<Text text="Loading..." />}>
986 <BadLazy />
987 </Suspense>,
988 {
989 unstable_isConcurrent: true,
990 },
991 );
992
993 await waitForAll(['Loading...']);
994
995 await resolveFakeImport(React.ViewTransition);
996 root.update(
997 <Suspense fallback={<Text text="Loading..." />}>
998 <BadLazy />
999 </Suspense>,
1000 );
1001 await waitForThrow(
1002 'Element type is invalid. Received a promise that resolves to: ViewTransition. ' +
1003 'Lazy element type must resolve to a class or function.',
1004 );
1005 });
1006
1007 it('throws with a useful error when wrapping Activity with lazy()', async () => {
1008 const BadLazy = lazy(() => fakeImport(React.Activity));
1009
1010 const root = ReactTestRenderer.create(
1011 <Suspense fallback={<Text text="Loading..." />}>
1012 <BadLazy />
1013 </Suspense>,
1014 {
1015 unstable_isConcurrent: true,
1016 },
1017 );
1018
1019 await waitForAll(['Loading...']);
1020
1021 await resolveFakeImport(React.Activity);
1022 root.update(
1023 <Suspense fallback={<Text text="Loading..." />}>
1024 <BadLazy />
1025 </Suspense>,
1026 );
1027 await waitForThrow(
1028 'Element type is invalid. Received a promise that resolves to: Activity. ' +
1029 'Lazy element type must resolve to a class or function.',
1030 );
1031 });
1032
1033 // @gate enableTransitionTracing
1034 it('throws with a useful error when wrapping TracingMarker with lazy()', async () => {
1035 const BadLazy = lazy(() => fakeImport(React.unstable_TracingMarker));
1036
1037 const root = ReactTestRenderer.create(
1038 <Suspense fallback={<Text text="Loading..." />}>
1039 <BadLazy />
1040 </Suspense>,
1041 {
1042 unstable_isConcurrent: true,
1043 },
1044 );
1045
1046 await waitForAll(['Loading...']);
1047
1048 await resolveFakeImport(React.unstable_TracingMarker);
1049 root.update(
1050 <Suspense fallback={<Text text="Loading..." />}>
1051 <BadLazy />
1052 </Suspense>,
1053 );
1054 await waitForThrow(
1055 'Element type is invalid. Received a promise that resolves to: TracingMarker. ' +
1056 'Lazy element type must resolve to a class or function.',
1057 );
1058 });
1059
1060 it('throws with a useful error when wrapping lazy() multiple times', async () => {
1061 const Lazy1 = lazy(() => fakeImport(Text));
1062 const Lazy2 = lazy(() => fakeImport(Lazy1));
1063
1064 const root = ReactTestRenderer.create(
1065 <Suspense fallback={<Text text="Loading..." />}>
1066 <Lazy2 text="Hello" />
1067 </Suspense>,
1068 {
1069 unstable_isConcurrent: true,
1070 },
1071 );
1072
1073 await waitForAll(['Loading...']);
1074 expect(root).not.toMatchRenderedOutput('Hello');
1075
1076 await resolveFakeImport(Lazy1);
1077 root.update(
1078 <Suspense fallback={<Text text="Loading..." />}>
1079 <Lazy2 text="Hello" />
1080 </Suspense>,
1081 );
1082 await waitForThrow(
1083 'Element type is invalid. Received a promise that resolves to: [object Object]. ' +
1084 'Lazy element type must resolve to a class or function.' +
1085 (__DEV__
1086 ? ' Did you wrap a component in React.lazy() more than once?'
1087 : ''),
1088 );
1089 });
1090
1091 it('resolves props for function component without defaultProps', async () => {
1092 function Add(props) {
1093 return props.inner + props.outer;
1094 }
1095 const LazyAdd = lazy(() => fakeImport(Add));
1096 const root = ReactTestRenderer.create(
1097 <Suspense fallback={<Text text="Loading..." />}>
1098 <LazyAdd inner="2" outer="2" />
1099 </Suspense>,
1100 {
1101 unstable_isConcurrent: true,
1102 },
1103 );
1104
1105 await waitForAll(['Loading...']);
1106 expect(root).not.toMatchRenderedOutput('22');
1107
1108 // Mount
1109 await act(() => resolveFakeImport(Add));
1110
1111 expect(root).toMatchRenderedOutput('22');
1112
1113 // Update
1114 root.update(
1115 <Suspense fallback={<Text text="Loading..." />}>
1116 <LazyAdd inner={false} outer={false} />
1117 </Suspense>,
1118 );
1119 await waitForAll([]);
1120 expect(root).toMatchRenderedOutput('0');
1121 });
1122
1123 it('resolves props for class component with defaultProps', async () => {
1124 class Add extends React.Component {
1125 render() {
1126 expect(this.props.innerWithDefault).toBe(42);
1127 return this.props.inner + this.props.outer;
1128 }
1129 }
1130 Add.defaultProps = {
1131 innerWithDefault: 42,
1132 };
1133 const LazyAdd = lazy(() => fakeImport(Add));
1134 const root = ReactTestRenderer.create(
1135 <Suspense fallback={<Text text="Loading..." />}>
1136 <LazyAdd inner="2" outer="2" />
1137 </Suspense>,
1138 {
1139 unstable_isConcurrent: true,
1140 },
1141 );
1142
1143 await waitForAll(['Loading...']);
1144 expect(root).not.toMatchRenderedOutput('22');
1145
1146 // Mount
1147 await act(() => resolveFakeImport(Add));
1148
1149 expect(root).toMatchRenderedOutput('22');
1150
1151 // Update
1152 root.update(
1153 <Suspense fallback={<Text text="Loading..." />}>
1154 <LazyAdd inner={false} outer={false} />
1155 </Suspense>,
1156 );
1157 await waitForAll([]);
1158 expect(root).toMatchRenderedOutput('0');
1159 });
1160
1161 it('resolves props for class component without defaultProps', async () => {
1162 class Add extends React.Component {
1163 render() {
1164 return this.props.inner + this.props.outer;
1165 }
1166 }
1167 const LazyAdd = lazy(() => fakeImport(Add));
1168 const root = ReactTestRenderer.create(
1169 <Suspense fallback={<Text text="Loading..." />}>
1170 <LazyAdd inner="2" outer="2" />
1171 </Suspense>,
1172 {
1173 unstable_isConcurrent: true,
1174 },
1175 );
1176
1177 await waitForAll(['Loading...']);
1178 expect(root).not.toMatchRenderedOutput('22');
1179
1180 // Mount
1181 await act(() => resolveFakeImport(Add));
1182
1183 expect(root).toMatchRenderedOutput('22');
1184
1185 // Update
1186 root.update(
1187 <Suspense fallback={<Text text="Loading..." />}>
1188 <LazyAdd inner={false} outer={false} />
1189 </Suspense>,
1190 );
1191 await waitForAll([]);
1192 expect(root).toMatchRenderedOutput('0');
1193 });
1194
1195 it('resolves props for forwardRef component without defaultProps', async () => {
1196 const Add = React.forwardRef((props, ref) => {
1197 return props.inner + props.outer;
1198 });
1199 Add.displayName = 'Add';
1200
1201 const LazyAdd = lazy(() => fakeImport(Add));
1202 const root = ReactTestRenderer.create(
1203 <Suspense fallback={<Text text="Loading..." />}>
1204 <LazyAdd inner="2" outer="2" />
1205 </Suspense>,
1206 {
1207 unstable_isConcurrent: true,
1208 },
1209 );
1210
1211 await waitForAll(['Loading...']);
1212 expect(root).not.toMatchRenderedOutput('22');
1213
1214 // Mount
1215 await act(() => resolveFakeImport(Add));
1216
1217 expect(root).toMatchRenderedOutput('22');
1218
1219 // Update
1220 root.update(
1221 <Suspense fallback={<Text text="Loading..." />}>
1222 <LazyAdd inner={false} outer={false} />
1223 </Suspense>,
1224 );
1225 await waitForAll([]);
1226 expect(root).toMatchRenderedOutput('0');
1227 });
1228
1229 it('resolves props for outer memo component without defaultProps', async () => {
1230 let Add = props => {
1231 return props.inner + props.outer;
1232 };
1233 Add = React.memo(Add);
1234 const LazyAdd = lazy(() => fakeImport(Add));
1235 const root = ReactTestRenderer.create(
1236 <Suspense fallback={<Text text="Loading..." />}>
1237 <LazyAdd inner="2" outer="2" />
1238 </Suspense>,
1239 {
1240 unstable_isConcurrent: true,
1241 },
1242 );
1243
1244 await waitForAll(['Loading...']);
1245 expect(root).not.toMatchRenderedOutput('22');
1246
1247 // Mount
1248 await act(() => resolveFakeImport(Add));
1249
1250 expect(root).toMatchRenderedOutput('22');
1251
1252 // Update
1253 root.update(
1254 <Suspense fallback={<Text text="Loading..." />}>
1255 <LazyAdd inner={false} outer={false} />
1256 </Suspense>,
1257 );
1258 await waitForAll([]);
1259 expect(root).toMatchRenderedOutput('0');
1260 });
1261
1262 it('resolves props for inner memo component without defaultProps', async () => {
1263 const Add = props => {
1264 return props.inner + props.outer;
1265 };
1266 Add.displayName = 'Add';
1267 const LazyAdd = lazy(() => fakeImport(Add));
1268 const root = ReactTestRenderer.create(
1269 <Suspense fallback={<Text text="Loading..." />}>
1270 <LazyAdd inner="2" outer="2" />
1271 </Suspense>,
1272 {
1273 unstable_isConcurrent: true,
1274 },
1275 );
1276
1277 await waitForAll(['Loading...']);
1278 expect(root).not.toMatchRenderedOutput('22');
1279
1280 // Mount
1281 await act(() => resolveFakeImport(Add));
1282
1283 expect(root).toMatchRenderedOutput('22');
1284
1285 // Update
1286 root.update(
1287 <Suspense fallback={<Text text="Loading..." />}>
1288 <LazyAdd inner={false} outer={false} />
1289 </Suspense>,
1290 );
1291 await waitForAll([]);
1292 expect(root).toMatchRenderedOutput('0');
1293 });
1294
1295 it('includes lazy-loaded component in warning stack', async () => {
1296 const Foo = props => <div>{[<Text text="A" />, <Text text="B" />]}</div>;
1297 const LazyFoo = lazy(() => {
1298 Scheduler.log('Started loading');
1299 return fakeImport(Foo);
1300 });
1301
1302 const root = ReactTestRenderer.create(
1303 <Suspense fallback={<Text text="Loading..." />}>
1304 <LazyFoo />
1305 </Suspense>,
1306 {
1307 unstable_isConcurrent: true,
1308 },
1309 );
1310
1311 await waitForAll(['Started loading', 'Loading...']);
1312 expect(root).not.toMatchRenderedOutput(<div>AB</div>);
1313
1314 await act(() => resolveFakeImport(Foo));
1315 assertLog(['A', 'B']);
1316 assertConsoleErrorDev([
1317 'Each child in a list should have a unique "key" prop.\n' +
1318 '\n' +
1319 'Check the render method of `Foo`. ' +
1320 'See https://react.dev/link/warning-keys for more information.\n' +
1321 ' in Foo (at **)',
1322 ]);
1323 expect(root).toMatchRenderedOutput(<div>AB</div>);
1324 });
1325
1326 it('supports class and forwardRef components', async () => {
1327 class Foo extends React.Component {
1328 render() {
1329 return <Text text="Foo" />;
1330 }
1331 }
1332 const LazyClass = lazy(() => {
1333 return fakeImport(Foo);
1334 });
1335
1336 class Bar extends React.Component {
1337 render() {
1338 return <Text text="Bar" />;
1339 }
1340 }
1341 const ForwardRefBar = React.forwardRef((props, ref) => {
1342 Scheduler.log('forwardRef');
1343 return <Bar ref={ref} />;
1344 });
1345
1346 const LazyForwardRef = lazy(() => {
1347 return fakeImport(ForwardRefBar);
1348 });
1349
1350 const ref = React.createRef();
1351 const root = ReactTestRenderer.create(
1352 <Suspense fallback={<Text text="Loading..." />}>
1353 <LazyClass />
1354 <LazyForwardRef ref={ref} />
1355 </Suspense>,
1356 {
1357 unstable_isConcurrent: true,
1358 },
1359 );
1360
1361 await waitForAll(['Loading...']);
1362 expect(root).not.toMatchRenderedOutput('FooBar');
1363 expect(ref.current).toBe(null);
1364
1365 await act(() => resolveFakeImport(Foo));
1366 assertLog(['Foo']);
1367
1368 await act(() => resolveFakeImport(ForwardRefBar));
1369 assertLog(['Foo', 'forwardRef', 'Bar']);
1370 expect(root).toMatchRenderedOutput('FooBar');
1371 expect(ref.current).not.toBe(null);
1372 });
1373
1374 it('should error with a component stack naming the resolved component', async () => {
1375 let componentStackMessage;
1376
1377 function ResolvedText() {
1378 throw new Error('oh no');
1379 }
1380 const LazyText = lazy(() => fakeImport(ResolvedText));
1381
1382 class ErrorBoundary extends React.Component {
1383 state = {error: null};
1384
1385 componentDidCatch(error, errMessage) {
1386 componentStackMessage = normalizeCodeLocInfo(errMessage.componentStack);
1387 this.setState({
1388 error,
1389 });
1390 }
1391
1392 render() {
1393 return this.state.error ? null : this.props.children;
1394 }
1395 }
1396
1397 ReactTestRenderer.create(
1398 <ErrorBoundary>
1399 <Suspense fallback={<Text text="Loading..." />}>
1400 <LazyText text="Hi" />
1401 </Suspense>
1402 </ErrorBoundary>,
1403 {unstable_isConcurrent: true},
1404 );
1405
1406 await waitForAll(['Loading...']);
1407
1408 await act(() => resolveFakeImport(ResolvedText));
1409 assertLog([]);
1410
1411 expect(componentStackMessage).toContain('in ResolvedText');
1412 });
1413
1414 it('should error with a component stack containing Lazy if unresolved', async () => {
1415 let componentStackMessage;
1416
1417 const LazyText = lazy(() => ({
1418 then(resolve, reject) {
1419 reject(new Error('oh no'));
1420 },
1421 }));
1422
1423 class ErrorBoundary extends React.Component {
1424 state = {error: null};
1425
1426 componentDidCatch(error, errMessage) {
1427 componentStackMessage = normalizeCodeLocInfo(errMessage.componentStack);
1428 this.setState({
1429 error,
1430 });
1431 }
1432
1433 render() {
1434 return this.state.error ? null : this.props.children;
1435 }
1436 }
1437
1438 await act(() => {
1439 ReactTestRenderer.create(
1440 <ErrorBoundary>
1441 <Suspense fallback={<Text text="Loading..." />}>
1442 <LazyText text="Hi" />
1443 </Suspense>
1444 </ErrorBoundary>,
1445 {unstable_isConcurrent: true},
1446 );
1447 });
1448
1449 assertLog([]);
1450
1451 expect(componentStackMessage).toContain('in Lazy');
1452 });
1453
1454 it('mount and reorder lazy types', async () => {
1455 class Child extends React.Component {
1456 componentWillUnmount() {
1457 Scheduler.log('Did unmount: ' + this.props.label);
1458 }
1459 componentDidMount() {
1460 Scheduler.log('Did mount: ' + this.props.label);
1461 }
1462 componentDidUpdate() {
1463 Scheduler.log('Did update: ' + this.props.label);
1464 }
1465 render() {
1466 return <Text text={this.props.label} />;
1467 }
1468 }
1469
1470 function ChildA({lowerCase}) {
1471 return <Child label={lowerCase ? 'a' : 'A'} />;
1472 }
1473
1474 function ChildB({lowerCase}) {
1475 return <Child label={lowerCase ? 'b' : 'B'} />;
1476 }
1477
1478 const LazyChildA = lazy(() => {
1479 Scheduler.log('Init A');
1480 return fakeImport(ChildA);
1481 });
1482 const LazyChildB = lazy(() => {
1483 Scheduler.log('Init B');
1484 return fakeImport(ChildB);
1485 });
1486 const LazyChildA2 = lazy(() => {
1487 Scheduler.log('Init A2');
1488 return fakeImport(ChildA);
1489 });
1490 let resolveB2;
1491 const LazyChildB2 = lazy(() => {
1492 Scheduler.log('Init B2');
1493 return new Promise(r => {
1494 resolveB2 = r;
1495 });
1496 });
1497
1498 function Parent({swap}) {
1499 return (
1500 <Suspense fallback={<Text text="Outer..." />}>
1501 <Suspense fallback={<Text text="Loading..." />}>
1502 {swap
1503 ? [
1504 <LazyChildB2 key="B" lowerCase={true} />,
1505 <LazyChildA2 key="A" lowerCase={true} />,
1506 ]
1507 : [<LazyChildA key="A" />, <LazyChildB key="B" />]}
1508 </Suspense>
1509 </Suspense>
1510 );
1511 }
1512
1513 const root = ReactTestRenderer.create(<Parent swap={false} />, {
1514 unstable_isConcurrent: true,
1515 });
1516
1517 await waitForAll([
1518 'Init A',
1519 'Loading...',
1520 // pre-warming
1521 'Init B',
1522 ]);
1523 expect(root).not.toMatchRenderedOutput('AB');
1524
1525 await act(() => resolveFakeImport(ChildA));
1526 assertLog(['A']);
1527
1528 await act(() => resolveFakeImport(ChildB));
1529 assertLog(['A', 'B', 'Did mount: A', 'Did mount: B']);
1530 expect(root).toMatchRenderedOutput('AB');
1531
1532 // Swap the position of A and B
1533 root.update(<Parent swap={true} />);
1534 await waitForAll([
1535 'Init B2',
1536 'Loading...',
1537 'Did unmount: A',
1538 'Did unmount: B',
1539 ]);
1540
1541 // The suspense boundary should've triggered now.
1542 expect(root).toMatchRenderedOutput('Loading...');
1543 await act(() => resolveB2({default: ChildB}));
1544
1545 // We need to flush to trigger the second one to load.
1546 assertLog(['Init A2', 'b', 'a', 'Did mount: b', 'Did mount: a']);
1547 expect(root).toMatchRenderedOutput('ba');
1548 });
1549
1550 it('mount and reorder lazy elements', async () => {
1551 class Child extends React.Component {
1552 componentDidMount() {
1553 Scheduler.log('Did mount: ' + this.props.label);
1554 }
1555 componentDidUpdate() {
1556 Scheduler.log('Did update: ' + this.props.label);
1557 }
1558 render() {
1559 return <Text text={this.props.label} />;
1560 }
1561 }
1562
1563 const ChildA = <Child key="A" label="A" />;
1564 const lazyChildA = lazy(() => {
1565 Scheduler.log('Init A');
1566 return fakeImport(ChildA);
1567 });
1568 const ChildB = <Child key="B" label="B" />;
1569 const lazyChildB = lazy(() => {
1570 Scheduler.log('Init B');
1571 return fakeImport(ChildB);
1572 });
1573 const ChildA2 = <Child key="A" label="a" />;
1574 const lazyChildA2 = lazy(() => {
1575 Scheduler.log('Init A2');
1576 return fakeImport(ChildA2);
1577 });
1578 const ChildB2 = <Child key="B" label="b" />;
1579 const lazyChildB2 = lazy(() => {
1580 Scheduler.log('Init B2');
1581 return fakeImport(ChildB2);
1582 });
1583
1584 function Parent({swap}) {
1585 return (
1586 <Suspense fallback={<Text text="Loading..." />}>
1587 {swap ? [lazyChildB2, lazyChildA2] : [lazyChildA, lazyChildB]}
1588 </Suspense>
1589 );
1590 }
1591
1592 const root = ReactTestRenderer.create(<Parent swap={false} />, {
1593 unstable_isConcurrent: true,
1594 });
1595
1596 await waitForAll(['Init A', 'Loading...']);
1597 expect(root).not.toMatchRenderedOutput('AB');
1598
1599 await act(() => resolveFakeImport(ChildA));
1600 // We need to flush to trigger the B to load.
1601 await assertLog(['Init B']);
1602 await act(() => resolveFakeImport(ChildB));
1603 assertLog(['A', 'B', 'Did mount: A', 'Did mount: B']);
1604 expect(root).toMatchRenderedOutput('AB');
1605
1606 // Swap the position of A and B
1607 React.startTransition(() => {
1608 root.update(<Parent swap={true} />);
1609 });
1610 await waitForAll(['Init B2', 'Loading...']);
1611 await act(() => resolveFakeImport(ChildB2));
1612 // We need to flush to trigger the second one to load.
1613 assertLog(['Init A2', 'Loading...']);
1614 await act(() => resolveFakeImport(ChildA2));
1615 assertLog(['b', 'a', 'Did update: b', 'Did update: a']);
1616 expect(root).toMatchRenderedOutput('ba');
1617 });
1618
1619 describe('legacy mode', () => {
1620 // @gate !disableLegacyMode
1621 it('mount and reorder lazy elements (legacy mode)', async () => {
1622 class Child extends React.Component {
1623 componentDidMount() {
1624 Scheduler.log('Did mount: ' + this.props.label);
1625 }
1626 componentDidUpdate() {
1627 Scheduler.log('Did update: ' + this.props.label);
1628 }
1629 render() {
1630 return <Text text={this.props.label} />;
1631 }
1632 }
1633
1634 const ChildA = <Child key="A" label="A" />;
1635 const lazyChildA = lazy(() => {
1636 Scheduler.log('Init A');
1637 return fakeImport(ChildA);
1638 });
1639 const ChildB = <Child key="B" label="B" />;
1640 const lazyChildB = lazy(() => {
1641 Scheduler.log('Init B');
1642 return fakeImport(ChildB);
1643 });
1644 const ChildA2 = <Child key="A" label="a" />;
1645 const lazyChildA2 = lazy(() => {
1646 Scheduler.log('Init A2');
1647 return fakeImport(ChildA2);
1648 });
1649 const ChildB2 = <Child key="B" label="b" />;
1650 const lazyChildB2 = lazy(() => {
1651 Scheduler.log('Init B2');
1652 return fakeImport(ChildB2);
1653 });
1654
1655 function Parent({swap}) {
1656 return (
1657 <Suspense fallback={<Text text="Loading..." />}>
1658 {swap ? [lazyChildB2, lazyChildA2] : [lazyChildA, lazyChildB]}
1659 </Suspense>
1660 );
1661 }
1662
1663 const root = ReactTestRenderer.create(<Parent swap={false} />, {
1664 unstable_isConcurrent: false,
1665 });
1666
1667 assertLog(['Init A', 'Loading...']);
1668 expect(root).not.toMatchRenderedOutput('AB');
1669
1670 await resolveFakeImport(ChildA);
1671 // We need to flush to trigger the B to load.
1672 await waitForAll(['Init B']);
1673 await resolveFakeImport(ChildB);
1674
1675 await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']);
1676 expect(root).toMatchRenderedOutput('AB');
1677
1678 // Swap the position of A and B
1679 root.update(<Parent swap={true} />);
1680 assertLog(['Init B2', 'Loading...']);
1681 await resolveFakeImport(ChildB2);
1682 // We need to flush to trigger the second one to load.
1683 await waitForAll(['Init A2']);
1684 await resolveFakeImport(ChildA2);
1685
1686 await waitForAll(['b', 'a', 'Did update: b', 'Did update: a']);
1687 expect(root).toMatchRenderedOutput('ba');
1688 });
1689
1690 // @gate !disableLegacyMode
1691 it('mount and reorder lazy types (legacy mode)', async () => {
1692 class Child extends React.Component {
1693 componentDidMount() {
1694 Scheduler.log('Did mount: ' + this.props.label);
1695 }
1696 componentDidUpdate() {
1697 Scheduler.log('Did update: ' + this.props.label);
1698 }
1699 render() {
1700 return <Text text={this.props.label} />;
1701 }
1702 }
1703
1704 function ChildA({lowerCase}) {
1705 return <Child label={lowerCase ? 'a' : 'A'} />;
1706 }
1707
1708 function ChildB({lowerCase}) {
1709 return <Child label={lowerCase ? 'b' : 'B'} />;
1710 }
1711
1712 const LazyChildA = lazy(() => {
1713 Scheduler.log('Init A');
1714 return fakeImport(ChildA);
1715 });
1716 const LazyChildB = lazy(() => {
1717 Scheduler.log('Init B');
1718 return fakeImport(ChildB);
1719 });
1720 const LazyChildA2 = lazy(() => {
1721 Scheduler.log('Init A2');
1722 return fakeImport(ChildA);
1723 });
1724 const LazyChildB2 = lazy(() => {
1725 Scheduler.log('Init B2');
1726 return fakeImport(ChildB);
1727 });
1728
1729 function Parent({swap}) {
1730 return (
1731 <Suspense fallback={<Text text="Outer..." />}>
1732 <Suspense fallback={<Text text="Loading..." />}>
1733 {swap
1734 ? [
1735 <LazyChildB2 key="B" lowerCase={true} />,
1736 <LazyChildA2 key="A" lowerCase={true} />,
1737 ]
1738 : [<LazyChildA key="A" />, <LazyChildB key="B" />]}
1739 </Suspense>
1740 </Suspense>
1741 );
1742 }
1743
1744 const root = ReactTestRenderer.create(<Parent swap={false} />, {
1745 unstable_isConcurrent: false,
1746 });
1747
1748 assertLog(['Init A', 'Init B', 'Loading...']);
1749 expect(root).not.toMatchRenderedOutput('AB');
1750
1751 await resolveFakeImport(ChildA);
1752 await resolveFakeImport(ChildB);
1753
1754 await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']);
1755 expect(root).toMatchRenderedOutput('AB');
1756
1757 // Swap the position of A and B
1758 root.update(<Parent swap={true} />);
1759 assertLog(['Init B2', 'Loading...']);
1760 await waitForAll(['Init A2', 'b', 'a', 'Did update: b', 'Did update: a']);
1761 expect(root).toMatchRenderedOutput('ba');
1762 });
1763 });
1764 });