1
+let React;
2
+let ReactFeatureFlags;
3
+let Fragment;
4
+let ReactNoop;
5
+let Scheduler;
6
+let Suspense;
7
+let SuspenseList;
8
+
9
+describe('ReactSuspenseList', () => {
10
+ beforeEach(() => {
11
+ jest.resetModules();
12
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
13
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
14
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
15
+ React = require('react');
16
+ Fragment = React.Fragment;
17
+ ReactNoop = require('react-noop-renderer');
18
+ Scheduler = require('scheduler');
19
+ Suspense = React.Suspense;
20
+ SuspenseList = React.unstable_SuspenseList;
21
+ });
22
+
23
+ function Text(props) {
24
+ Scheduler.yieldValue(props.text);
25
+ return <span>{props.text}</span>;
26
+ }
27
+
28
+ function createAsyncText(text) {
29
+ let resolved = false;
30
+ let Component = function() {
31
+ if (!resolved) {
32
+ Scheduler.yieldValue('Suspend! [' + text + ']');
33
+ throw promise;
34
+ }
35
+ return <Text text={text} />;
36
+ };
37
+ let promise = new Promise(resolve => {
38
+ Component.resolve = function() {
39
+ resolved = true;
40
+ return resolve();
41
+ };
42
+ });
43
+ return Component;
44
+ }
45
+
46
+ it('warns if an unsupported revealOrder option is used', () => {
47
+ function Foo() {
48
+ return (
49
+ <SuspenseList revealOrder="something">
50
+ <Suspense fallback="Loading">Content</Suspense>
51
+ </SuspenseList>
52
+ );
53
+ }
54
+
55
+ ReactNoop.render(<Foo />);
56
+
57
+ expect(() => Scheduler.flushAll()).toWarnDev([
58
+ 'Warning: "something" is not a supported revealOrder on ' +
59
+ '<SuspenseList />. Did you mean "together"?' +
60
+ '\n in SuspenseList (at **)' +
61
+ '\n in Foo (at **)',
62
+ ]);
63
+ });
64
+
65
+ it('shows content independently by default', async () => {
66
+ let A = createAsyncText('A');
67
+ let B = createAsyncText('B');
68
+ let C = createAsyncText('C');
69
+
70
+ function Foo() {
71
+ return (
72
+ <SuspenseList>
73
+ <Suspense fallback={<Text text="Loading A" />}>
74
+ <A />
75
+ </Suspense>
76
+ <Suspense fallback={<Text text="Loading B" />}>
77
+ <B />
78
+ </Suspense>
79
+ <Suspense fallback={<Text text="Loading C" />}>
80
+ <C />
81
+ </Suspense>
82
+ </SuspenseList>
83
+ );
84
+ }
85
+
86
+ await A.resolve();
87
+
88
+ ReactNoop.render(<Foo />);
89
+
90
+ expect(Scheduler).toFlushAndYield([
91
+ 'A',
92
+ 'Suspend! [B]',
93
+ 'Loading B',
94
+ 'Suspend! [C]',
95
+ 'Loading C',
96
+ ]);
97
+
98
+ expect(ReactNoop).toMatchRenderedOutput(
99
+ <Fragment>
100
+ <span>A</span>
101
+ <span>Loading B</span>
102
+ <span>Loading C</span>
103
+ </Fragment>,
104
+ );
105
+
106
+ await C.resolve();
107
+
108
+ expect(Scheduler).toFlushAndYield(['C']);
109
+
110
+ expect(ReactNoop).toMatchRenderedOutput(
111
+ <Fragment>
112
+ <span>A</span>
113
+ <span>Loading B</span>
114
+ <span>C</span>
115
+ </Fragment>,
116
+ );
117
+
118
+ await B.resolve();
119
+
120
+ expect(Scheduler).toFlushAndYield(['B']);
121
+
122
+ expect(ReactNoop).toMatchRenderedOutput(
123
+ <Fragment>
124
+ <span>A</span>
125
+ <span>B</span>
126
+ <span>C</span>
127
+ </Fragment>,
128
+ );
129
+ });
130
+
131
+ it('shows content independently in legacy mode regardless of option', async () => {
132
+ let A = createAsyncText('A');
133
+ let B = createAsyncText('B');
134
+ let C = createAsyncText('C');
135
+
136
+ function Foo() {
137
+ return (
138
+ <SuspenseList revealOrder="together">
139
+ <Suspense fallback={<Text text="Loading A" />}>
140
+ <A />
141
+ </Suspense>
142
+ <Suspense fallback={<Text text="Loading B" />}>
143
+ <B />
144
+ </Suspense>
145
+ <Suspense fallback={<Text text="Loading C" />}>
146
+ <C />
147
+ </Suspense>
148
+ </SuspenseList>
149
+ );
150
+ }
151
+
152
+ await A.resolve();
153
+
154
+ ReactNoop.renderLegacySyncRoot(<Foo />);
155
+
156
+ expect(Scheduler).toHaveYielded([
157
+ 'A',
158
+ 'Suspend! [B]',
159
+ 'Loading B',
160
+ 'Suspend! [C]',
161
+ 'Loading C',
162
+ ]);
163
+
164
+ expect(ReactNoop).toMatchRenderedOutput(
165
+ <Fragment>
166
+ <span>A</span>
167
+ <span>Loading B</span>
168
+ <span>Loading C</span>
169
+ </Fragment>,
170
+ );
171
+
172
+ await C.resolve();
173
+
174
+ expect(Scheduler).toFlushAndYield(['C']);
175
+
176
+ expect(ReactNoop).toMatchRenderedOutput(
177
+ <Fragment>
178
+ <span>A</span>
179
+ <span>Loading B</span>
180
+ <span>C</span>
181
+ </Fragment>,
182
+ );
183
+
184
+ await B.resolve();
185
+
186
+ expect(Scheduler).toFlushAndYield(['B']);
187
+
188
+ expect(ReactNoop).toMatchRenderedOutput(
189
+ <Fragment>
190
+ <span>A</span>
191
+ <span>B</span>
192
+ <span>C</span>
193
+ </Fragment>,
194
+ );
195
+ });
196
+
197
+ it('displays all "together"', async () => {
198
+ let A = createAsyncText('A');
199
+ let B = createAsyncText('B');
200
+ let C = createAsyncText('C');
201
+
202
+ function Foo() {
203
+ return (
204
+ <SuspenseList revealOrder="together">
205
+ <Suspense fallback={<Text text="Loading A" />}>
206
+ <A />
207
+ </Suspense>
208
+ <Suspense fallback={<Text text="Loading B" />}>
209
+ <B />
210
+ </Suspense>
211
+ <Suspense fallback={<Text text="Loading C" />}>
212
+ <C />
213
+ </Suspense>
214
+ </SuspenseList>
215
+ );
216
+ }
217
+
218
+ await A.resolve();
219
+
220
+ ReactNoop.render(<Foo />);
221
+
222
+ expect(Scheduler).toFlushAndYield([
223
+ 'A',
224
+ 'Suspend! [B]',
225
+ 'Loading B',
226
+ 'Suspend! [C]',
227
+ 'Loading C',
228
+ 'Loading A',
229
+ 'Loading B',
230
+ 'Loading C',
231
+ ]);
232
+
233
+ expect(ReactNoop).toMatchRenderedOutput(
234
+ <Fragment>
235
+ <span>Loading A</span>
236
+ <span>Loading B</span>
237
+ <span>Loading C</span>
238
+ </Fragment>,
239
+ );
240
+
241
+ await B.resolve();
242
+
243
+ expect(Scheduler).toFlushAndYield(['A', 'B', 'Suspend! [C]']);
244
+
245
+ expect(ReactNoop).toMatchRenderedOutput(
246
+ <Fragment>
247
+ <span>Loading A</span>
248
+ <span>Loading B</span>
249
+ <span>Loading C</span>
250
+ </Fragment>,
251
+ );
252
+
253
+ await C.resolve();
254
+
255
+ expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
256
+
257
+ expect(ReactNoop).toMatchRenderedOutput(
258
+ <Fragment>
259
+ <span>A</span>
260
+ <span>B</span>
261
+ <span>C</span>
262
+ </Fragment>,
263
+ );
264
+ });
265
+
266
+ it('displays all "together" even when nested as siblings', async () => {
267
+ let A = createAsyncText('A');
268
+ let B = createAsyncText('B');
269
+ let C = createAsyncText('C');
270
+
271
+ function Foo() {
272
+ return (
273
+ <SuspenseList revealOrder="together">
274
+ <div>
275
+ <Suspense fallback={<Text text="Loading A" />}>
276
+ <A />
277
+ </Suspense>
278
+ <Suspense fallback={<Text text="Loading B" />}>
279
+ <B />
280
+ </Suspense>
281
+ </div>
282
+ <div>
283
+ <Suspense fallback={<Text text="Loading C" />}>
284
+ <C />
285
+ </Suspense>
286
+ </div>
287
+ </SuspenseList>
288
+ );
289
+ }
290
+
291
+ await A.resolve();
292
+
293
+ ReactNoop.render(<Foo />);
294
+
295
+ expect(Scheduler).toFlushAndYield([
296
+ 'A',
297
+ 'Suspend! [B]',
298
+ 'Loading B',
299
+ 'Suspend! [C]',
300
+ 'Loading C',
301
+ 'Loading A',
302
+ 'Loading B',
303
+ 'Loading C',
304
+ ]);
305
+
306
+ expect(ReactNoop).toMatchRenderedOutput(
307
+ <Fragment>
308
+ <div>
309
+ <span>Loading A</span>
310
+ <span>Loading B</span>
311
+ </div>
312
+ <div>
313
+ <span>Loading C</span>
314
+ </div>
315
+ </Fragment>,
316
+ );
317
+
318
+ await B.resolve();
319
+
320
+ expect(Scheduler).toFlushAndYield(['A', 'B', 'Suspend! [C]']);
321
+
322
+ expect(ReactNoop).toMatchRenderedOutput(
323
+ <Fragment>
324
+ <div>
325
+ <span>Loading A</span>
326
+ <span>Loading B</span>
327
+ </div>
328
+ <div>
329
+ <span>Loading C</span>
330
+ </div>
331
+ </Fragment>,
332
+ );
333
+
334
+ await C.resolve();
335
+
336
+ expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
337
+
338
+ expect(ReactNoop).toMatchRenderedOutput(
339
+ <Fragment>
340
+ <div>
341
+ <span>A</span>
342
+ <span>B</span>
343
+ </div>
344
+ <div>
345
+ <span>C</span>
346
+ </div>
347
+ </Fragment>,
348
+ );
349
+ });
350
+
351
+ it('displays all "together" in nested SuspenseLists', async () => {
352
+ let A = createAsyncText('A');
353
+ let B = createAsyncText('B');
354
+ let C = createAsyncText('C');
355
+
356
+ function Foo() {
357
+ return (
358
+ <SuspenseList revealOrder="together">
359
+ <Suspense fallback={<Text text="Loading A" />}>
360
+ <A />
361
+ </Suspense>
362
+ <SuspenseList revealOrder="together">
363
+ <Suspense fallback={<Text text="Loading B" />}>
364
+ <B />
365
+ </Suspense>
366
+ <Suspense fallback={<Text text="Loading C" />}>
367
+ <C />
368
+ </Suspense>
369
+ </SuspenseList>
370
+ </SuspenseList>
371
+ );
372
+ }
373
+
374
+ await A.resolve();
375
+ await B.resolve();
376
+
377
+ ReactNoop.render(<Foo />);
378
+
379
+ expect(Scheduler).toFlushAndYield([
380
+ 'A',
381
+ 'B',
382
+ 'Suspend! [C]',
383
+ 'Loading C',
384
+ 'Loading B',
385
+ 'Loading C',
386
+ 'Loading A',
387
+ 'Loading B',
388
+ 'Loading C',
389
+ ]);
390
+
391
+ expect(ReactNoop).toMatchRenderedOutput(
392
+ <Fragment>
393
+ <span>Loading A</span>
394
+ <span>Loading B</span>
395
+ <span>Loading C</span>
396
+ </Fragment>,
397
+ );
398
+
399
+ await C.resolve();
400
+
401
+ expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
402
+
403
+ expect(ReactNoop).toMatchRenderedOutput(
404
+ <Fragment>
405
+ <span>A</span>
406
+ <span>B</span>
407
+ <span>C</span>
408
+ </Fragment>,
409
+ );
410
+ });
411
+
412
+ it('displays all "together" in nested SuspenseLists where the inner is default', async () => {
413
+ let A = createAsyncText('A');
414
+ let B = createAsyncText('B');
415
+ let C = createAsyncText('C');
416
+
417
+ function Foo() {
418
+ return (
419
+ <SuspenseList revealOrder="together">
420
+ <Suspense fallback={<Text text="Loading A" />}>
421
+ <A />
422
+ </Suspense>
423
+ <SuspenseList>
424
+ <Suspense fallback={<Text text="Loading B" />}>
425
+ <B />
426
+ </Suspense>
427
+ <Suspense fallback={<Text text="Loading C" />}>
428
+ <C />
429
+ </Suspense>
430
+ </SuspenseList>
431
+ </SuspenseList>
432
+ );
433
+ }
434
+
435
+ await A.resolve();
436
+ await B.resolve();
437
+
438
+ ReactNoop.render(<Foo />);
439
+
440
+ expect(Scheduler).toFlushAndYield([
441
+ 'A',
442
+ 'B',
443
+ 'Suspend! [C]',
444
+ 'Loading C',
445
+ 'Loading A',
446
+ 'Loading B',
447
+ 'Loading C',
448
+ ]);
449
+
450
+ expect(ReactNoop).toMatchRenderedOutput(
451
+ <Fragment>
452
+ <span>Loading A</span>
453
+ <span>Loading B</span>
454
+ <span>Loading C</span>
455
+ </Fragment>,
456
+ );
457
+
458
+ await C.resolve();
459
+
460
+ expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
461
+
462
+ expect(ReactNoop).toMatchRenderedOutput(
463
+ <Fragment>
464
+ <span>A</span>
465
+ <span>B</span>
466
+ <span>C</span>
467
+ </Fragment>,
468
+ );
469
+ });
470
+
471
+ it('avoided boundaries can be coordinate with SuspenseList', async () => {
472
+ let A = createAsyncText('A');
473
+ let B = createAsyncText('B');
474
+ let C = createAsyncText('C');
475
+
476
+ function Foo({showMore}) {
477
+ return (
478
+ <Suspense fallback={<Text text="Loading" />}>
479
+ <SuspenseList revealOrder="together">
480
+ <Suspense
481
+ unstable_avoidThisFallback={true}
482
+ fallback={<Text text="Loading A" />}>
483
+ <A />
484
+ </Suspense>
485
+ {showMore ? (
486
+ <Fragment>
487
+ <Suspense
488
+ unstable_avoidThisFallback={true}
489
+ fallback={<Text text="Loading B" />}>
490
+ <B />
491
+ </Suspense>
492
+ <Suspense
493
+ unstable_avoidThisFallback={true}
494
+ fallback={<Text text="Loading C" />}>
495
+ <C />
496
+ </Suspense>
497
+ </Fragment>
498
+ ) : null}
499
+ </SuspenseList>
500
+ </Suspense>
501
+ );
502
+ }
503
+
504
+ ReactNoop.render(<Foo />);
505
+
506
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading']);
507
+
508
+ expect(ReactNoop).toMatchRenderedOutput(<span>Loading</span>);
509
+
510
+ await A.resolve();
511
+
512
+ expect(Scheduler).toFlushAndYield(['A']);
513
+
514
+ expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
515
+
516
+ // Let's do an update that should consult the avoided boundaries.
517
+ ReactNoop.render(<Foo showMore={true} />);
518
+
519
+ expect(Scheduler).toFlushAndYield([
520
+ 'A',
521
+ 'Suspend! [B]',
522
+ 'Loading B',
523
+ 'Suspend! [C]',
524
+ 'Loading C',
525
+ ]);
526
+
527
+ // This will suspend, since the boundaries are avoided. Give them
528
+ // time to display their loading states.
529
+ jest.advanceTimersByTime(500);
530
+
531
+ // A is already showing content so it doesn't turn into a fallback.
532
+ expect(ReactNoop).toMatchRenderedOutput(
533
+ <Fragment>
534
+ <span>A</span>
535
+ <span>Loading B</span>
536
+ <span>Loading C</span>
537
+ </Fragment>,
538
+ );
539
+
540
+ await B.resolve();
541
+
542
+ expect(Scheduler).toFlushAndYield(['B']);
543
+
544
+ // Even though we could now show B, we're still waiting on C.
545
+ expect(ReactNoop).toMatchRenderedOutput(
546
+ <Fragment>
547
+ <span>A</span>
548
+ <span>Loading B</span>
549
+ <span>Loading C</span>
550
+ </Fragment>,
551
+ );
552
+
553
+ await C.resolve();
554
+
555
+ expect(Scheduler).toFlushAndYield(['B', 'C']);
556
+
557
+ expect(ReactNoop).toMatchRenderedOutput(
558
+ <Fragment>
559
+ <span>A</span>
560
+ <span>B</span>
561
+ <span>C</span>
562
+ </Fragment>,
563
+ );
564
+ });
565
+});