1
+/**
2
+ * Copyright (c) Facebook, Inc. and its 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 React;
11
+let ReactNoop;
12
+let Scheduler;
13
+let Suspense;
14
+let getCacheForType;
15
+let caches;
16
+let seededCache;
17
+let ErrorBoundary;
18
+
19
+describe('ReactSuspenseEffectsSemantics', () => {
20
+ beforeEach(() => {
21
+ jest.resetModules();
22
+
23
+ React = require('react');
24
+ ReactNoop = require('react-noop-renderer');
25
+ Scheduler = require('scheduler');
26
+ Suspense = React.Suspense;
27
+
28
+ getCacheForType = React.unstable_getCacheForType;
29
+
30
+ caches = [];
31
+ seededCache = null;
32
+
33
+ ErrorBoundary = class extends React.Component {
34
+ state = {error: null};
35
+ componentDidCatch(error) {
36
+ this.setState({error});
37
+ }
38
+ render() {
39
+ if (this.state.error) {
40
+ Scheduler.unstable_yieldValue('ErrorBoundary render: catch');
41
+ return this.props.fallback;
42
+ }
43
+ Scheduler.unstable_yieldValue('ErrorBoundary render: try');
44
+ return this.props.children;
45
+ }
46
+ };
47
+ });
48
+
49
+ function createTextCache() {
50
+ if (seededCache !== null) {
51
+ // Trick to seed a cache before it exists.
52
+ // TODO: Need a built-in API to seed data before the initial render (i.e.
53
+ // not a refresh because nothing has mounted yet).
54
+ const cache = seededCache;
55
+ seededCache = null;
56
+ return cache;
57
+ }
58
+
59
+ const data = new Map();
60
+ const version = caches.length + 1;
61
+ const cache = {
62
+ version,
63
+ data,
64
+ resolve(text) {
65
+ const record = data.get(text);
66
+ if (record === undefined) {
67
+ const newRecord = {
68
+ status: 'resolved',
69
+ value: text,
70
+ };
71
+ data.set(text, newRecord);
72
+ } else if (record.status === 'pending') {
73
+ const thenable = record.value;
74
+ record.status = 'resolved';
75
+ record.value = text;
76
+ thenable.pings.forEach(t => t());
77
+ }
78
+ },
79
+ reject(text, error) {
80
+ const record = data.get(text);
81
+ if (record === undefined) {
82
+ const newRecord = {
83
+ status: 'rejected',
84
+ value: error,
85
+ };
86
+ data.set(text, newRecord);
87
+ } else if (record.status === 'pending') {
88
+ const thenable = record.value;
89
+ record.status = 'rejected';
90
+ record.value = error;
91
+ thenable.pings.forEach(t => t());
92
+ }
93
+ },
94
+ };
95
+ caches.push(cache);
96
+ return cache;
97
+ }
98
+
99
+ function readText(text) {
100
+ const textCache = getCacheForType(createTextCache);
101
+ const record = textCache.data.get(text);
102
+ if (record !== undefined) {
103
+ switch (record.status) {
104
+ case 'pending':
105
+ Scheduler.unstable_yieldValue(`Suspend:${text}`);
106
+ throw record.value;
107
+ case 'rejected':
108
+ Scheduler.unstable_yieldValue(`Error:${text}`);
109
+ throw record.value;
110
+ case 'resolved':
111
+ return textCache.version;
112
+ }
113
+ } else {
114
+ Scheduler.unstable_yieldValue(`Suspend:${text}`);
115
+
116
+ const thenable = {
117
+ pings: [],
118
+ then(resolve) {
119
+ if (newRecord.status === 'pending') {
120
+ thenable.pings.push(resolve);
121
+ } else {
122
+ Promise.resolve().then(() => resolve(newRecord.value));
123
+ }
124
+ },
125
+ };
126
+
127
+ const newRecord = {
128
+ status: 'pending',
129
+ value: thenable,
130
+ };
131
+ textCache.data.set(text, newRecord);
132
+
133
+ throw thenable;
134
+ }
135
+ }
136
+
137
+ function Text({children = null, text}) {
138
+ Scheduler.unstable_yieldValue(`Text:${text} render`);
139
+ React.useLayoutEffect(() => {
140
+ Scheduler.unstable_yieldValue(`Text:${text} create layout`);
141
+ return () => {
142
+ Scheduler.unstable_yieldValue(`Text:${text} destroy layout`);
143
+ };
144
+ }, []);
145
+ React.useEffect(() => {
146
+ Scheduler.unstable_yieldValue(`Text:${text} create passive`);
147
+ return () => {
148
+ Scheduler.unstable_yieldValue(`Text:${text} destroy passive`);
149
+ };
150
+ }, []);
151
+ return <span prop={text}>{children}</span>;
152
+ }
153
+
154
+ function AsyncText({children = null, text}) {
155
+ readText(text);
156
+ Scheduler.unstable_yieldValue(`AsyncText:${text} render`);
157
+ React.useLayoutEffect(() => {
158
+ Scheduler.unstable_yieldValue(`AsyncText:${text} create layout`);
159
+ return () => {
160
+ Scheduler.unstable_yieldValue(`AsyncText:${text} destroy layout`);
161
+ };
162
+ }, []);
163
+ React.useEffect(() => {
164
+ Scheduler.unstable_yieldValue(`AsyncText:${text} create passive`);
165
+ return () => {
166
+ Scheduler.unstable_yieldValue(`AsyncText:${text} destroy passive`);
167
+ };
168
+ }, []);
169
+ return <span prop={text}>{children}</span>;
170
+ }
171
+
172
+ function resolveMostRecentTextCache(text) {
173
+ if (caches.length === 0) {
174
+ throw Error('Cache does not exist.');
175
+ } else {
176
+ // Resolve the most recently created cache. An older cache can by
177
+ // resolved with `caches[index].resolve(text)`.
178
+ caches[caches.length - 1].resolve(text);
179
+ }
180
+ }
181
+
182
+ const resolveText = resolveMostRecentTextCache;
183
+
184
+ function span(prop, children = []) {
185
+ return {type: 'span', children, prop, hidden: false};
186
+ }
187
+
188
+ function spanHidden(prop, children = []) {
189
+ return {type: 'span', children, prop, hidden: true};
190
+ }
191
+
192
+ function advanceTimers(ms) {
193
+ // Note: This advances Jest's virtual time but not React's. Use
194
+ // ReactNoop.expire for that.
195
+ if (typeof ms !== 'number') {
196
+ throw new Error('Must specify ms');
197
+ }
198
+ jest.advanceTimersByTime(ms);
199
+ // Wait until the end of the current tick
200
+ // We cannot use a timer since we're faking them
201
+ return Promise.resolve().then(() => {});
202
+ }
203
+
204
+ describe('when a component suspends during initial mount', () => {
205
+ // @gate enableCache
206
+ it('should not change behavior in concurrent mode', async () => {
207
+ class ClassText extends React.Component {
208
+ componentDidMount() {
209
+ const {text} = this.props;
210
+ Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
211
+ }
212
+ componentDidUpdate() {
213
+ const {text} = this.props;
214
+ Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
215
+ }
216
+ componentWillUnmount() {
217
+ const {text} = this.props;
218
+ Scheduler.unstable_yieldValue(
219
+ `ClassText:${text} componentWillUnmount`,
220
+ );
221
+ }
222
+ render() {
223
+ const {children, text} = this.props;
224
+ Scheduler.unstable_yieldValue(`ClassText:${text} render`);
225
+ return <span prop={text}>{children}</span>;
226
+ }
227
+ }
228
+
229
+ function App({children = null}) {
230
+ Scheduler.unstable_yieldValue('App render');
231
+ React.useLayoutEffect(() => {
232
+ Scheduler.unstable_yieldValue('App create layout');
233
+ return () => {
234
+ Scheduler.unstable_yieldValue('App destroy layout');
235
+ };
236
+ }, []);
237
+ React.useEffect(() => {
238
+ Scheduler.unstable_yieldValue('App create passive');
239
+ return () => {
240
+ Scheduler.unstable_yieldValue('App destroy passive');
241
+ };
242
+ }, []);
243
+ return (
244
+ <>
245
+ <Suspense fallback={<Text text="Fallback" />}>
246
+ <Text text="Inside:Before" />
247
+ {children}
248
+ <ClassText text="Inside:After" />
249
+ </Suspense>
250
+ <Text text="Outside" />
251
+ </>
252
+ );
253
+ }
254
+
255
+ // Mount and suspend.
256
+ await ReactNoop.act(async () => {
257
+ ReactNoop.render(
258
+ <App>
259
+ <AsyncText text="Async" ms={1000} />
260
+ </App>,
261
+ );
262
+ });
263
+ expect(Scheduler).toHaveYielded([
264
+ 'App render',
265
+ 'Text:Inside:Before render',
266
+ 'Suspend:Async',
267
+ 'ClassText:Inside:After render',
268
+ 'Text:Fallback render',
269
+ 'Text:Outside render',
270
+ 'Text:Fallback create layout',
271
+ 'Text:Outside create layout',
272
+ 'App create layout',
273
+ 'Text:Fallback create passive',
274
+ 'Text:Outside create passive',
275
+ 'App create passive',
276
+ ]);
277
+ expect(ReactNoop.getChildren()).toEqual([
278
+ span('Fallback'),
279
+ span('Outside'),
280
+ ]);
281
+
282
+ // Resolving the suspended resource should
283
+ await ReactNoop.act(async () => {
284
+ await resolveText('Async');
285
+ });
286
+ expect(Scheduler).toHaveYielded([
287
+ 'Text:Inside:Before render',
288
+ 'AsyncText:Async render',
289
+ 'ClassText:Inside:After render',
290
+ 'Text:Fallback destroy layout',
291
+ 'Text:Inside:Before create layout',
292
+ 'AsyncText:Async create layout',
293
+ 'ClassText:Inside:After componentDidMount',
294
+ 'Text:Fallback destroy passive',
295
+ 'Text:Inside:Before create passive',
296
+ 'AsyncText:Async create passive',
297
+ ]);
298
+ expect(ReactNoop.getChildren()).toEqual([
299
+ span('Inside:Before'),
300
+ span('Async'),
301
+ span('Inside:After'),
302
+ span('Outside'),
303
+ ]);
304
+
305
+ await ReactNoop.act(async () => {
306
+ ReactNoop.render(null);
307
+ });
308
+ expect(Scheduler).toHaveYielded([
309
+ 'App destroy layout',
310
+ 'Text:Inside:Before destroy layout',
311
+ 'AsyncText:Async destroy layout',
312
+ 'ClassText:Inside:After componentWillUnmount',
313
+ 'Text:Outside destroy layout',
314
+ 'App destroy passive',
315
+ 'Text:Inside:Before destroy passive',
316
+ 'AsyncText:Async destroy passive',
317
+ 'Text:Outside destroy passive',
318
+ ]);
319
+ expect(ReactNoop.getChildren()).toEqual([]);
320
+ });
321
+
322
+ // @gate enableCache
323
+ it('should not change behavior in sync', async () => {
324
+ class ClassText extends React.Component {
325
+ componentDidMount() {
326
+ const {text} = this.props;
327
+ Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
328
+ }
329
+ componentDidUpdate() {
330
+ const {text} = this.props;
331
+ Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
332
+ }
333
+ componentWillUnmount() {
334
+ const {text} = this.props;
335
+ Scheduler.unstable_yieldValue(
336
+ `ClassText:${text} componentWillUnmount`,
337
+ );
338
+ }
339
+ render() {
340
+ const {children, text} = this.props;
341
+ Scheduler.unstable_yieldValue(`ClassText:${text} render`);
342
+ return <span prop={text}>{children}</span>;
343
+ }
344
+ }
345
+
346
+ function App({children = null}) {
347
+ Scheduler.unstable_yieldValue('App render');
348
+ React.useLayoutEffect(() => {
349
+ Scheduler.unstable_yieldValue('App create layout');
350
+ return () => {
351
+ Scheduler.unstable_yieldValue('App destroy layout');
352
+ };
353
+ }, []);
354
+ React.useEffect(() => {
355
+ Scheduler.unstable_yieldValue('App create passive');
356
+ return () => {
357
+ Scheduler.unstable_yieldValue('App destroy passive');
358
+ };
359
+ }, []);
360
+ return (
361
+ <>
362
+ <Suspense fallback={<Text text="Fallback" />}>
363
+ <Text text="Inside:Before" />
364
+ {children}
365
+ <ClassText text="Inside:After" />
366
+ </Suspense>
367
+ <Text text="Outside" />
368
+ </>
369
+ );
370
+ }
371
+
372
+ // Mount and suspend.
373
+ ReactNoop.act(() => {
374
+ ReactNoop.renderLegacySyncRoot(
375
+ <App>
376
+ <AsyncText text="Async" ms={1000} />
377
+ </App>,
378
+ );
379
+ });
380
+ expect(Scheduler).toHaveYielded([
381
+ 'App render',
382
+ 'Text:Inside:Before render',
383
+ 'Suspend:Async',
384
+ 'ClassText:Inside:After render',
385
+ 'Text:Fallback render',
386
+ 'Text:Outside render',
387
+ 'Text:Inside:Before create layout',
388
+ 'ClassText:Inside:After componentDidMount',
389
+ 'Text:Fallback create layout',
390
+ 'Text:Outside create layout',
391
+ 'App create layout',
392
+ 'Text:Inside:Before create passive',
393
+ 'Text:Fallback create passive',
394
+ 'Text:Outside create passive',
395
+ 'App create passive',
396
+ ]);
397
+ expect(ReactNoop.getChildren()).toEqual([
398
+ spanHidden('Inside:Before'),
399
+ spanHidden('Inside:After'),
400
+ span('Fallback'),
401
+ span('Outside'),
402
+ ]);
403
+
404
+ // Resolving the suspended resource should
405
+ await ReactNoop.act(async () => {
406
+ await resolveText('Async');
407
+ });
408
+ expect(Scheduler).toHaveYielded([
409
+ 'AsyncText:Async render',
410
+ 'Text:Fallback destroy layout',
411
+ 'AsyncText:Async create layout',
412
+ 'Text:Fallback destroy passive',
413
+ 'AsyncText:Async create passive',
414
+ ]);
415
+ expect(ReactNoop.getChildren()).toEqual([
416
+ span('Inside:Before'),
417
+ span('Async'),
418
+ span('Inside:After'),
419
+ span('Outside'),
420
+ ]);
421
+
422
+ await ReactNoop.act(async () => {
423
+ ReactNoop.renderLegacySyncRoot(null);
424
+ });
425
+ expect(Scheduler).toHaveYielded([
426
+ 'App destroy layout',
427
+ 'Text:Inside:Before destroy layout',
428
+ 'AsyncText:Async destroy layout',
429
+ 'ClassText:Inside:After componentWillUnmount',
430
+ 'Text:Outside destroy layout',
431
+ 'App destroy passive',
432
+ 'Text:Inside:Before destroy passive',
433
+ 'AsyncText:Async destroy passive',
434
+ 'Text:Outside destroy passive',
435
+ ]);
436
+ expect(ReactNoop.getChildren()).toEqual([]);
437
+ });
438
+ });
439
+
440
+ describe('layout effects within a tree that re-suspends in an update', () => {
441
+ // @gate enableCache
442
+ it('should not be destroyed or recreated in legacy roots', async () => {
443
+ function App({children = null}) {
444
+ Scheduler.unstable_yieldValue('App render');
445
+ React.useLayoutEffect(() => {
446
+ Scheduler.unstable_yieldValue('App create layout');
447
+ return () => {
448
+ Scheduler.unstable_yieldValue('App destroy layout');
449
+ };
450
+ }, []);
451
+ React.useEffect(() => {
452
+ Scheduler.unstable_yieldValue('App create passive');
453
+ return () => {
454
+ Scheduler.unstable_yieldValue('App destroy passive');
455
+ };
456
+ }, []);
457
+ return (
458
+ <>
459
+ <Suspense fallback={<Text text="Fallback" />}>
460
+ <Text text="Inside:Before" />
461
+ {children}
462
+ <Text text="Inside:After" />
463
+ </Suspense>
464
+ <Text text="Outside" />
465
+ </>
466
+ );
467
+ }
468
+
469
+ // Mount
470
+ ReactNoop.act(() => {
471
+ ReactNoop.renderLegacySyncRoot(<App />);
472
+ });
473
+ expect(Scheduler).toHaveYielded([
474
+ 'App render',
475
+ 'Text:Inside:Before render',
476
+ 'Text:Inside:After render',
477
+ 'Text:Outside render',
478
+ 'Text:Inside:Before create layout',
479
+ 'Text:Inside:After create layout',
480
+ 'Text:Outside create layout',
481
+ 'App create layout',
482
+ 'Text:Inside:Before create passive',
483
+ 'Text:Inside:After create passive',
484
+ 'Text:Outside create passive',
485
+ 'App create passive',
486
+ ]);
487
+ expect(ReactNoop.getChildren()).toEqual([
488
+ span('Inside:Before'),
489
+ span('Inside:After'),
490
+ span('Outside'),
491
+ ]);
492
+
493
+ // Schedule an update that causes React to suspend.
494
+ ReactNoop.act(() => {
495
+ ReactNoop.renderLegacySyncRoot(
496
+ <App>
497
+ <AsyncText text="Async" ms={1000} />
498
+ </App>,
499
+ );
500
+ });
501
+ expect(Scheduler).toHaveYielded([
502
+ 'App render',
503
+ 'Text:Inside:Before render',
504
+ 'Suspend:Async',
505
+ 'Text:Inside:After render',
506
+ 'Text:Fallback render',
507
+ 'Text:Outside render',
508
+ 'Text:Fallback create layout',
509
+ 'Text:Fallback create passive',
510
+ ]);
511
+ expect(ReactNoop.getChildren()).toEqual([
512
+ spanHidden('Inside:Before'),
513
+ spanHidden('Inside:After'),
514
+ span('Fallback'),
515
+ span('Outside'),
516
+ ]);
517
+
518
+ await advanceTimers(1000);
519
+
520
+ // Noop since sync root has already committed
521
+ expect(Scheduler).toHaveYielded([]);
522
+ expect(ReactNoop.getChildren()).toEqual([
523
+ spanHidden('Inside:Before'),
524
+ spanHidden('Inside:After'),
525
+ span('Fallback'),
526
+ span('Outside'),
527
+ ]);
528
+
529
+ // Resolving the suspended resource should re-create inner layout effects.
530
+ await ReactNoop.act(async () => {
531
+ await resolveText('Async');
532
+ });
533
+ expect(Scheduler).toHaveYielded([
534
+ 'AsyncText:Async render',
535
+ 'Text:Fallback destroy layout',
536
+ 'AsyncText:Async create layout',
537
+ 'Text:Fallback destroy passive',
538
+ 'AsyncText:Async create passive',
539
+ ]);
540
+ expect(ReactNoop.getChildren()).toEqual([
541
+ span('Inside:Before'),
542
+ span('Async'),
543
+ span('Inside:After'),
544
+ span('Outside'),
545
+ ]);
546
+
547
+ await ReactNoop.act(async () => {
548
+ ReactNoop.renderLegacySyncRoot(null);
549
+ });
550
+ expect(Scheduler).toHaveYielded([
551
+ 'App destroy layout',
552
+ 'Text:Inside:Before destroy layout',
553
+ 'AsyncText:Async destroy layout',
554
+ 'Text:Inside:After destroy layout',
555
+ 'Text:Outside destroy layout',
556
+ 'App destroy passive',
557
+ 'Text:Inside:Before destroy passive',
558
+ 'AsyncText:Async destroy passive',
559
+ 'Text:Inside:After destroy passive',
560
+ 'Text:Outside destroy passive',
561
+ ]);
562
+ });
563
+
564
+ // @gate enableSuspenseLayoutEffectSemantics
565
+ // @gate enableCache
566
+ it('should be destroyed and recreated for function components', async () => {
567
+ function App({children = null}) {
568
+ Scheduler.unstable_yieldValue('App render');
569
+ React.useLayoutEffect(() => {
570
+ Scheduler.unstable_yieldValue('App create layout');
571
+ return () => {
572
+ Scheduler.unstable_yieldValue('App destroy layout');
573
+ };
574
+ }, []);
575
+ React.useEffect(() => {
576
+ Scheduler.unstable_yieldValue('App create passive');
577
+ return () => {
578
+ Scheduler.unstable_yieldValue('App destroy passive');
579
+ };
580
+ }, []);
581
+ return (
582
+ <>
583
+ <Suspense fallback={<Text text="Fallback" />}>
584
+ <Text text="Inside:Before" />
585
+ {children}
586
+ <Text text="Inside:After" />
587
+ </Suspense>
588
+ <Text text="Outside" />
589
+ </>
590
+ );
591
+ }
592
+
593
+ await ReactNoop.act(async () => {
594
+ ReactNoop.render(<App />);
595
+ });
596
+ expect(Scheduler).toHaveYielded([
597
+ 'App render',
598
+ 'Text:Inside:Before render',
599
+ 'Text:Inside:After render',
600
+ 'Text:Outside render',
601
+ 'Text:Inside:Before create layout',
602
+ 'Text:Inside:After create layout',
603
+ 'Text:Outside create layout',
604
+ 'App create layout',
605
+ 'Text:Inside:Before create passive',
606
+ 'Text:Inside:After create passive',
607
+ 'Text:Outside create passive',
608
+ 'App create passive',
609
+ ]);
610
+ expect(ReactNoop.getChildren()).toEqual([
611
+ span('Inside:Before'),
612
+ span('Inside:After'),
613
+ span('Outside'),
614
+ ]);
615
+
616
+ // Schedule an update that causes React to suspend.
617
+ ReactNoop.act(() => {
618
+ ReactNoop.render(
619
+ <App>
620
+ <AsyncText text="Async" ms={1000} />
621
+ </App>,
622
+ );
623
+ });
624
+ expect(Scheduler).toHaveYielded([
625
+ 'App render',
626
+ 'Text:Inside:Before render',
627
+ 'Suspend:Async',
628
+ 'Text:Inside:After render',
629
+ 'Text:Fallback render',
630
+ 'Text:Outside render',
631
+ ]);
632
+ expect(ReactNoop.getChildren()).toEqual([
633
+ span('Inside:Before'),
634
+ span('Inside:After'),
635
+ span('Outside'),
636
+ ]);
637
+
638
+ await advanceTimers(1000);
639
+
640
+ // Timing out should commit the fallback and destroy inner layout effects.
641
+ expect(Scheduler).toHaveYielded([
642
+ 'Text:Inside:Before destroy layout',
643
+ 'Text:Inside:After destroy layout',
644
+ 'Text:Fallback create layout',
645
+ ]);
646
+ expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
647
+ expect(ReactNoop.getChildren()).toEqual([
648
+ spanHidden('Inside:Before'),
649
+ spanHidden('Inside:After'),
650
+ span('Fallback'),
651
+ span('Outside'),
652
+ ]);
653
+
654
+ // Resolving the suspended resource should re-create inner layout effects.
655
+ await ReactNoop.act(async () => {
656
+ await resolveText('Async');
657
+ });
658
+ expect(Scheduler).toHaveYielded([
659
+ 'Text:Inside:Before render',
660
+ 'AsyncText:Async render',
661
+ 'Text:Inside:After render',
662
+ 'Text:Fallback destroy layout',
663
+ 'Text:Inside:Before create layout',
664
+ 'AsyncText:Async create layout',
665
+ 'Text:Inside:After create layout',
666
+ 'Text:Fallback destroy passive',
667
+ 'AsyncText:Async create passive',
668
+ ]);
669
+ expect(ReactNoop.getChildren()).toEqual([
670
+ span('Inside:Before'),
671
+ span('Async'),
672
+ span('Inside:After'),
673
+ span('Outside'),
674
+ ]);
675
+
676
+ await ReactNoop.act(async () => {
677
+ ReactNoop.render(null);
678
+ });
679
+ expect(Scheduler).toHaveYielded([
680
+ 'App destroy layout',
681
+ 'Text:Inside:Before destroy layout',
682
+ 'AsyncText:Async destroy layout',
683
+ 'Text:Inside:After destroy layout',
684
+ 'Text:Outside destroy layout',
685
+ 'App destroy passive',
686
+ 'Text:Inside:Before destroy passive',
687
+ 'AsyncText:Async destroy passive',
688
+ 'Text:Inside:After destroy passive',
689
+ 'Text:Outside destroy passive',
690
+ ]);
691
+ });
692
+
693
+ // @gate enableSuspenseLayoutEffectSemantics
694
+ // @gate enableCache
695
+ it('should be destroyed and recreated for class components', async () => {
696
+ class ClassText extends React.Component {
697
+ componentDidMount() {
698
+ const {text} = this.props;
699
+ Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
700
+ }
701
+ componentDidUpdate() {
702
+ const {text} = this.props;
703
+ Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
704
+ }
705
+ componentWillUnmount() {
706
+ const {text} = this.props;
707
+ Scheduler.unstable_yieldValue(
708
+ `ClassText:${text} componentWillUnmount`,
709
+ );
710
+ }
711
+ render() {
712
+ const {children, text} = this.props;
713
+ Scheduler.unstable_yieldValue(`ClassText:${text} render`);
714
+ return <span prop={text}>{children}</span>;
715
+ }
716
+ }
717
+
718
+ function App({children = null}) {
719
+ Scheduler.unstable_yieldValue('App render');
720
+ React.useLayoutEffect(() => {
721
+ Scheduler.unstable_yieldValue('App create layout');
722
+ return () => {
723
+ Scheduler.unstable_yieldValue('App destroy layout');
724
+ };
725
+ }, []);
726
+ React.useEffect(() => {
727
+ Scheduler.unstable_yieldValue('App create passive');
728
+ return () => {
729
+ Scheduler.unstable_yieldValue('App destroy passive');
730
+ };
731
+ }, []);
732
+ return (
733
+ <>
734
+ <Suspense fallback={<ClassText text="Fallback" />}>
735
+ <ClassText text="Inside:Before" />
736
+ {children}
737
+ <ClassText text="Inside:After" />
738
+ </Suspense>
739
+ <ClassText text="Outside" />
740
+ </>
741
+ );
742
+ }
743
+
744
+ // Mount
745
+ await ReactNoop.act(async () => {
746
+ ReactNoop.render(<App />);
747
+ });
748
+ expect(Scheduler).toHaveYielded([
749
+ 'App render',
750
+ 'ClassText:Inside:Before render',
751
+ 'ClassText:Inside:After render',
752
+ 'ClassText:Outside render',
753
+ 'ClassText:Inside:Before componentDidMount',
754
+ 'ClassText:Inside:After componentDidMount',
755
+ 'ClassText:Outside componentDidMount',
756
+ 'App create layout',
757
+ 'App create passive',
758
+ ]);
759
+ expect(ReactNoop.getChildren()).toEqual([
760
+ span('Inside:Before'),
761
+ span('Inside:After'),
762
+ span('Outside'),
763
+ ]);
764
+
765
+ // Schedule an update that causes React to suspend.
766
+ ReactNoop.act(() => {
767
+ ReactNoop.render(
768
+ <App>
769
+ <AsyncText text="Async" ms={1000} />
770
+ </App>,
771
+ );
772
+ });
773
+ expect(Scheduler).toHaveYielded([
774
+ 'App render',
775
+ 'ClassText:Inside:Before render',
776
+ 'Suspend:Async',
777
+ 'ClassText:Inside:After render',
778
+ 'ClassText:Fallback render',
779
+ 'ClassText:Outside render',
780
+ ]);
781
+ expect(ReactNoop.getChildren()).toEqual([
782
+ span('Inside:Before'),
783
+ span('Inside:After'),
784
+ span('Outside'),
785
+ ]);
786
+
787
+ await advanceTimers(1000);
788
+
789
+ // Timing out should commit the fallback and destroy inner layout effects.
790
+ expect(Scheduler).toHaveYielded([
791
+ 'ClassText:Inside:Before componentWillUnmount',
792
+ 'ClassText:Inside:After componentWillUnmount',
793
+ 'ClassText:Fallback componentDidMount',
794
+ 'ClassText:Outside componentDidUpdate',
795
+ ]);
796
+ expect(ReactNoop.getChildren()).toEqual([
797
+ spanHidden('Inside:Before'),
798
+ spanHidden('Inside:After'),
799
+ span('Fallback'),
800
+ span('Outside'),
801
+ ]);
802
+
803
+ // Resolving the suspended resource should re-create inner layout effects.
804
+ await ReactNoop.act(async () => {
805
+ await resolveText('Async');
806
+ });
807
+ expect(Scheduler).toHaveYielded([
808
+ 'ClassText:Inside:Before render',
809
+ 'AsyncText:Async render',
810
+ 'ClassText:Inside:After render',
811
+ 'ClassText:Fallback componentWillUnmount',
812
+ 'ClassText:Inside:Before componentDidMount',
813
+ 'AsyncText:Async create layout',
814
+ 'ClassText:Inside:After componentDidMount',
815
+ 'AsyncText:Async create passive',
816
+ ]);
817
+ expect(ReactNoop.getChildren()).toEqual([
818
+ span('Inside:Before'),
819
+ span('Async'),
820
+ span('Inside:After'),
821
+ span('Outside'),
822
+ ]);
823
+
824
+ await ReactNoop.act(async () => {
825
+ ReactNoop.render(null);
826
+ });
827
+ expect(Scheduler).toHaveYielded([
828
+ 'App destroy layout',
829
+ 'ClassText:Inside:Before componentWillUnmount',
830
+ 'AsyncText:Async destroy layout',
831
+ 'ClassText:Inside:After componentWillUnmount',
832
+ 'ClassText:Outside componentWillUnmount',
833
+ 'App destroy passive',
834
+ 'AsyncText:Async destroy passive',
835
+ ]);
836
+ });
837
+
838
+ // @gate enableSuspenseLayoutEffectSemantics
839
+ // @gate enableCache
840
+ it('should be destroyed and recreated when nested below host components', async () => {
841
+ function App({children = null}) {
842
+ Scheduler.unstable_yieldValue('App render');
843
+ React.useLayoutEffect(() => {
844
+ Scheduler.unstable_yieldValue('App create layout');
845
+ return () => {
846
+ Scheduler.unstable_yieldValue('App destroy layout');
847
+ };
848
+ }, []);
849
+ React.useEffect(() => {
850
+ Scheduler.unstable_yieldValue('App create passive');
851
+ return () => {
852
+ Scheduler.unstable_yieldValue('App destroy passive');
853
+ };
854
+ }, []);
855
+ return (
856
+ <Suspense fallback={<Text text="Fallback" />}>
857
+ {children}
858
+ <Text text="Outer">
859
+ <Text text="Inner" />
860
+ </Text>
861
+ </Suspense>
862
+ );
863
+ }
864
+
865
+ // Mount
866
+ await ReactNoop.act(async () => {
867
+ ReactNoop.render(<App />);
868
+ });
869
+ expect(Scheduler).toHaveYielded([
870
+ 'App render',
871
+ 'Text:Outer render',
872
+ 'Text:Inner render',
873
+ 'Text:Inner create layout',
874
+ 'Text:Outer create layout',
875
+ 'App create layout',
876
+ 'Text:Inner create passive',
877
+ 'Text:Outer create passive',
878
+ 'App create passive',
879
+ ]);
880
+ expect(ReactNoop.getChildren()).toEqual([span('Outer', [span('Inner')])]);
881
+
882
+ // Schedule an update that causes React to suspend.
883
+ ReactNoop.act(() => {
884
+ ReactNoop.render(
885
+ <App>
886
+ <AsyncText text="Async" ms={1000} />
887
+ </App>,
888
+ );
889
+ });
890
+ expect(Scheduler).toHaveYielded([
891
+ 'App render',
892
+ 'Suspend:Async',
893
+ 'Text:Outer render',
894
+ 'Text:Inner render',
895
+ 'Text:Fallback render',
896
+ ]);
897
+ expect(ReactNoop.getChildren()).toEqual([span('Outer', [span('Inner')])]);
898
+
899
+ await advanceTimers(1000);
900
+
901
+ // Timing out should commit the fallback and destroy inner layout effects.
902
+ expect(Scheduler).toHaveYielded([
903
+ 'Text:Outer destroy layout',
904
+ 'Text:Inner destroy layout',
905
+ 'Text:Fallback create layout',
906
+ ]);
907
+ expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
908
+ expect(ReactNoop.getChildren()).toEqual([
909
+ spanHidden('Outer', [spanHidden('Inner')]),
910
+ span('Fallback'),
911
+ ]);
912
+
913
+ // Resolving the suspended resource should re-create inner layout effects.
914
+ await ReactNoop.act(async () => {
915
+ await resolveText('Async');
916
+ });
917
+ expect(Scheduler).toHaveYielded([
918
+ 'AsyncText:Async render',
919
+ 'Text:Outer render',
920
+ 'Text:Inner render',
921
+ 'Text:Fallback destroy layout',
922
+ 'AsyncText:Async create layout',
923
+ 'Text:Inner create layout',
924
+ 'Text:Outer create layout',
925
+ 'Text:Fallback destroy passive',
926
+ 'AsyncText:Async create passive',
927
+ ]);
928
+ expect(ReactNoop.getChildren()).toEqual([
929
+ span('Async'),
930
+ span('Outer', [span('Inner')]),
931
+ ]);
932
+
933
+ await ReactNoop.act(async () => {
934
+ ReactNoop.render(null);
935
+ });
936
+ expect(Scheduler).toHaveYielded([
937
+ 'App destroy layout',
938
+ 'AsyncText:Async destroy layout',
939
+ 'Text:Outer destroy layout',
940
+ 'Text:Inner destroy layout',
941
+ 'App destroy passive',
942
+ 'AsyncText:Async destroy passive',
943
+ 'Text:Outer destroy passive',
944
+ 'Text:Inner destroy passive',
945
+ ]);
946
+ });
947
+
948
+ // @gate enableSuspenseLayoutEffectSemantics
949
+ // @gate enableCache
950
+ it('should be destroyed and recreated even if there is a bailout because of memoization', async () => {
951
+ const MemoizedText = React.memo(Text, () => true);
952
+
953
+ function App({children = null}) {
954
+ Scheduler.unstable_yieldValue('App render');
955
+ React.useLayoutEffect(() => {
956
+ Scheduler.unstable_yieldValue('App create layout');
957
+ return () => {
958
+ Scheduler.unstable_yieldValue('App destroy layout');
959
+ };
960
+ }, []);
961
+ React.useEffect(() => {
962
+ Scheduler.unstable_yieldValue('App create passive');
963
+ return () => {
964
+ Scheduler.unstable_yieldValue('App destroy passive');
965
+ };
966
+ }, []);
967
+ return (
968
+ <Suspense fallback={<Text text="Fallback" />}>
969
+ {children}
970
+ <Text text="Outer">
971
+ <MemoizedText text="MemoizedInner" />
972
+ </Text>
973
+ </Suspense>
974
+ );
975
+ }
976
+
977
+ // Mount
978
+ await ReactNoop.act(async () => {
979
+ ReactNoop.render(<App />);
980
+ });
981
+ expect(Scheduler).toHaveYielded([
982
+ 'App render',
983
+ 'Text:Outer render',
984
+ 'Text:MemoizedInner render',
985
+ 'Text:MemoizedInner create layout',
986
+ 'Text:Outer create layout',
987
+ 'App create layout',
988
+ 'Text:MemoizedInner create passive',
989
+ 'Text:Outer create passive',
990
+ 'App create passive',
991
+ ]);
992
+ expect(ReactNoop.getChildren()).toEqual([
993
+ span('Outer', [span('MemoizedInner')]),
994
+ ]);
995
+
996
+ // Schedule an update that causes React to suspend.
997
+ ReactNoop.act(() => {
998
+ ReactNoop.render(
999
+ <App>
1000
+ <AsyncText text="Async" ms={1000} />
1001
+ </App>,
1002
+ );
1003
+ });
1004
+ expect(Scheduler).toHaveYielded([
1005
+ 'App render',
1006
+ 'Suspend:Async',
1007
+ 'Text:Outer render',
1008
+ // Text:MemoizedInner is memoized
1009
+ 'Text:Fallback render',
1010
+ ]);
1011
+ expect(ReactNoop.getChildren()).toEqual([
1012
+ span('Outer', [span('MemoizedInner')]),
1013
+ ]);
1014
+
1015
+ await advanceTimers(1000);
1016
+
1017
+ // Timing out should commit the fallback and destroy inner layout effects.
1018
+ // Even though the innermost layout effects are beneat a hidden HostComponent.
1019
+ expect(Scheduler).toHaveYielded([
1020
+ 'Text:Outer destroy layout',
1021
+ 'Text:MemoizedInner destroy layout',
1022
+ 'Text:Fallback create layout',
1023
+ ]);
1024
+ expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
1025
+ expect(ReactNoop.getChildren()).toEqual([
1026
+ spanHidden('Outer', [spanHidden('MemoizedInner')]),
1027
+ span('Fallback'),
1028
+ ]);
1029
+
1030
+ // Resolving the suspended resource should re-create inner layout effects.
1031
+ await ReactNoop.act(async () => {
1032
+ await resolveText('Async');
1033
+ });
1034
+ expect(Scheduler).toHaveYielded([
1035
+ 'AsyncText:Async render',
1036
+ 'Text:Outer render',
1037
+ 'Text:Fallback destroy layout',
1038
+ 'AsyncText:Async create layout',
1039
+ 'Text:MemoizedInner create layout',
1040
+ 'Text:Outer create layout',
1041
+ 'Text:Fallback destroy passive',
1042
+ 'AsyncText:Async create passive',
1043
+ ]);
1044
+ expect(ReactNoop.getChildren()).toEqual([
1045
+ span('Async'),
1046
+ span('Outer', [span('MemoizedInner')]),
1047
+ ]);
1048
+
1049
+ await ReactNoop.act(async () => {
1050
+ ReactNoop.render(null);
1051
+ });
1052
+ expect(Scheduler).toHaveYielded([
1053
+ 'App destroy layout',
1054
+ 'AsyncText:Async destroy layout',
1055
+ 'Text:Outer destroy layout',
1056
+ 'Text:MemoizedInner destroy layout',
1057
+ 'App destroy passive',
1058
+ 'AsyncText:Async destroy passive',
1059
+ 'Text:Outer destroy passive',
1060
+ 'Text:MemoizedInner destroy passive',
1061
+ ]);
1062
+ });
1063
+
1064
+ // @gate enableSuspenseLayoutEffectSemantics
1065
+ // @gate enableCache
1066
+ it('should respect nested suspense boundaries', async () => {
1067
+ function App({innerChildren = null, outerChildren = null}) {
1068
+ return (
1069
+ <Suspense fallback={<Text text="OuterFallback" />}>
1070
+ <Text text="Outer" />
1071
+ {outerChildren}
1072
+ <Suspense fallback={<Text text="InnerFallback" />}>
1073
+ <Text text="Inner" />
1074
+ {innerChildren}
1075
+ </Suspense>
1076
+ </Suspense>
1077
+ );
1078
+ }
1079
+
1080
+ // Mount
1081
+ await ReactNoop.act(async () => {
1082
+ ReactNoop.render(<App />);
1083
+ });
1084
+ expect(Scheduler).toHaveYielded([
1085
+ 'Text:Outer render',
1086
+ 'Text:Inner render',
1087
+ 'Text:Outer create layout',
1088
+ 'Text:Inner create layout',
1089
+ 'Text:Outer create passive',
1090
+ 'Text:Inner create passive',
1091
+ ]);
1092
+ expect(ReactNoop.getChildren()).toEqual([span('Outer'), span('Inner')]);
1093
+
1094
+ // Suspend the inner Suspense subtree (only inner effects should be destroyed)
1095
+ ReactNoop.act(() => {
1096
+ ReactNoop.render(
1097
+ <App innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />} />,
1098
+ );
1099
+ });
1100
+ await advanceTimers(1000);
1101
+ expect(Scheduler).toHaveYielded([
1102
+ 'Text:Outer render',
1103
+ 'Text:Inner render',
1104
+ 'Suspend:InnerAsync_1',
1105
+ 'Text:InnerFallback render',
1106
+ 'Text:Inner destroy layout',
1107
+ 'Text:InnerFallback create layout',
1108
+ ]);
1109
+ expect(Scheduler).toFlushAndYield(['Text:InnerFallback create passive']);
1110
+ expect(ReactNoop.getChildren()).toEqual([
1111
+ span('Outer'),
1112
+ spanHidden('Inner'),
1113
+ span('InnerFallback'),
1114
+ ]);
1115
+
1116
+ // Suspend the outer Suspense subtree (outer effects and inner fallback effects should be destroyed)
1117
+ // (This check also ensures we don't destroy effects for mounted inner fallback.)
1118
+ ReactNoop.act(() => {
1119
+ ReactNoop.render(
1120
+ <App
1121
+ outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
1122
+ innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />}
1123
+ />,
1124
+ );
1125
+ });
1126
+ await advanceTimers(1000);
1127
+ expect(Scheduler).toHaveYielded([
1128
+ 'Text:Outer render',
1129
+ 'Suspend:OuterAsync_1',
1130
+ 'Text:Inner render',
1131
+ 'Suspend:InnerAsync_1',
1132
+ 'Text:InnerFallback render',
1133
+ 'Text:OuterFallback render',
1134
+ 'Text:Outer destroy layout',
1135
+ 'Text:InnerFallback destroy layout',
1136
+ 'Text:OuterFallback create layout',
1137
+ ]);
1138
+ expect(Scheduler).toFlushAndYield(['Text:OuterFallback create passive']);
1139
+ expect(ReactNoop.getChildren()).toEqual([
1140
+ spanHidden('Outer'),
1141
+ spanHidden('Inner'),
1142
+ spanHidden('InnerFallback'),
1143
+ span('OuterFallback'),
1144
+ ]);
1145
+
1146
+ // Show the inner Susepnse subtree (no effects should be recreated)
1147
+ await ReactNoop.act(async () => {
1148
+ await resolveText('InnerAsync_1');
1149
+ });
1150
+ expect(Scheduler).toHaveYielded([
1151
+ 'Text:Outer render',
1152
+ 'Suspend:OuterAsync_1',
1153
+ 'Text:Inner render',
1154
+ 'AsyncText:InnerAsync_1 render',
1155
+ ]);
1156
+ expect(ReactNoop.getChildren()).toEqual([
1157
+ spanHidden('Outer'),
1158
+ spanHidden('Inner'),
1159
+ spanHidden('InnerFallback'),
1160
+ span('OuterFallback'),
1161
+ ]);
1162
+
1163
+ // Suspend the inner Suspense subtree (no effects should be destroyed)
1164
+ ReactNoop.act(() => {
1165
+ ReactNoop.render(
1166
+ <App
1167
+ outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
1168
+ innerChildren={<AsyncText text="InnerAsync_2" ms={1000} />}
1169
+ />,
1170
+ );
1171
+ });
1172
+ await advanceTimers(1000);
1173
+ expect(Scheduler).toHaveYielded([
1174
+ 'Text:Outer render',
1175
+ 'Suspend:OuterAsync_1',
1176
+ 'Text:Inner render',
1177
+ 'Suspend:InnerAsync_2',
1178
+ 'Text:InnerFallback render',
1179
+ 'Text:OuterFallback render',
1180
+ ]);
1181
+ expect(ReactNoop.getChildren()).toEqual([
1182
+ spanHidden('Outer'),
1183
+ spanHidden('Inner'),
1184
+ spanHidden('InnerFallback'),
1185
+ span('OuterFallback'),
1186
+ ]);
1187
+
1188
+ // Show the outer Susepnse subtree (only outer effects should be recreated)
1189
+ await ReactNoop.act(async () => {
1190
+ await resolveText('OuterAsync_1');
1191
+ });
1192
+ expect(Scheduler).toHaveYielded([
1193
+ 'Text:Outer render',
1194
+ 'AsyncText:OuterAsync_1 render',
1195
+ 'Text:Inner render',
1196
+ 'Suspend:InnerAsync_2',
1197
+ 'Text:InnerFallback render',
1198
+ 'Text:OuterFallback destroy layout',
1199
+ 'Text:Outer create layout',
1200
+ 'AsyncText:OuterAsync_1 create layout',
1201
+ 'Text:InnerFallback create layout',
1202
+ 'Text:OuterFallback destroy passive',
1203
+ 'AsyncText:OuterAsync_1 create passive',
1204
+ ]);
1205
+ expect(ReactNoop.getChildren()).toEqual([
1206
+ span('Outer'),
1207
+ span('OuterAsync_1'),
1208
+ spanHidden('Inner'),
1209
+ span('InnerFallback'),
1210
+ ]);
1211
+
1212
+ // Show the inner Susepnse subtree (only inner effects should be recreated)
1213
+ await ReactNoop.act(async () => {
1214
+ await resolveText('InnerAsync_2');
1215
+ });
1216
+ expect(Scheduler).toHaveYielded([
1217
+ 'Text:Inner render',
1218
+ 'AsyncText:InnerAsync_2 render',
1219
+ 'Text:InnerFallback destroy layout',
1220
+ 'Text:Inner create layout',
1221
+ 'AsyncText:InnerAsync_2 create layout',
1222
+ 'Text:InnerFallback destroy passive',
1223
+ 'AsyncText:InnerAsync_2 create passive',
1224
+ ]);
1225
+ expect(ReactNoop.getChildren()).toEqual([
1226
+ span('Outer'),
1227
+ span('OuterAsync_1'),
1228
+ span('Inner'),
1229
+ span('InnerAsync_2'),
1230
+ ]);
1231
+
1232
+ // Suspend the outer Suspense subtree (all effects should be destroyed)
1233
+ ReactNoop.act(() => {
1234
+ ReactNoop.render(
1235
+ <App
1236
+ outerChildren={<AsyncText text="OuterAsync_2" ms={1000} />}
1237
+ innerChildren={<AsyncText text="InnerAsync_2" ms={1000} />}
1238
+ />,
1239
+ );
1240
+ });
1241
+ await advanceTimers(1000);
1242
+ expect(Scheduler).toHaveYielded([
1243
+ 'Text:Outer render',
1244
+ 'Suspend:OuterAsync_2',
1245
+ 'Text:Inner render',
1246
+ 'AsyncText:InnerAsync_2 render',
1247
+ 'Text:OuterFallback render',
1248
+ 'Text:Outer destroy layout',
1249
+ 'AsyncText:OuterAsync_1 destroy layout',
1250
+ 'Text:Inner destroy layout',
1251
+ 'AsyncText:InnerAsync_2 destroy layout',
1252
+ 'Text:OuterFallback create layout',
1253
+ ]);
1254
+ expect(ReactNoop.getChildren()).toEqual([
1255
+ spanHidden('Outer'),
1256
+ spanHidden('OuterAsync_1'),
1257
+ spanHidden('Inner'),
1258
+ spanHidden('InnerAsync_2'),
1259
+ span('OuterFallback'),
1260
+ ]);
1261
+
1262
+ // Show the outer Suspense subtree (all effects should be recreated)
1263
+ await ReactNoop.act(async () => {
1264
+ await resolveText('OuterAsync_2');
1265
+ });
1266
+ expect(Scheduler).toHaveYielded([
1267
+ 'Text:OuterFallback create passive',
1268
+ 'Text:Outer render',
1269
+ 'AsyncText:OuterAsync_2 render',
1270
+ 'Text:Inner render',
1271
+ 'AsyncText:InnerAsync_2 render',
1272
+ 'Text:OuterFallback destroy layout',
1273
+ 'Text:Outer create layout',
1274
+ 'AsyncText:OuterAsync_2 create layout',
1275
+ 'Text:Inner create layout',
1276
+ 'AsyncText:InnerAsync_2 create layout',
1277
+ 'Text:OuterFallback destroy passive',
1278
+ ]);
1279
+ expect(ReactNoop.getChildren()).toEqual([
1280
+ span('Outer'),
1281
+ span('OuterAsync_2'),
1282
+ span('Inner'),
1283
+ span('InnerAsync_2'),
1284
+ ]);
1285
+ });
1286
+
1287
+ // @gate enableSuspenseLayoutEffectSemantics
1288
+ // @gate enableCache
1289
+ it('should be cleaned up inside of a fallback that suspends', async () => {
1290
+ function App({fallbackChildren = null, outerChildren = null}) {
1291
+ return (
1292
+ <>
1293
+ <Suspense
1294
+ fallback={
1295
+ <>
1296
+ <Suspense fallback={<Text text="Fallback:Fallback" />}>
1297
+ <Text text="Fallback:Inside" />
1298
+ {fallbackChildren}
1299
+ </Suspense>
1300
+ <Text text="Fallback:Outside" />
1301
+ </>
1302
+ }>
1303
+ <Text text="Inside" />
1304
+ {outerChildren}
1305
+ </Suspense>
1306
+ <Text text="Outside" />
1307
+ </>
1308
+ );
1309
+ }
1310
+
1311
+ // Mount
1312
+ await ReactNoop.act(async () => {
1313
+ ReactNoop.render(<App />);
1314
+ });
1315
+ expect(Scheduler).toHaveYielded([
1316
+ 'Text:Inside render',
1317
+ 'Text:Outside render',
1318
+ 'Text:Inside create layout',
1319
+ 'Text:Outside create layout',
1320
+ 'Text:Inside create passive',
1321
+ 'Text:Outside create passive',
1322
+ ]);
1323
+ expect(ReactNoop.getChildren()).toEqual([
1324
+ span('Inside'),
1325
+ span('Outside'),
1326
+ ]);
1327
+
1328
+ // Suspend the outer shell
1329
+ ReactNoop.act(() => {
1330
+ ReactNoop.render(
1331
+ <App outerChildren={<AsyncText text="OutsideAsync" ms={1000} />} />,
1332
+ );
1333
+ });
1334
+ expect(Scheduler).toHaveYielded([
1335
+ 'Text:Inside render',
1336
+ 'Suspend:OutsideAsync',
1337
+ 'Text:Fallback:Inside render',
1338
+ 'Text:Fallback:Outside render',
1339
+ 'Text:Outside render',
1340
+ ]);
1341
+ expect(ReactNoop.getChildren()).toEqual([
1342
+ span('Inside'),
1343
+ span('Outside'),
1344
+ ]);
1345
+
1346
+ // Timing out should commit the fallback and destroy inner layout effects.
1347
+ await advanceTimers(1000);
1348
+ expect(Scheduler).toHaveYielded([
1349
+ 'Text:Inside destroy layout',
1350
+ 'Text:Fallback:Inside create layout',
1351
+ 'Text:Fallback:Outside create layout',
1352
+ ]);
1353
+ expect(Scheduler).toFlushAndYield([
1354
+ 'Text:Fallback:Inside create passive',
1355
+ 'Text:Fallback:Outside create passive',
1356
+ ]);
1357
+ expect(ReactNoop.getChildren()).toEqual([
1358
+ spanHidden('Inside'),
1359
+ span('Fallback:Inside'),
1360
+ span('Fallback:Outside'),
1361
+ span('Outside'),
1362
+ ]);
1363
+
1364
+ // Suspend the fallback and verify that it's effects get cleaned up as well
1365
+ ReactNoop.act(() => {
1366
+ ReactNoop.render(
1367
+ <App
1368
+ fallbackChildren={<AsyncText text="FallbackAsync" ms={1000} />}
1369
+ outerChildren={<AsyncText text="OutsideAsync" ms={1000} />}
1370
+ />,
1371
+ );
1372
+ });
1373
+ expect(Scheduler).toHaveYielded([
1374
+ 'Text:Inside render',
1375
+ 'Suspend:OutsideAsync',
1376
+ 'Text:Fallback:Inside render',
1377
+ 'Suspend:FallbackAsync',
1378
+ 'Text:Fallback:Fallback render',
1379
+ 'Text:Fallback:Outside render',
1380
+ 'Text:Outside render',
1381
+ ]);
1382
+ expect(ReactNoop.getChildren()).toEqual([
1383
+ spanHidden('Inside'),
1384
+ span('Fallback:Inside'),
1385
+ span('Fallback:Outside'),
1386
+ span('Outside'),
1387
+ ]);
1388
+
1389
+ // Timing out should commit the inner fallback and destroy outer fallback layout effects.
1390
+ await advanceTimers(1000);
1391
+ expect(Scheduler).toHaveYielded([
1392
+ 'Text:Fallback:Inside destroy layout',
1393
+ 'Text:Fallback:Fallback create layout',
1394
+ ]);
1395
+ expect(Scheduler).toFlushAndYield([
1396
+ 'Text:Fallback:Fallback create passive',
1397
+ ]);
1398
+ expect(ReactNoop.getChildren()).toEqual([
1399
+ spanHidden('Inside'),
1400
+ spanHidden('Fallback:Inside'),
1401
+ span('Fallback:Fallback'),
1402
+ span('Fallback:Outside'),
1403
+ span('Outside'),
1404
+ ]);
1405
+
1406
+ // Resolving both resources should cleanup fallabck effects and recreate main effects
1407
+ await ReactNoop.act(async () => {
1408
+ await resolveText('FallbackAsync');
1409
+ await resolveText('OutsideAsync');
1410
+ });
1411
+ expect(Scheduler).toHaveYielded([
1412
+ 'Text:Inside render',
1413
+ 'AsyncText:OutsideAsync render',
1414
+ 'Text:Fallback:Fallback destroy layout',
1415
+ 'Text:Fallback:Outside destroy layout',
1416
+ 'Text:Inside create layout',
1417
+ 'AsyncText:OutsideAsync create layout',
1418
+ 'Text:Fallback:Inside destroy passive',
1419
+ 'Text:Fallback:Fallback destroy passive',
1420
+ 'Text:Fallback:Outside destroy passive',
1421
+ 'AsyncText:OutsideAsync create passive',
1422
+ ]);
1423
+ expect(ReactNoop.getChildren()).toEqual([
1424
+ span('Inside'),
1425
+ span('OutsideAsync'),
1426
+ span('Outside'),
1427
+ ]);
1428
+ });
1429
+
1430
+ // @gate enableSuspenseLayoutEffectSemantics
1431
+ // @gate enableCache
1432
+ it('should be cleaned up inside of a fallback that suspends (alternate)', async () => {
1433
+ function App({fallbackChildren = null, outerChildren = null}) {
1434
+ return (
1435
+ <>
1436
+ <Suspense
1437
+ fallback={
1438
+ <>
1439
+ <Suspense fallback={<Text text="Fallback:Fallback" />}>
1440
+ <Text text="Fallback:Inside" />
1441
+ {fallbackChildren}
1442
+ </Suspense>
1443
+ <Text text="Fallback:Outside" />
1444
+ </>
1445
+ }>
1446
+ <Text text="Inside" />
1447
+ {outerChildren}
1448
+ </Suspense>
1449
+ <Text text="Outside" />
1450
+ </>
1451
+ );
1452
+ }
1453
+
1454
+ // Mount
1455
+ await ReactNoop.act(async () => {
1456
+ ReactNoop.render(<App />);
1457
+ });
1458
+ expect(Scheduler).toHaveYielded([
1459
+ 'Text:Inside render',
1460
+ 'Text:Outside render',
1461
+ 'Text:Inside create layout',
1462
+ 'Text:Outside create layout',
1463
+ 'Text:Inside create passive',
1464
+ 'Text:Outside create passive',
1465
+ ]);
1466
+ expect(ReactNoop.getChildren()).toEqual([
1467
+ span('Inside'),
1468
+ span('Outside'),
1469
+ ]);
1470
+
1471
+ // Suspend both the outer boundary and the fallback
1472
+ ReactNoop.act(() => {
1473
+ ReactNoop.render(
1474
+ <App
1475
+ outerChildren={<AsyncText text="OutsideAsync" ms={1000} />}
1476
+ fallbackChildren={<AsyncText text="FallbackAsync" ms={1000} />}
1477
+ />,
1478
+ );
1479
+ });
1480
+ await advanceTimers(1000);
1481
+ expect(Scheduler).toHaveYielded([
1482
+ 'Text:Inside render',
1483
+ 'Suspend:OutsideAsync',
1484
+ 'Text:Fallback:Inside render',
1485
+ 'Suspend:FallbackAsync',
1486
+ 'Text:Fallback:Fallback render',
1487
+ 'Text:Fallback:Outside render',
1488
+ 'Text:Outside render',
1489
+ 'Text:Inside destroy layout',
1490
+ 'Text:Fallback:Fallback create layout',
1491
+ 'Text:Fallback:Outside create layout',
1492
+ ]);
1493
+ expect(Scheduler).toFlushAndYield([
1494
+ 'Text:Fallback:Fallback create passive',
1495
+ 'Text:Fallback:Outside create passive',
1496
+ ]);
1497
+ expect(ReactNoop.getChildren()).toEqual([
1498
+ spanHidden('Inside'),
1499
+ span('Fallback:Fallback'),
1500
+ span('Fallback:Outside'),
1501
+ span('Outside'),
1502
+ ]);
1503
+
1504
+ // Resolving the inside fallback
1505
+ await ReactNoop.act(async () => {
1506
+ await resolveText('FallbackAsync');
1507
+ });
1508
+ expect(Scheduler).toHaveYielded([
1509
+ 'Text:Fallback:Inside render',
1510
+ 'AsyncText:FallbackAsync render',
1511
+ 'Text:Fallback:Fallback destroy layout',
1512
+ 'Text:Fallback:Inside create layout',
1513
+ 'AsyncText:FallbackAsync create layout',
1514
+ 'Text:Fallback:Fallback destroy passive',
1515
+ 'Text:Fallback:Inside create passive',
1516
+ 'AsyncText:FallbackAsync create passive',
1517
+ ]);
1518
+ expect(ReactNoop.getChildren()).toEqual([
1519
+ spanHidden('Inside'),
1520
+ span('Fallback:Inside'),
1521
+ span('FallbackAsync'),
1522
+ span('Fallback:Outside'),
1523
+ span('Outside'),
1524
+ ]);
1525
+
1526
+ // Resolving the outer fallback only
1527
+ await ReactNoop.act(async () => {
1528
+ await resolveText('OutsideAsync');
1529
+ });
1530
+ expect(Scheduler).toHaveYielded([
1531
+ 'Text:Inside render',
1532
+ 'AsyncText:OutsideAsync render',
1533
+ 'Text:Fallback:Inside destroy layout',
1534
+ 'AsyncText:FallbackAsync destroy layout',
1535
+ 'Text:Fallback:Outside destroy layout',
1536
+ 'Text:Inside create layout',
1537
+ 'AsyncText:OutsideAsync create layout',
1538
+ 'Text:Fallback:Inside destroy passive',
1539
+ 'AsyncText:FallbackAsync destroy passive',
1540
+ 'Text:Fallback:Outside destroy passive',
1541
+ 'AsyncText:OutsideAsync create passive',
1542
+ ]);
1543
+ expect(ReactNoop.getChildren()).toEqual([
1544
+ span('Inside'),
1545
+ span('OutsideAsync'),
1546
+ span('Outside'),
1547
+ ]);
1548
+ });
1549
+
1550
+ // @gate enableSuspenseLayoutEffectSemantics
1551
+ // @gate enableCache
1552
+ it('should be cleaned up deeper inside of a subtree that suspends', async () => {
1553
+ function ConditionalSuspense({shouldSuspend}) {
1554
+ if (shouldSuspend) {
1555
+ readText('Suspend');
1556
+ }
1557
+ return <Text text="Inside" />;
1558
+ }
1559
+
1560
+ function App({children = null, shouldSuspend}) {
1561
+ return (
1562
+ <>
1563
+ <Suspense fallback={<Text text="Fallback" />}>
1564
+ <ConditionalSuspense shouldSuspend={shouldSuspend} />
1565
+ </Suspense>
1566
+ <Text text="Outside" />
1567
+ </>
1568
+ );
1569
+ }
1570
+
1571
+ // Mount
1572
+ await ReactNoop.act(async () => {
1573
+ ReactNoop.render(<App shouldSuspend={false} />);
1574
+ });
1575
+ expect(Scheduler).toHaveYielded([
1576
+ 'Text:Inside render',
1577
+ 'Text:Outside render',
1578
+ 'Text:Inside create layout',
1579
+ 'Text:Outside create layout',
1580
+ 'Text:Inside create passive',
1581
+ 'Text:Outside create passive',
1582
+ ]);
1583
+ expect(ReactNoop.getChildren()).toEqual([
1584
+ span('Inside'),
1585
+ span('Outside'),
1586
+ ]);
1587
+
1588
+ // Suspending a component in the middle of the tree
1589
+ // should still properly cleanup effects deeper in the tree
1590
+ ReactNoop.act(() => {
1591
+ ReactNoop.render(<App shouldSuspend={true} />);
1592
+ });
1593
+ expect(Scheduler).toHaveYielded([
1594
+ 'Suspend:Suspend',
1595
+ 'Text:Fallback render',
1596
+ 'Text:Outside render',
1597
+ ]);
1598
+ expect(ReactNoop.getChildren()).toEqual([
1599
+ span('Inside'),
1600
+ span('Outside'),
1601
+ ]);
1602
+
1603
+ // Timing out should commit the inner fallback and destroy outer fallback layout effects.
1604
+ await advanceTimers(1000);
1605
+ expect(Scheduler).toHaveYielded([
1606
+ 'Text:Inside destroy layout',
1607
+ 'Text:Fallback create layout',
1608
+ ]);
1609
+ expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
1610
+ expect(ReactNoop.getChildren()).toEqual([
1611
+ spanHidden('Inside'),
1612
+ span('Fallback'),
1613
+ span('Outside'),
1614
+ ]);
1615
+
1616
+ // Resolving should cleanup.
1617
+ await ReactNoop.act(async () => {
1618
+ await resolveText('Suspend');
1619
+ });
1620
+ expect(Scheduler).toHaveYielded([
1621
+ 'Text:Inside render',
1622
+ 'Text:Fallback destroy layout',
1623
+ 'Text:Inside create layout',
1624
+ 'Text:Fallback destroy passive',
1625
+ ]);
1626
+ expect(ReactNoop.getChildren()).toEqual([
1627
+ span('Inside'),
1628
+ span('Outside'),
1629
+ ]);
1630
+ });
1631
+
1632
+ describe('that throw errors', () => {
1633
+ // @gate enableSuspenseLayoutEffectSemantics
1634
+ // @gate enableCache
1635
+ it('are properly handled for componentDidMount', async () => {
1636
+ let componentDidMountShouldThrow = false;
1637
+
1638
+ class ThrowsInDidMount extends React.Component {
1639
+ componentWillUnmount() {
1640
+ Scheduler.unstable_yieldValue(
1641
+ 'ThrowsInDidMount componentWillUnmount',
1642
+ );
1643
+ }
1644
+ componentDidMount() {
1645
+ Scheduler.unstable_yieldValue('ThrowsInDidMount componentDidMount');
1646
+ if (componentDidMountShouldThrow) {
1647
+ throw Error('expected');
1648
+ }
1649
+ }
1650
+ render() {
1651
+ Scheduler.unstable_yieldValue('ThrowsInDidMount render');
1652
+ return <span prop="ThrowsInDidMount" />;
1653
+ }
1654
+ }
1655
+
1656
+ function App({children = null}) {
1657
+ Scheduler.unstable_yieldValue('App render');
1658
+ React.useLayoutEffect(() => {
1659
+ Scheduler.unstable_yieldValue('App create layout');
1660
+ return () => {
1661
+ Scheduler.unstable_yieldValue('App destroy layout');
1662
+ };
1663
+ }, []);
1664
+ return (
1665
+ <>
1666
+ <Suspense fallback={<Text text="Fallback" />}>
1667
+ {children}
1668
+ <ThrowsInDidMount />
1669
+ <Text text="Inside" />
1670
+ </Suspense>
1671
+ <Text text="Outside" />
1672
+ </>
1673
+ );
1674
+ }
1675
+
1676
+ await ReactNoop.act(async () => {
1677
+ ReactNoop.render(
1678
+ <ErrorBoundary fallback={<Text text="Error" />}>
1679
+ <App />
1680
+ </ErrorBoundary>,
1681
+ );
1682
+ });
1683
+ expect(Scheduler).toHaveYielded([
1684
+ 'ErrorBoundary render: try',
1685
+ 'App render',
1686
+ 'ThrowsInDidMount render',
1687
+ 'Text:Inside render',
1688
+ 'Text:Outside render',
1689
+ 'ThrowsInDidMount componentDidMount',
1690
+ 'Text:Inside create layout',
1691
+ 'Text:Outside create layout',
1692
+ 'App create layout',
1693
+ 'Text:Inside create passive',
1694
+ 'Text:Outside create passive',
1695
+ ]);
1696
+ expect(ReactNoop.getChildren()).toEqual([
1697
+ span('ThrowsInDidMount'),
1698
+ span('Inside'),
1699
+ span('Outside'),
1700
+ ]);
1701
+
1702
+ // Schedule an update that causes React to suspend.
1703
+ await ReactNoop.act(async () => {
1704
+ ReactNoop.render(
1705
+ <ErrorBoundary fallback={<Text text="Error" />}>
1706
+ <App>
1707
+ <AsyncText text="Async" ms={1000} />
1708
+ </App>
1709
+ </ErrorBoundary>,
1710
+ );
1711
+ });
1712
+ expect(Scheduler).toHaveYielded([
1713
+ 'ErrorBoundary render: try',
1714
+ 'App render',
1715
+ 'Suspend:Async',
1716
+ 'ThrowsInDidMount render',
1717
+ 'Text:Inside render',
1718
+ 'Text:Fallback render',
1719
+ 'Text:Outside render',
1720
+ 'ThrowsInDidMount componentWillUnmount',
1721
+ 'Text:Inside destroy layout',
1722
+ 'Text:Fallback create layout',
1723
+ 'Text:Fallback create passive',
1724
+ ]);
1725
+ expect(ReactNoop.getChildren()).toEqual([
1726
+ spanHidden('ThrowsInDidMount'),
1727
+ spanHidden('Inside'),
1728
+ span('Fallback'),
1729
+ span('Outside'),
1730
+ ]);
1731
+
1732
+ // Resolve the pending suspense and throw
1733
+ componentDidMountShouldThrow = true;
1734
+ await ReactNoop.act(async () => {
1735
+ await resolveText('Async');
1736
+ });
1737
+ expect(Scheduler).toHaveYielded([
1738
+ 'AsyncText:Async render',
1739
+ 'ThrowsInDidMount render',
1740
+ 'Text:Inside render',
1741
+ 'Text:Fallback destroy layout',
1742
+ 'AsyncText:Async create layout',
1743
+
1744
+ // Even though an error was thrown in componentDidMount,
1745
+ // subsequent layout effects should still be destroyed.
1746
+ 'ThrowsInDidMount componentDidMount',
1747
+ 'Text:Inside create layout',
1748
+
1749
+ // Finish the in-progress commit
1750
+ 'Text:Fallback destroy passive',
1751
+ 'AsyncText:Async create passive',
1752
+
1753
+ // Destroy layout and passive effects in the errored tree.
1754
+ 'App destroy layout',
1755
+ 'AsyncText:Async destroy layout',
1756
+ 'ThrowsInDidMount componentWillUnmount',
1757
+ 'Text:Inside destroy layout',
1758
+ 'Text:Outside destroy layout',
1759
+ 'AsyncText:Async destroy passive',
1760
+ 'Text:Inside destroy passive',
1761
+ 'Text:Outside destroy passive',
1762
+
1763
+ // Render fallback
1764
+ 'ErrorBoundary render: catch',
1765
+ 'Text:Error render',
1766
+ 'Text:Error create layout',
1767
+ 'Text:Error create passive',
1768
+ ]);
1769
+ expect(ReactNoop.getChildren()).toEqual([span('Error')]);
1770
+ });
1771
+
1772
+ // @gate enableSuspenseLayoutEffectSemantics
1773
+ // @gate enableCache
1774
+ it('are properly handled for componentWillUnmount', async () => {
1775
+ class ThrowsInWillUnmount extends React.Component {
1776
+ componentDidMount() {
1777
+ Scheduler.unstable_yieldValue(
1778
+ 'ThrowsInWillUnmount componentDidMount',
1779
+ );
1780
+ }
1781
+ componentWillUnmount() {
1782
+ Scheduler.unstable_yieldValue(
1783
+ 'ThrowsInWillUnmount componentWillUnmount',
1784
+ );
1785
+ throw Error('expected');
1786
+ }
1787
+ render() {
1788
+ Scheduler.unstable_yieldValue('ThrowsInWillUnmount render');
1789
+ return <span prop="ThrowsInWillUnmount" />;
1790
+ }
1791
+ }
1792
+
1793
+ function App({children = null}) {
1794
+ Scheduler.unstable_yieldValue('App render');
1795
+ React.useLayoutEffect(() => {
1796
+ Scheduler.unstable_yieldValue('App create layout');
1797
+ return () => {
1798
+ Scheduler.unstable_yieldValue('App destroy layout');
1799
+ };
1800
+ }, []);
1801
+ return (
1802
+ <>
1803
+ <Suspense fallback={<Text text="Fallback" />}>
1804
+ {children}
1805
+ <ThrowsInWillUnmount />
1806
+ <Text text="Inside" />
1807
+ </Suspense>
1808
+ <Text text="Outside" />
1809
+ </>
1810
+ );
1811
+ }
1812
+
1813
+ await ReactNoop.act(async () => {
1814
+ ReactNoop.render(
1815
+ <ErrorBoundary fallback={<Text text="Error" />}>
1816
+ <App />
1817
+ </ErrorBoundary>,
1818
+ );
1819
+ });
1820
+ expect(Scheduler).toHaveYielded([
1821
+ 'ErrorBoundary render: try',
1822
+ 'App render',
1823
+ 'ThrowsInWillUnmount render',
1824
+ 'Text:Inside render',
1825
+ 'Text:Outside render',
1826
+ 'ThrowsInWillUnmount componentDidMount',
1827
+ 'Text:Inside create layout',
1828
+ 'Text:Outside create layout',
1829
+ 'App create layout',
1830
+ 'Text:Inside create passive',
1831
+ 'Text:Outside create passive',
1832
+ ]);
1833
+ expect(ReactNoop.getChildren()).toEqual([
1834
+ span('ThrowsInWillUnmount'),
1835
+ span('Inside'),
1836
+ span('Outside'),
1837
+ ]);
1838
+
1839
+ // Schedule an update that suspends and triggers our error code.
1840
+ await ReactNoop.act(async () => {
1841
+ ReactNoop.render(
1842
+ <ErrorBoundary fallback={<Text text="Error" />}>
1843
+ <App>
1844
+ <AsyncText text="Async" ms={1000} />
1845
+ </App>
1846
+ </ErrorBoundary>,
1847
+ );
1848
+ });
1849
+ expect(Scheduler).toHaveYielded([
1850
+ 'ErrorBoundary render: try',
1851
+ 'App render',
1852
+ 'Suspend:Async',
1853
+ 'ThrowsInWillUnmount render',
1854
+ 'Text:Inside render',
1855
+ 'Text:Fallback render',
1856
+ 'Text:Outside render',
1857
+
1858
+ // Even though an error was thrown in componentWillUnmount,
1859
+ // subsequent layout effects should still be destroyed.
1860
+ 'ThrowsInWillUnmount componentWillUnmount',
1861
+ 'Text:Inside destroy layout',
1862
+
1863
+ // Finish the in-progess commit
1864
+ 'Text:Fallback create layout',
1865
+ 'Text:Fallback create passive',
1866
+
1867
+ // Destroy layout and passive effects in the errored tree.
1868
+ 'App destroy layout',
1869
+ 'ThrowsInWillUnmount componentWillUnmount',
1870
+ 'Text:Fallback destroy layout',
1871
+ 'Text:Outside destroy layout',
1872
+ 'Text:Inside destroy passive',
1873
+ 'Text:Fallback destroy passive',
1874
+ 'Text:Outside destroy passive',
1875
+
1876
+ // Render fallback
1877
+ 'ErrorBoundary render: catch',
1878
+ 'Text:Error render',
1879
+ 'Text:Error create layout',
1880
+ 'Text:Error create passive',
1881
+ ]);
1882
+ expect(ReactNoop.getChildren()).toEqual([span('Error')]);
1883
+ });
1884
+
1885
+ // @gate enableSuspenseLayoutEffectSemantics
1886
+ // @gate enableCache
1887
+ // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
1888
+ it('are properly handled for layout effect creation', async () => {
1889
+ let useLayoutEffectShouldThrow = false;
1890
+
1891
+ function ThrowsInLayoutEffect() {
1892
+ Scheduler.unstable_yieldValue('ThrowsInLayoutEffect render');
1893
+ React.useLayoutEffect(() => {
1894
+ Scheduler.unstable_yieldValue(
1895
+ 'ThrowsInLayoutEffect useLayoutEffect create',
1896
+ );
1897
+ if (useLayoutEffectShouldThrow) {
1898
+ throw Error('expected');
1899
+ }
1900
+ return () => {
1901
+ Scheduler.unstable_yieldValue(
1902
+ 'ThrowsInLayoutEffect useLayoutEffect destroy',
1903
+ );
1904
+ };
1905
+ }, []);
1906
+ return <span prop="ThrowsInLayoutEffect" />;
1907
+ }
1908
+
1909
+ function App({children = null}) {
1910
+ Scheduler.unstable_yieldValue('App render');
1911
+ React.useLayoutEffect(() => {
1912
+ Scheduler.unstable_yieldValue('App create layout');
1913
+ return () => {
1914
+ Scheduler.unstable_yieldValue('App destroy layout');
1915
+ };
1916
+ }, []);
1917
+ return (
1918
+ <>
1919
+ <Suspense fallback={<Text text="Fallback" />}>
1920
+ {children}
1921
+ <ThrowsInLayoutEffect />
1922
+ <Text text="Inside" />
1923
+ </Suspense>
1924
+ <Text text="Outside" />
1925
+ </>
1926
+ );
1927
+ }
1928
+
1929
+ await ReactNoop.act(async () => {
1930
+ ReactNoop.render(
1931
+ <ErrorBoundary fallback={<Text text="Error" />}>
1932
+ <App />
1933
+ </ErrorBoundary>,
1934
+ );
1935
+ });
1936
+ expect(Scheduler).toHaveYielded([
1937
+ 'ErrorBoundary render: try',
1938
+ 'App render',
1939
+ 'ThrowsInLayoutEffect render',
1940
+ 'Text:Inside render',
1941
+ 'Text:Outside render',
1942
+ 'ThrowsInLayoutEffect useLayoutEffect create',
1943
+ 'Text:Inside create layout',
1944
+ 'Text:Outside create layout',
1945
+ 'App create layout',
1946
+ 'Text:Inside create passive',
1947
+ 'Text:Outside create passive',
1948
+ ]);
1949
+ expect(ReactNoop.getChildren()).toEqual([
1950
+ span('ThrowsInLayoutEffect'),
1951
+ span('Inside'),
1952
+ span('Outside'),
1953
+ ]);
1954
+
1955
+ // Schedule an update that causes React to suspend.
1956
+ await ReactNoop.act(async () => {
1957
+ ReactNoop.render(
1958
+ <ErrorBoundary fallback={<Text text="Error" />}>
1959
+ <App>
1960
+ <AsyncText text="Async" ms={1000} />
1961
+ </App>
1962
+ </ErrorBoundary>,
1963
+ );
1964
+ });
1965
+ expect(Scheduler).toHaveYielded([
1966
+ 'ErrorBoundary render: try',
1967
+ 'App render',
1968
+ 'Suspend:Async',
1969
+ 'ThrowsInLayoutEffect render',
1970
+ 'Text:Inside render',
1971
+ 'Text:Fallback render',
1972
+ 'Text:Outside render',
1973
+ 'ThrowsInLayoutEffect useLayoutEffect destroy',
1974
+ 'Text:Inside destroy layout',
1975
+ 'Text:Fallback create layout',
1976
+ 'Text:Fallback create passive',
1977
+ ]);
1978
+ expect(ReactNoop.getChildren()).toEqual([
1979
+ spanHidden('ThrowsInLayoutEffect'),
1980
+ spanHidden('Inside'),
1981
+ span('Fallback'),
1982
+ span('Outside'),
1983
+ ]);
1984
+
1985
+ // Resolve the pending suspense and throw
1986
+ useLayoutEffectShouldThrow = true;
1987
+ await ReactNoop.act(async () => {
1988
+ await resolveText('Async');
1989
+ });
1990
+ expect(Scheduler).toHaveYielded([
1991
+ 'AsyncText:Async render',
1992
+ 'ThrowsInLayoutEffect render',
1993
+ 'Text:Inside render',
1994
+
1995
+ 'Text:Fallback destroy layout',
1996
+
1997
+ // Even though an error was thrown in useLayoutEffect,
1998
+ // subsequent layout effects should still be created.
1999
+ 'AsyncText:Async create layout',
2000
+ 'ThrowsInLayoutEffect useLayoutEffect create',
2001
+ 'Text:Inside create layout',
2002
+
2003
+ // Finish the in-progess commit
2004
+ 'Text:Fallback destroy passive',
2005
+ 'AsyncText:Async create passive',
2006
+
2007
+ // Destroy layout and passive effects in the errored tree.
2008
+ 'App destroy layout',
2009
+ 'AsyncText:Async destroy layout',
2010
+ 'Text:Inside destroy layout',
2011
+ 'Text:Outside destroy layout',
2012
+ 'AsyncText:Async destroy passive',
2013
+ 'Text:Inside destroy passive',
2014
+ 'Text:Outside destroy passive',
2015
+
2016
+ // Render fallback
2017
+ 'ErrorBoundary render: catch',
2018
+ 'Text:Error render',
2019
+ 'Text:Error create layout',
2020
+ 'Text:Error create passive',
2021
+ ]);
2022
+ expect(ReactNoop.getChildren()).toEqual([span('Error')]);
2023
+ });
2024
+
2025
+ // @gate enableSuspenseLayoutEffectSemantics
2026
+ // @gate enableCache
2027
+ // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
2028
+ it('are properly handled for layout effect descruction', async () => {
2029
+ function ThrowsInLayoutEffectDestroy() {
2030
+ Scheduler.unstable_yieldValue('ThrowsInLayoutEffectDestroy render');
2031
+ React.useLayoutEffect(() => {
2032
+ Scheduler.unstable_yieldValue(
2033
+ 'ThrowsInLayoutEffectDestroy useLayoutEffect create',
2034
+ );
2035
+ return () => {
2036
+ Scheduler.unstable_yieldValue(
2037
+ 'ThrowsInLayoutEffectDestroy useLayoutEffect destroy',
2038
+ );
2039
+ throw Error('expected');
2040
+ };
2041
+ }, []);
2042
+ return <span prop="ThrowsInLayoutEffectDestroy" />;
2043
+ }
2044
+
2045
+ function App({children = null}) {
2046
+ Scheduler.unstable_yieldValue('App render');
2047
+ React.useLayoutEffect(() => {
2048
+ Scheduler.unstable_yieldValue('App create layout');
2049
+ return () => {
2050
+ Scheduler.unstable_yieldValue('App destroy layout');
2051
+ };
2052
+ }, []);
2053
+ return (
2054
+ <>
2055
+ <Suspense fallback={<Text text="Fallback" />}>
2056
+ {children}
2057
+ <ThrowsInLayoutEffectDestroy />
2058
+ <Text text="Inside" />
2059
+ </Suspense>
2060
+ <Text text="Outside" />
2061
+ </>
2062
+ );
2063
+ }
2064
+
2065
+ await ReactNoop.act(async () => {
2066
+ ReactNoop.render(
2067
+ <ErrorBoundary fallback={<Text text="Error" />}>
2068
+ <App />
2069
+ </ErrorBoundary>,
2070
+ );
2071
+ });
2072
+ expect(Scheduler).toHaveYielded([
2073
+ 'ErrorBoundary render: try',
2074
+ 'App render',
2075
+ 'ThrowsInLayoutEffectDestroy render',
2076
+ 'Text:Inside render',
2077
+ 'Text:Outside render',
2078
+ 'ThrowsInLayoutEffectDestroy useLayoutEffect create',
2079
+ 'Text:Inside create layout',
2080
+ 'Text:Outside create layout',
2081
+ 'App create layout',
2082
+ 'Text:Inside create passive',
2083
+ 'Text:Outside create passive',
2084
+ ]);
2085
+ expect(ReactNoop.getChildren()).toEqual([
2086
+ span('ThrowsInLayoutEffectDestroy'),
2087
+ span('Inside'),
2088
+ span('Outside'),
2089
+ ]);
2090
+
2091
+ // Schedule an update that suspends and triggers our error code.
2092
+ await ReactNoop.act(async () => {
2093
+ ReactNoop.render(
2094
+ <ErrorBoundary fallback={<Text text="Error" />}>
2095
+ <App>
2096
+ <AsyncText text="Async" ms={1000} />
2097
+ </App>
2098
+ </ErrorBoundary>,
2099
+ );
2100
+ });
2101
+ expect(Scheduler).toHaveYielded([
2102
+ 'ErrorBoundary render: try',
2103
+ 'App render',
2104
+ 'Suspend:Async',
2105
+ 'ThrowsInLayoutEffectDestroy render',
2106
+ 'Text:Inside render',
2107
+ 'Text:Fallback render',
2108
+ 'Text:Outside render',
2109
+
2110
+ // Even though an error was thrown in useLayoutEffect destroy,
2111
+ // subsequent layout effects should still be destroyed.
2112
+ 'ThrowsInLayoutEffectDestroy useLayoutEffect destroy',
2113
+ 'Text:Inside destroy layout',
2114
+
2115
+ // Finish the in-progess commit
2116
+ 'Text:Fallback create layout',
2117
+ 'Text:Fallback create passive',
2118
+
2119
+ // Destroy layout and passive effects in the errored tree.
2120
+ 'App destroy layout',
2121
+ 'Text:Fallback destroy layout',
2122
+ 'Text:Outside destroy layout',
2123
+ 'Text:Inside destroy passive',
2124
+ 'Text:Fallback destroy passive',
2125
+ 'Text:Outside destroy passive',
2126
+
2127
+ // Render fallback
2128
+ 'ErrorBoundary render: catch',
2129
+ 'Text:Error render',
2130
+ 'Text:Error create layout',
2131
+ 'Text:Error create passive',
2132
+ ]);
2133
+ expect(ReactNoop.getChildren()).toEqual([span('Error')]);
2134
+ });
2135
+ });
2136
+
2137
+ // @gate enableSuspenseLayoutEffectSemantics
2138
+ // @gate enableCache
2139
+ it('should be only destroy layout effects once if a tree suspends in multiple places', async () => {
2140
+ class ClassText extends React.Component {
2141
+ componentDidMount() {
2142
+ const {text} = this.props;
2143
+ Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
2144
+ }
2145
+ componentDidUpdate() {
2146
+ const {text} = this.props;
2147
+ Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
2148
+ }
2149
+ componentWillUnmount() {
2150
+ const {text} = this.props;
2151
+ Scheduler.unstable_yieldValue(
2152
+ `ClassText:${text} componentWillUnmount`,
2153
+ );
2154
+ }
2155
+ render() {
2156
+ const {children, text} = this.props;
2157
+ Scheduler.unstable_yieldValue(`ClassText:${text} render`);
2158
+ return <span prop={text}>{children}</span>;
2159
+ }
2160
+ }
2161
+
2162
+ function App({children = null}) {
2163
+ return (
2164
+ <Suspense fallback={<ClassText text="Fallback" />}>
2165
+ <Text text="Function" />
2166
+ {children}
2167
+ <ClassText text="Class" />
2168
+ </Suspense>
2169
+ );
2170
+ }
2171
+
2172
+ await ReactNoop.act(async () => {
2173
+ ReactNoop.render(<App />);
2174
+ });
2175
+ expect(Scheduler).toHaveYielded([
2176
+ 'Text:Function render',
2177
+ 'ClassText:Class render',
2178
+ 'Text:Function create layout',
2179
+ 'ClassText:Class componentDidMount',
2180
+ 'Text:Function create passive',
2181
+ ]);
2182
+ expect(ReactNoop.getChildren()).toEqual([
2183
+ span('Function'),
2184
+ span('Class'),
2185
+ ]);
2186
+
2187
+ // Schedule an update that causes React to suspend.
2188
+ ReactNoop.act(() => {
2189
+ ReactNoop.render(
2190
+ <App>
2191
+ <AsyncText text="Async_1" ms={1000} />
2192
+ <AsyncText text="Async_2" ms={2000} />
2193
+ </App>,
2194
+ );
2195
+ });
2196
+ expect(Scheduler).toHaveYielded([
2197
+ 'Text:Function render',
2198
+ 'Suspend:Async_1',
2199
+ 'Suspend:Async_2',
2200
+ 'ClassText:Class render',
2201
+ 'ClassText:Fallback render',
2202
+ ]);
2203
+ expect(ReactNoop.getChildren()).toEqual([
2204
+ span('Function'),
2205
+ span('Class'),
2206
+ ]);
2207
+
2208
+ await advanceTimers(1000);
2209
+
2210
+ // Timing out should commit the fallback and destroy inner layout effects.
2211
+ expect(Scheduler).toHaveYielded([
2212
+ 'Text:Function destroy layout',
2213
+ 'ClassText:Class componentWillUnmount',
2214
+ 'ClassText:Fallback componentDidMount',
2215
+ ]);
2216
+ expect(ReactNoop.getChildren()).toEqual([
2217
+ spanHidden('Function'),
2218
+ spanHidden('Class'),
2219
+ span('Fallback'),
2220
+ ]);
2221
+
2222
+ // Resolving the suspended resource should re-create inner layout effects.
2223
+ await ReactNoop.act(async () => {
2224
+ await resolveText('Async_1');
2225
+ });
2226
+ expect(Scheduler).toHaveYielded([
2227
+ 'Text:Function render',
2228
+ 'AsyncText:Async_1 render',
2229
+ 'Suspend:Async_2',
2230
+ 'ClassText:Class render',
2231
+ ]);
2232
+ expect(ReactNoop.getChildren()).toEqual([
2233
+ spanHidden('Function'),
2234
+ spanHidden('Class'),
2235
+ span('Fallback'),
2236
+ ]);
2237
+
2238
+ // Resolving the suspended resource should re-create inner layout effects.
2239
+ await ReactNoop.act(async () => {
2240
+ await resolveText('Async_2');
2241
+ });
2242
+ expect(Scheduler).toHaveYielded([
2243
+ 'Text:Function render',
2244
+ 'AsyncText:Async_1 render',
2245
+ 'AsyncText:Async_2 render',
2246
+ 'ClassText:Class render',
2247
+ 'ClassText:Fallback componentWillUnmount',
2248
+ 'Text:Function create layout',
2249
+ 'AsyncText:Async_1 create layout',
2250
+ 'AsyncText:Async_2 create layout',
2251
+ 'ClassText:Class componentDidMount',
2252
+ 'AsyncText:Async_1 create passive',
2253
+ 'AsyncText:Async_2 create passive',
2254
+ ]);
2255
+ expect(ReactNoop.getChildren()).toEqual([
2256
+ span('Function'),
2257
+ span('Async_1'),
2258
+ span('Async_2'),
2259
+ span('Class'),
2260
+ ]);
2261
+
2262
+ await ReactNoop.act(async () => {
2263
+ ReactNoop.render(null);
2264
+ });
2265
+ expect(Scheduler).toHaveYielded([
2266
+ 'Text:Function destroy layout',
2267
+ 'AsyncText:Async_1 destroy layout',
2268
+ 'AsyncText:Async_2 destroy layout',
2269
+ 'ClassText:Class componentWillUnmount',
2270
+ 'Text:Function destroy passive',
2271
+ 'AsyncText:Async_1 destroy passive',
2272
+ 'AsyncText:Async_2 destroy passive',
2273
+ ]);
2274
+ });
2275
+
2276
+ // @gate enableSuspenseLayoutEffectSemantics
2277
+ // @gate enableCache
2278
+ it('should be only destroy layout effects once if a component suspends multiple times', async () => {
2279
+ class ClassText extends React.Component {
2280
+ componentDidMount() {
2281
+ const {text} = this.props;
2282
+ Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
2283
+ }
2284
+ componentDidUpdate() {
2285
+ const {text} = this.props;
2286
+ Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
2287
+ }
2288
+ componentWillUnmount() {
2289
+ const {text} = this.props;
2290
+ Scheduler.unstable_yieldValue(
2291
+ `ClassText:${text} componentWillUnmount`,
2292
+ );
2293
+ }
2294
+ render() {
2295
+ const {children, text} = this.props;
2296
+ Scheduler.unstable_yieldValue(`ClassText:${text} render`);
2297
+ return <span prop={text}>{children}</span>;
2298
+ }
2299
+ }
2300
+
2301
+ let textToRead = null;
2302
+
2303
+ function Suspender() {
2304
+ Scheduler.unstable_yieldValue(`Suspender "${textToRead}" render`);
2305
+ if (textToRead !== null) {
2306
+ readText(textToRead);
2307
+ }
2308
+ return <span prop="Suspender" />;
2309
+ }
2310
+
2311
+ function App({children = null}) {
2312
+ return (
2313
+ <Suspense fallback={<ClassText text="Fallback" />}>
2314
+ <Text text="Function" />
2315
+ <Suspender />
2316
+ <ClassText text="Class" />
2317
+ </Suspense>
2318
+ );
2319
+ }
2320
+
2321
+ await ReactNoop.act(async () => {
2322
+ ReactNoop.render(<App />);
2323
+ });
2324
+ expect(Scheduler).toHaveYielded([
2325
+ 'Text:Function render',
2326
+ 'Suspender "null" render',
2327
+ 'ClassText:Class render',
2328
+ 'Text:Function create layout',
2329
+ 'ClassText:Class componentDidMount',
2330
+ 'Text:Function create passive',
2331
+ ]);
2332
+ expect(ReactNoop.getChildren()).toEqual([
2333
+ span('Function'),
2334
+ span('Suspender'),
2335
+ span('Class'),
2336
+ ]);
2337
+
2338
+ // Schedule an update that causes React to suspend.
2339
+ textToRead = 'A';
2340
+ ReactNoop.act(() => {
2341
+ ReactNoop.render(<App />);
2342
+ });
2343
+ expect(Scheduler).toHaveYielded([
2344
+ 'Text:Function render',
2345
+ 'Suspender "A" render',
2346
+ 'Suspend:A',
2347
+ 'ClassText:Class render',
2348
+ 'ClassText:Fallback render',
2349
+ ]);
2350
+ expect(ReactNoop.getChildren()).toEqual([
2351
+ span('Function'),
2352
+ span('Suspender'),
2353
+ span('Class'),
2354
+ ]);
2355
+
2356
+ await advanceTimers(1000);
2357
+
2358
+ // Timing out should commit the fallback and destroy inner layout effects.
2359
+ expect(Scheduler).toHaveYielded([
2360
+ 'Text:Function destroy layout',
2361
+ 'ClassText:Class componentWillUnmount',
2362
+ 'ClassText:Fallback componentDidMount',
2363
+ ]);
2364
+ expect(ReactNoop.getChildren()).toEqual([
2365
+ spanHidden('Function'),
2366
+ spanHidden('Suspender'),
2367
+ spanHidden('Class'),
2368
+ span('Fallback'),
2369
+ ]);
2370
+
2371
+ // Resolving the suspended resource should re-create inner layout effects.
2372
+ textToRead = 'B';
2373
+ await ReactNoop.act(async () => {
2374
+ await resolveText('A');
2375
+ });
2376
+ expect(Scheduler).toHaveYielded([
2377
+ 'Text:Function render',
2378
+ 'Suspender "B" render',
2379
+ 'Suspend:B',
2380
+ 'ClassText:Class render',
2381
+ ]);
2382
+ expect(ReactNoop.getChildren()).toEqual([
2383
+ spanHidden('Function'),
2384
+ spanHidden('Suspender'),
2385
+ spanHidden('Class'),
2386
+ span('Fallback'),
2387
+ ]);
2388
+
2389
+ // Resolving the suspended resource should re-create inner layout effects.
2390
+ await ReactNoop.act(async () => {
2391
+ await resolveText('B');
2392
+ });
2393
+ expect(Scheduler).toHaveYielded([
2394
+ 'Text:Function render',
2395
+ 'Suspender "B" render',
2396
+ 'ClassText:Class render',
2397
+ 'ClassText:Fallback componentWillUnmount',
2398
+ 'Text:Function create layout',
2399
+ 'ClassText:Class componentDidMount',
2400
+ ]);
2401
+ expect(ReactNoop.getChildren()).toEqual([
2402
+ span('Function'),
2403
+ span('Suspender'),
2404
+ span('Class'),
2405
+ ]);
2406
+
2407
+ await ReactNoop.act(async () => {
2408
+ ReactNoop.render(null);
2409
+ });
2410
+ expect(Scheduler).toHaveYielded([
2411
+ 'Text:Function destroy layout',
2412
+ 'ClassText:Class componentWillUnmount',
2413
+ 'Text:Function destroy passive',
2414
+ ]);
2415
+ });
2416
+ });
2417
+
2418
+ describe('refs within a tree that re-suspends in an update', () => {
2419
+ function RefCheckerOuter({Component}) {
2420
+ const refObject = React.useRef(null);
2421
+
2422
+ const manualRef = React.useMemo(() => ({current: null}), []);
2423
+ const refCallback = React.useCallback(value => {
2424
+ Scheduler.unstable_yieldValue(
2425
+ `RefCheckerOuter refCallback value? ${value != null}`,
2426
+ );
2427
+ manualRef.current = value;
2428
+ }, []);
2429
+
2430
+ Scheduler.unstable_yieldValue(`RefCheckerOuter render`);
2431
+
2432
+ React.useLayoutEffect(() => {
2433
+ Scheduler.unstable_yieldValue(
2434
+ `RefCheckerOuter create layout refObject? ${refObject.current !=
2435
+ null} refCallback? ${manualRef.current != null}`,
2436
+ );
2437
+ return () => {
2438
+ Scheduler.unstable_yieldValue(
2439
+ `RefCheckerOuter destroy layout refObject? ${refObject.current !=
2440
+ null} refCallback? ${manualRef.current != null}`,
2441
+ );
2442
+ };
2443
+ }, []);
2444
+
2445
+ return (
2446
+ <>
2447
+ <Component ref={refObject} prop="refObject">
2448
+ <RefCheckerInner forwardedRef={refObject} text="refObject" />
2449
+ </Component>
2450
+ <Component ref={refCallback} prop="refCallback">
2451
+ <RefCheckerInner forwardedRef={manualRef} text="refCallback" />
2452
+ </Component>
2453
+ </>
2454
+ );
2455
+ }
2456
+
2457
+ function RefCheckerInner({forwardedRef, text}) {
2458
+ Scheduler.unstable_yieldValue(`RefCheckerInner:${text} render`);
2459
+ React.useLayoutEffect(() => {
2460
+ Scheduler.unstable_yieldValue(
2461
+ `RefCheckerInner:${text} create layout ref? ${forwardedRef.current !=
2462
+ null}`,
2463
+ );
2464
+ return () => {
2465
+ Scheduler.unstable_yieldValue(
2466
+ `RefCheckerInner:${text} destroy layout ref? ${forwardedRef.current !=
2467
+ null}`,
2468
+ );
2469
+ };
2470
+ }, []);
2471
+ return null;
2472
+ }
2473
+
2474
+ // @gate enableCache
2475
+ it('should not be cleared within legacy roots', async () => {
2476
+ class ClassComponent extends React.Component {
2477
+ render() {
2478
+ Scheduler.unstable_yieldValue(
2479
+ `ClassComponent:${this.props.prop} render`,
2480
+ );
2481
+ return this.props.children;
2482
+ }
2483
+ }
2484
+
2485
+ function App({children}) {
2486
+ Scheduler.unstable_yieldValue(`App render`);
2487
+ return (
2488
+ <Suspense fallback={<Text text="Fallback" />}>
2489
+ {children}
2490
+ <RefCheckerOuter Component={ClassComponent} />
2491
+ </Suspense>
2492
+ );
2493
+ }
2494
+
2495
+ ReactNoop.act(() => {
2496
+ ReactNoop.renderLegacySyncRoot(<App />);
2497
+ });
2498
+ expect(Scheduler).toHaveYielded([
2499
+ 'App render',
2500
+ 'RefCheckerOuter render',
2501
+ 'ClassComponent:refObject render',
2502
+ 'RefCheckerInner:refObject render',
2503
+ 'ClassComponent:refCallback render',
2504
+ 'RefCheckerInner:refCallback render',
2505
+ 'RefCheckerInner:refObject create layout ref? false',
2506
+ 'RefCheckerInner:refCallback create layout ref? false',
2507
+ 'RefCheckerOuter refCallback value? true',
2508
+ 'RefCheckerOuter create layout refObject? true refCallback? true',
2509
+ ]);
2510
+ expect(ReactNoop.getChildren()).toEqual([]);
2511
+
2512
+ // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2513
+ ReactNoop.act(() => {
2514
+ ReactNoop.renderLegacySyncRoot(
2515
+ <App children={<AsyncText text="Async" ms={1000} />} />,
2516
+ );
2517
+ });
2518
+ await advanceTimers(1000);
2519
+ expect(Scheduler).toHaveYielded([
2520
+ 'App render',
2521
+ 'Suspend:Async',
2522
+ 'RefCheckerOuter render',
2523
+ 'ClassComponent:refObject render',
2524
+ 'RefCheckerInner:refObject render',
2525
+ 'ClassComponent:refCallback render',
2526
+ 'RefCheckerInner:refCallback render',
2527
+ 'Text:Fallback render',
2528
+ 'Text:Fallback create layout',
2529
+ 'Text:Fallback create passive',
2530
+ ]);
2531
+ expect(ReactNoop.getChildren()).toEqual([span('Fallback')]);
2532
+
2533
+ // Resolving the suspended resource should re-create inner layout effects.
2534
+ await ReactNoop.act(async () => {
2535
+ await resolveText('Async');
2536
+ });
2537
+ expect(Scheduler).toHaveYielded([
2538
+ 'AsyncText:Async render',
2539
+ 'Text:Fallback destroy layout',
2540
+ 'AsyncText:Async create layout',
2541
+ 'Text:Fallback destroy passive',
2542
+ 'AsyncText:Async create passive',
2543
+ ]);
2544
+ expect(ReactNoop.getChildren()).toEqual([span('Async')]);
2545
+
2546
+ await ReactNoop.act(async () => {
2547
+ ReactNoop.renderLegacySyncRoot(null);
2548
+ });
2549
+ expect(Scheduler).toHaveYielded([
2550
+ 'AsyncText:Async destroy layout',
2551
+ 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2552
+ 'RefCheckerInner:refObject destroy layout ref? false',
2553
+ 'RefCheckerOuter refCallback value? false',
2554
+ 'RefCheckerInner:refCallback destroy layout ref? false',
2555
+ 'AsyncText:Async destroy passive',
2556
+ ]);
2557
+ expect(ReactNoop.getChildren()).toEqual([]);
2558
+ });
2559
+
2560
+ // @gate enableSuspenseLayoutEffectSemantics
2561
+ // @gate enableCache
2562
+ it('should be cleared and reset for host components', async () => {
2563
+ function App({children}) {
2564
+ Scheduler.unstable_yieldValue(`App render`);
2565
+ return (
2566
+ <Suspense fallback={<Text text="Fallback" />}>
2567
+ {children}
2568
+ <RefCheckerOuter Component="span" />
2569
+ </Suspense>
2570
+ );
2571
+ }
2572
+
2573
+ // Mount
2574
+ await ReactNoop.act(async () => {
2575
+ ReactNoop.render(<App />);
2576
+ });
2577
+ expect(Scheduler).toHaveYielded([
2578
+ 'App render',
2579
+ 'RefCheckerOuter render',
2580
+ 'RefCheckerInner:refObject render',
2581
+ 'RefCheckerInner:refCallback render',
2582
+ 'RefCheckerInner:refObject create layout ref? false',
2583
+ 'RefCheckerInner:refCallback create layout ref? false',
2584
+ 'RefCheckerOuter refCallback value? true',
2585
+ 'RefCheckerOuter create layout refObject? true refCallback? true',
2586
+ ]);
2587
+ expect(ReactNoop.getChildren()).toEqual([
2588
+ span('refObject'),
2589
+ span('refCallback'),
2590
+ ]);
2591
+
2592
+ // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2593
+ ReactNoop.act(() => {
2594
+ ReactNoop.render(
2595
+ <App children={<AsyncText text="Async" ms={1000} />} />,
2596
+ );
2597
+ });
2598
+ await advanceTimers(1000);
2599
+ expect(Scheduler).toHaveYielded([
2600
+ 'App render',
2601
+ 'Suspend:Async',
2602
+ 'RefCheckerOuter render',
2603
+ 'RefCheckerInner:refObject render',
2604
+ 'RefCheckerInner:refCallback render',
2605
+ 'Text:Fallback render',
2606
+ 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2607
+ 'RefCheckerInner:refObject destroy layout ref? false',
2608
+ 'RefCheckerOuter refCallback value? false',
2609
+ 'RefCheckerInner:refCallback destroy layout ref? false',
2610
+ 'Text:Fallback create layout',
2611
+ ]);
2612
+ expect(ReactNoop.getChildren()).toEqual([
2613
+ spanHidden('refObject'),
2614
+ spanHidden('refCallback'),
2615
+ span('Fallback'),
2616
+ ]);
2617
+
2618
+ // Resolving the suspended resource should re-create inner layout effects.
2619
+ await ReactNoop.act(async () => {
2620
+ await resolveText('Async');
2621
+ });
2622
+ expect(Scheduler).toHaveYielded([
2623
+ 'Text:Fallback create passive',
2624
+ 'AsyncText:Async render',
2625
+ 'RefCheckerOuter render',
2626
+ 'RefCheckerInner:refObject render',
2627
+ 'RefCheckerInner:refCallback render',
2628
+ 'Text:Fallback destroy layout',
2629
+ 'AsyncText:Async create layout',
2630
+ 'RefCheckerInner:refObject create layout ref? false',
2631
+ 'RefCheckerInner:refCallback create layout ref? false',
2632
+ 'RefCheckerOuter refCallback value? true',
2633
+ 'RefCheckerOuter create layout refObject? true refCallback? true',
2634
+ 'Text:Fallback destroy passive',
2635
+ 'AsyncText:Async create passive',
2636
+ ]);
2637
+ expect(ReactNoop.getChildren()).toEqual([
2638
+ span('Async'),
2639
+ span('refObject'),
2640
+ span('refCallback'),
2641
+ ]);
2642
+
2643
+ await ReactNoop.act(async () => {
2644
+ ReactNoop.render(null);
2645
+ });
2646
+ expect(Scheduler).toHaveYielded([
2647
+ 'AsyncText:Async destroy layout',
2648
+ 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2649
+ 'RefCheckerInner:refObject destroy layout ref? false',
2650
+ 'RefCheckerOuter refCallback value? false',
2651
+ 'RefCheckerInner:refCallback destroy layout ref? false',
2652
+ 'AsyncText:Async destroy passive',
2653
+ ]);
2654
+ expect(ReactNoop.getChildren()).toEqual([]);
2655
+ });
2656
+
2657
+ // @gate enableSuspenseLayoutEffectSemantics
2658
+ // @gate enableCache
2659
+ it('should be cleared and reset for class components', async () => {
2660
+ class ClassComponent extends React.Component {
2661
+ render() {
2662
+ Scheduler.unstable_yieldValue(
2663
+ `ClassComponent:${this.props.prop} render`,
2664
+ );
2665
+ return this.props.children;
2666
+ }
2667
+ }
2668
+
2669
+ function App({children}) {
2670
+ Scheduler.unstable_yieldValue(`App render`);
2671
+ return (
2672
+ <Suspense fallback={<Text text="Fallback" />}>
2673
+ {children}
2674
+ <RefCheckerOuter Component={ClassComponent} />
2675
+ </Suspense>
2676
+ );
2677
+ }
2678
+
2679
+ // Mount
2680
+ await ReactNoop.act(async () => {
2681
+ ReactNoop.render(<App />);
2682
+ });
2683
+ expect(Scheduler).toHaveYielded([
2684
+ 'App render',
2685
+ 'RefCheckerOuter render',
2686
+ 'ClassComponent:refObject render',
2687
+ 'RefCheckerInner:refObject render',
2688
+ 'ClassComponent:refCallback render',
2689
+ 'RefCheckerInner:refCallback render',
2690
+ 'RefCheckerInner:refObject create layout ref? false',
2691
+ 'RefCheckerInner:refCallback create layout ref? false',
2692
+ 'RefCheckerOuter refCallback value? true',
2693
+ 'RefCheckerOuter create layout refObject? true refCallback? true',
2694
+ ]);
2695
+ expect(ReactNoop.getChildren()).toEqual([]);
2696
+
2697
+ // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2698
+ ReactNoop.act(() => {
2699
+ ReactNoop.render(
2700
+ <App children={<AsyncText text="Async" ms={1000} />} />,
2701
+ );
2702
+ });
2703
+ await advanceTimers(1000);
2704
+ expect(Scheduler).toHaveYielded([
2705
+ 'App render',
2706
+ 'Suspend:Async',
2707
+ 'RefCheckerOuter render',
2708
+ 'ClassComponent:refObject render',
2709
+ 'RefCheckerInner:refObject render',
2710
+ 'ClassComponent:refCallback render',
2711
+ 'RefCheckerInner:refCallback render',
2712
+ 'Text:Fallback render',
2713
+ 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2714
+ 'RefCheckerInner:refObject destroy layout ref? false',
2715
+ 'RefCheckerOuter refCallback value? false',
2716
+ 'RefCheckerInner:refCallback destroy layout ref? false',
2717
+ 'Text:Fallback create layout',
2718
+ ]);
2719
+ expect(ReactNoop.getChildren()).toEqual([span('Fallback')]);
2720
+
2721
+ // Resolving the suspended resource should re-create inner layout effects.
2722
+ await ReactNoop.act(async () => {
2723
+ await resolveText('Async');
2724
+ });
2725
+ expect(Scheduler).toHaveYielded([
2726
+ 'Text:Fallback create passive',
2727
+ 'AsyncText:Async render',
2728
+ 'RefCheckerOuter render',
2729
+ 'ClassComponent:refObject render',
2730
+ 'RefCheckerInner:refObject render',
2731
+ 'ClassComponent:refCallback render',
2732
+ 'RefCheckerInner:refCallback render',
2733
+ 'Text:Fallback destroy layout',
2734
+ 'AsyncText:Async create layout',
2735
+ 'RefCheckerInner:refObject create layout ref? false',
2736
+ 'RefCheckerInner:refCallback create layout ref? false',
2737
+ 'RefCheckerOuter refCallback value? true',
2738
+ 'RefCheckerOuter create layout refObject? true refCallback? true',
2739
+ 'Text:Fallback destroy passive',
2740
+ 'AsyncText:Async create passive',
2741
+ ]);
2742
+ expect(ReactNoop.getChildren()).toEqual([span('Async')]);
2743
+
2744
+ await ReactNoop.act(async () => {
2745
+ ReactNoop.render(null);
2746
+ });
2747
+ expect(Scheduler).toHaveYielded([
2748
+ 'AsyncText:Async destroy layout',
2749
+ 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2750
+ 'RefCheckerInner:refObject destroy layout ref? false',
2751
+ 'RefCheckerOuter refCallback value? false',
2752
+ 'RefCheckerInner:refCallback destroy layout ref? false',
2753
+ 'AsyncText:Async destroy passive',
2754
+ ]);
2755
+ expect(ReactNoop.getChildren()).toEqual([]);
2756
+ });
2757
+
2758
+ // @gate enableSuspenseLayoutEffectSemantics
2759
+ // @gate enableCache
2760
+ it('should be cleared and reset for function components with useImperativeHandle', async () => {
2761
+ const FunctionComponent = React.forwardRef((props, ref) => {
2762
+ Scheduler.unstable_yieldValue('FunctionComponent render');
2763
+ React.useImperativeHandle(
2764
+ ref,
2765
+ () => ({
2766
+ // Noop
2767
+ }),
2768
+ [],
2769
+ );
2770
+ return props.children;
2771
+ });
2772
+ FunctionComponent.displayName = 'FunctionComponent';
2773
+
2774
+ function App({children}) {
2775
+ Scheduler.unstable_yieldValue(`App render`);
2776
+ return (
2777
+ <Suspense fallback={<Text text="Fallback" />}>
2778
+ {children}
2779
+ <RefCheckerOuter Component={FunctionComponent} />
2780
+ </Suspense>
2781
+ );
2782
+ }
2783
+
2784
+ // Mount
2785
+ await ReactNoop.act(async () => {
2786
+ ReactNoop.render(<App />);
2787
+ });
2788
+ expect(Scheduler).toHaveYielded([
2789
+ 'App render',
2790
+ 'RefCheckerOuter render',
2791
+ 'FunctionComponent render',
2792
+ 'RefCheckerInner:refObject render',
2793
+ 'FunctionComponent render',
2794
+ 'RefCheckerInner:refCallback render',
2795
+ 'RefCheckerInner:refObject create layout ref? false',
2796
+ 'RefCheckerInner:refCallback create layout ref? false',
2797
+ 'RefCheckerOuter refCallback value? true',
2798
+ 'RefCheckerOuter create layout refObject? true refCallback? true',
2799
+ ]);
2800
+ expect(ReactNoop.getChildren()).toEqual([]);
2801
+
2802
+ // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2803
+ ReactNoop.act(() => {
2804
+ ReactNoop.render(
2805
+ <App children={<AsyncText text="Async" ms={1000} />} />,
2806
+ );
2807
+ });
2808
+ await advanceTimers(1000);
2809
+ expect(Scheduler).toHaveYielded([
2810
+ 'App render',
2811
+ 'Suspend:Async',
2812
+ 'RefCheckerOuter render',
2813
+ 'FunctionComponent render',
2814
+ 'RefCheckerInner:refObject render',
2815
+ 'FunctionComponent render',
2816
+ 'RefCheckerInner:refCallback render',
2817
+ 'Text:Fallback render',
2818
+ 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2819
+ 'RefCheckerInner:refObject destroy layout ref? false',
2820
+ 'RefCheckerOuter refCallback value? false',
2821
+ 'RefCheckerInner:refCallback destroy layout ref? false',
2822
+ 'Text:Fallback create layout',
2823
+ ]);
2824
+ expect(ReactNoop.getChildren()).toEqual([span('Fallback')]);
2825
+
2826
+ // Resolving the suspended resource should re-create inner layout effects.
2827
+ await ReactNoop.act(async () => {
2828
+ await resolveText('Async');
2829
+ });
2830
+ expect(Scheduler).toHaveYielded([
2831
+ 'Text:Fallback create passive',
2832
+ 'AsyncText:Async render',
2833
+ 'RefCheckerOuter render',
2834
+ 'FunctionComponent render',
2835
+ 'RefCheckerInner:refObject render',
2836
+ 'FunctionComponent render',
2837
+ 'RefCheckerInner:refCallback render',
2838
+ 'Text:Fallback destroy layout',
2839
+ 'AsyncText:Async create layout',
2840
+ 'RefCheckerInner:refObject create layout ref? false',
2841
+ 'RefCheckerInner:refCallback create layout ref? false',
2842
+ 'RefCheckerOuter refCallback value? true',
2843
+ 'RefCheckerOuter create layout refObject? true refCallback? true',
2844
+ 'Text:Fallback destroy passive',
2845
+ 'AsyncText:Async create passive',
2846
+ ]);
2847
+ expect(ReactNoop.getChildren()).toEqual([span('Async')]);
2848
+
2849
+ await ReactNoop.act(async () => {
2850
+ ReactNoop.render(null);
2851
+ });
2852
+ expect(Scheduler).toHaveYielded([
2853
+ 'AsyncText:Async destroy layout',
2854
+ 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2855
+ 'RefCheckerInner:refObject destroy layout ref? false',
2856
+ 'RefCheckerOuter refCallback value? false',
2857
+ 'RefCheckerInner:refCallback destroy layout ref? false',
2858
+ 'AsyncText:Async destroy passive',
2859
+ ]);
2860
+ expect(ReactNoop.getChildren()).toEqual([]);
2861
+ });
2862
+
2863
+ // @gate enableSuspenseLayoutEffectSemantics
2864
+ // @gate enableCache
2865
+ it('should not reset for user-managed values', async () => {
2866
+ function RefChecker({forwardedRef}) {
2867
+ Scheduler.unstable_yieldValue(`RefChecker render`);
2868
+ React.useLayoutEffect(() => {
2869
+ Scheduler.unstable_yieldValue(
2870
+ `RefChecker create layout ref? ${forwardedRef.current === 'test'}`,
2871
+ );
2872
+ return () => {
2873
+ Scheduler.unstable_yieldValue(
2874
+ `RefChecker destroy layout ref? ${forwardedRef.current ===
2875
+ 'test'}`,
2876
+ );
2877
+ };
2878
+ }, []);
2879
+ return null;
2880
+ }
2881
+
2882
+ function App({children = null}) {
2883
+ const ref = React.useRef('test');
2884
+ Scheduler.unstable_yieldValue(`App render`);
2885
+ React.useLayoutEffect(() => {
2886
+ Scheduler.unstable_yieldValue(
2887
+ `App create layout ref? ${ref.current === 'test'}`,
2888
+ );
2889
+ return () => {
2890
+ Scheduler.unstable_yieldValue(
2891
+ `App destroy layout ref? ${ref.current === 'test'}`,
2892
+ );
2893
+ };
2894
+ }, []);
2895
+ return (
2896
+ <Suspense fallback={<Text text="Fallback" />}>
2897
+ {children}
2898
+ <RefChecker forwardedRef={ref} />
2899
+ </Suspense>
2900
+ );
2901
+ }
2902
+
2903
+ // Mount
2904
+ await ReactNoop.act(async () => {
2905
+ ReactNoop.render(<App />);
2906
+ });
2907
+ expect(Scheduler).toHaveYielded([
2908
+ 'App render',
2909
+ 'RefChecker render',
2910
+ 'RefChecker create layout ref? true',
2911
+ 'App create layout ref? true',
2912
+ ]);
2913
+ expect(ReactNoop.getChildren()).toEqual([]);
2914
+
2915
+ // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2916
+ ReactNoop.act(() => {
2917
+ ReactNoop.render(
2918
+ <App children={<AsyncText text="Async" ms={1000} />} />,
2919
+ );
2920
+ });
2921
+ await advanceTimers(1000);
2922
+ expect(Scheduler).toHaveYielded([
2923
+ 'App render',
2924
+ 'Suspend:Async',
2925
+ 'RefChecker render',
2926
+ 'Text:Fallback render',
2927
+ 'RefChecker destroy layout ref? true',
2928
+ 'Text:Fallback create layout',
2929
+ ]);
2930
+ expect(ReactNoop.getChildren()).toEqual([span('Fallback')]);
2931
+
2932
+ // Resolving the suspended resource should re-create inner layout effects.
2933
+ await ReactNoop.act(async () => {
2934
+ await resolveText('Async');
2935
+ });
2936
+ expect(Scheduler).toHaveYielded([
2937
+ 'Text:Fallback create passive',
2938
+ 'AsyncText:Async render',
2939
+ 'RefChecker render',
2940
+ 'Text:Fallback destroy layout',
2941
+ 'AsyncText:Async create layout',
2942
+ 'RefChecker create layout ref? true',
2943
+ 'Text:Fallback destroy passive',
2944
+ 'AsyncText:Async create passive',
2945
+ ]);
2946
+ expect(ReactNoop.getChildren()).toEqual([span('Async')]);
2947
+
2948
+ await ReactNoop.act(async () => {
2949
+ ReactNoop.render(null);
2950
+ });
2951
+ expect(Scheduler).toHaveYielded([
2952
+ 'App destroy layout ref? true',
2953
+ 'AsyncText:Async destroy layout',
2954
+ 'RefChecker destroy layout ref? true',
2955
+ 'AsyncText:Async destroy passive',
2956
+ ]);
2957
+ expect(ReactNoop.getChildren()).toEqual([]);
2958
+ });
2959
+
2960
+ describe('that throw errors', () => {
2961
+ // @gate enableSuspenseLayoutEffectSemantics
2962
+ // @gate enableCache
2963
+ // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
2964
+ it('are properly handled in ref callbacks', async () => {
2965
+ let useRefCallbackShouldThrow = false;
2966
+
2967
+ function ThrowsInRefCallback() {
2968
+ Scheduler.unstable_yieldValue('ThrowsInRefCallback render');
2969
+ const refCallback = React.useCallback(value => {
2970
+ Scheduler.unstable_yieldValue(
2971
+ 'ThrowsInRefCallback refCallback ref? ' + !!value,
2972
+ );
2973
+ if (useRefCallbackShouldThrow) {
2974
+ throw Error('expected');
2975
+ }
2976
+ }, []);
2977
+ return <span ref={refCallback} prop="ThrowsInRefCallback" />;
2978
+ }
2979
+
2980
+ function App({children = null}) {
2981
+ Scheduler.unstable_yieldValue('App render');
2982
+ React.useLayoutEffect(() => {
2983
+ Scheduler.unstable_yieldValue('App create layout');
2984
+ return () => {
2985
+ Scheduler.unstable_yieldValue('App destroy layout');
2986
+ };
2987
+ }, []);
2988
+ return (
2989
+ <>
2990
+ <Suspense fallback={<Text text="Fallback" />}>
2991
+ {children}
2992
+ <ThrowsInRefCallback />
2993
+ <Text text="Inside" />
2994
+ </Suspense>
2995
+ <Text text="Outside" />
2996
+ </>
2997
+ );
2998
+ }
2999
+
3000
+ await ReactNoop.act(async () => {
3001
+ ReactNoop.render(
3002
+ <ErrorBoundary fallback={<Text text="Error" />}>
3003
+ <App />
3004
+ </ErrorBoundary>,
3005
+ );
3006
+ });
3007
+ expect(Scheduler).toHaveYielded([
3008
+ 'ErrorBoundary render: try',
3009
+ 'App render',
3010
+ 'ThrowsInRefCallback render',
3011
+ 'Text:Inside render',
3012
+ 'Text:Outside render',
3013
+ 'ThrowsInRefCallback refCallback ref? true',
3014
+ 'Text:Inside create layout',
3015
+ 'Text:Outside create layout',
3016
+ 'App create layout',
3017
+ 'Text:Inside create passive',
3018
+ 'Text:Outside create passive',
3019
+ ]);
3020
+ expect(ReactNoop.getChildren()).toEqual([
3021
+ span('ThrowsInRefCallback'),
3022
+ span('Inside'),
3023
+ span('Outside'),
3024
+ ]);
3025
+
3026
+ // Schedule an update that causes React to suspend.
3027
+ await ReactNoop.act(async () => {
3028
+ ReactNoop.render(
3029
+ <ErrorBoundary fallback={<Text text="Error" />}>
3030
+ <App>
3031
+ <AsyncText text="Async" ms={1000} />
3032
+ </App>
3033
+ </ErrorBoundary>,
3034
+ );
3035
+ });
3036
+ expect(Scheduler).toHaveYielded([
3037
+ 'ErrorBoundary render: try',
3038
+ 'App render',
3039
+ 'Suspend:Async',
3040
+ 'ThrowsInRefCallback render',
3041
+ 'Text:Inside render',
3042
+ 'Text:Fallback render',
3043
+ 'Text:Outside render',
3044
+ 'ThrowsInRefCallback refCallback ref? false',
3045
+ 'Text:Inside destroy layout',
3046
+ 'Text:Fallback create layout',
3047
+ 'Text:Fallback create passive',
3048
+ ]);
3049
+ expect(ReactNoop.getChildren()).toEqual([
3050
+ spanHidden('ThrowsInRefCallback'),
3051
+ spanHidden('Inside'),
3052
+ span('Fallback'),
3053
+ span('Outside'),
3054
+ ]);
3055
+
3056
+ // Resolve the pending suspense and throw
3057
+ useRefCallbackShouldThrow = true;
3058
+ await ReactNoop.act(async () => {
3059
+ await resolveText('Async');
3060
+ });
3061
+ expect(Scheduler).toHaveYielded([
3062
+ 'AsyncText:Async render',
3063
+ 'ThrowsInRefCallback render',
3064
+ 'Text:Inside render',
3065
+
3066
+ // Even though an error was thrown in refCallback,
3067
+ // subsequent layout effects should still be created.
3068
+ 'Text:Fallback destroy layout',
3069
+ 'AsyncText:Async create layout',
3070
+ 'ThrowsInRefCallback refCallback ref? true',
3071
+ 'Text:Inside create layout',
3072
+
3073
+ // Finish the in-progress commit
3074
+ 'Text:Fallback destroy passive',
3075
+ 'AsyncText:Async create passive',
3076
+
3077
+ // Destroy layout and passive effects in the errored tree.
3078
+ 'App destroy layout',
3079
+ 'AsyncText:Async destroy layout',
3080
+ 'ThrowsInRefCallback refCallback ref? false',
3081
+ 'Text:Inside destroy layout',
3082
+ 'Text:Outside destroy layout',
3083
+ 'AsyncText:Async destroy passive',
3084
+ 'Text:Inside destroy passive',
3085
+ 'Text:Outside destroy passive',
3086
+
3087
+ // Render fallback
3088
+ 'ErrorBoundary render: catch',
3089
+ 'Text:Error render',
3090
+ 'Text:Error create layout',
3091
+ 'Text:Error create passive',
3092
+ ]);
3093
+ expect(ReactNoop.getChildren()).toEqual([span('Error')]);
3094
+ });
3095
+ });
3096
+ });
3097
+});