108
]);
109
});
110
111
- it('suspends rendering and continues later', async () => {
111
+ it('does not restart rendering for initial render', async () => {
112
function Bar(props) {
113
Scheduler.yieldValue('Bar');
114
return props.children;
115
}
116
117
function Foo() {
118
+ Scheduler.yieldValue('Foo');
119
+ return (
120
+ <React.Fragment>
121
+ <Suspense fallback={<Text text="Loading..." />}>
122
+ <Bar>
123
+ <AsyncText text="A" ms={100} />
124
+ <Text text="B" />
125
+ </Bar>
126
+ </Suspense>
127
+ <Text text="C" />
128
+ <Text text="D" />
129
+ </React.Fragment>
130
+ );
131
+ }
132
+
133
+ ReactNoop.render(<Foo />);
134
+ expect(Scheduler).toFlushAndYieldThrough([
135
+ 'Foo',
136
+ 'Bar',
137
+ // A suspends
138
+ 'Suspend! [A]',
139
+ // But we keep rendering the siblings
140
+ 'B',
141
+ 'Loading...',
142
+ 'C',
143
+ // We leave D incomplete.
144
+ ]);
145
+ expect(ReactNoop.getChildren()).toEqual([]);
146
+
147
+ // Flush the promise completely
148
+ Scheduler.advanceTime(100);
149
+ await advanceTimers(100);
150
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
151
+
152
+ // Even though the promise has resolved, we should now flush
153
+ // and commit the in progress render instead of restarting.
154
+ expect(Scheduler).toFlushAndYield(['D']);
155
+ expect(ReactNoop.getChildren()).toEqual([
156
+ span('Loading...'),
157
+ span('C'),
158
+ span('D'),
159
+ ]);
160
+
161
+ // Await one micro task to attach the retry listeners.
162
+ await null;
163
+
164
+ // Next, we'll flush the complete content.
165
+ expect(Scheduler).toFlushAndYield(['Bar', 'A', 'B']);
166
+
167
+ expect(ReactNoop.getChildren()).toEqual([
168
+ span('A'),
169
+ span('B'),
170
+ span('C'),
171
+ span('D'),
172
+ ]);
173
+ });
174
+
175
+ it('suspends rendering and continues later', async () => {
176
+ function Bar(props) {
177
+ Scheduler.yieldValue('Bar');
178
+ return props.children;
179
+ }
180
+
181
+ function Foo({renderBar}) {
182
Scheduler.yieldValue('Foo');
183
return (
184
<Suspense fallback={<Text text="Loading..." />}>
121
- <Bar>
122
- <AsyncText text="A" ms={100} />
123
- <Text text="B" />
124
- </Bar>
185
+ {renderBar ? (
186
+ <Bar>
187
+ <AsyncText text="A" ms={100} />
188
+ <Text text="B" />
189
+ </Bar>
190
+ ) : null}
191
</Suspense>
192
);
193
}
194
195
+ // Render empty shell.
196
ReactNoop.render(<Foo />);
197
+ expect(Scheduler).toFlushAndYield(['Foo']);
198
+
199
+ // The update will suspend.
200
+ ReactNoop.render(<Foo renderBar={true} />);
201
expect(Scheduler).toFlushAndYield([
202
'Foo',
203
'Bar',
241
'Suspend! [B]',
242
'Loading B...',
243
]);
173
- expect(ReactNoop.getChildren()).toEqual([]);
174
-
175
- // Advance time by enough to timeout both components and commit their placeholders
176
- ReactNoop.expire(4000);
177
- await advanceTimers(4000);
178
-
179
- expect(Scheduler).toFlushWithoutYielding();
244
expect(ReactNoop.getChildren()).toEqual([
245
span('Loading A...'),
246
span('Loading B...'),
249
// Advance time by enough that the first Suspense's promise resolves and
250
// switches back to the normal view. The second Suspense should still
251
// show the placeholder
188
- ReactNoop.expire(1000);
189
- await advanceTimers(1000);
252
+ ReactNoop.expire(5000);
253
+ await advanceTimers(5000);
254
255
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
256
expect(Scheduler).toFlushAndYield(['A']);
267
});
268
269
it('continues rendering siblings after suspending', async () => {
270
+ // A shell is needed. The update cause it to suspend.
271
+ ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
272
+ expect(Scheduler).toFlushAndYield([]);
273
+ // B suspends. Continue rendering the remaining siblings.
274
ReactNoop.render(
275
<Suspense fallback={<Text text="Loading..." />}>
276
<Text text="A" />
321
}
322
323
const errorBoundary = React.createRef();
256
- function App() {
324
+ function App({renderContent}) {
325
return (
326
<Suspense fallback={<Text text="Loading..." />}>
259
- <ErrorBoundary ref={errorBoundary}>
260
- <AsyncText text="Result" ms={1000} />
261
- </ErrorBoundary>
327
+ {renderContent ? (
328
+ <ErrorBoundary ref={errorBoundary}>
329
+ <AsyncText text="Result" ms={1000} />
330
+ </ErrorBoundary>
331
+ ) : null}
332
</Suspense>
333
);
334
}
335
336
ReactNoop.render(<App />);
337
+ expect(Scheduler).toFlushAndYield([]);
338
+ expect(ReactNoop.getChildren()).toEqual([]);
339
+
340
+ ReactNoop.render(<App renderContent={true} />);
341
expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
342
expect(ReactNoop.getChildren()).toEqual([]);
343
392
393
ReactNoop.render(<App />);
394
expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
321
- expect(ReactNoop.getChildren()).toEqual([]);
322
-
323
- ReactNoop.expire(2000);
324
- await advanceTimers(2000);
325
- expect(Scheduler).toFlushWithoutYielding();
395
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
396
397
textResourceShouldFail = true;
329
- ReactNoop.expire(1000);
330
- await advanceTimers(1000);
398
+ ReactNoop.expire(3000);
399
+ await advanceTimers(3000);
400
textResourceShouldFail = false;
401
402
expect(Scheduler).toHaveYielded(['Promise rejected [Result]']);
463
function App(props) {
464
return (
465
<Suspense fallback={<Text text="Loading..." />}>
397
- <AsyncText text="A" />
466
+ {props.showA && <AsyncText text="A" />}
467
{props.showB && <Text text="B" />}
468
</Suspense>
469
);
470
}
471
403
- ReactNoop.render(<App showB={false} />);
472
+ ReactNoop.render(<App showA={false} showB={false} />);
473
+ expect(Scheduler).toFlushAndYield([]);
474
+ expect(ReactNoop.getChildren()).toEqual([]);
475
+
476
+ ReactNoop.render(<App showA={true} showB={false} />);
477
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
478
expect(ReactNoop.getChildren()).toEqual([]);
479
480
// Advance React's virtual time by enough to fall into a new async bucket,
481
// but not enough to expire the suspense timeout.
482
ReactNoop.expire(120);
410
- ReactNoop.render(<App showB={true} />);
483
+ ReactNoop.render(<App showA={true} showB={true} />);
484
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'B', 'Loading...']);
485
expect(ReactNoop.getChildren()).toEqual([]);
486
521
});
522
523
it('forces an expiration after an update times out', async () => {
524
+ ReactNoop.render(
525
+ <Fragment>
526
+ <Suspense fallback={<Text text="Loading..." />} />
527
+ </Fragment>,
528
+ );
529
+ expect(Scheduler).toFlushAndYield([]);
530
+
531
ReactNoop.render(
532
<Fragment>
533
<Suspense fallback={<Text text="Loading..." />}>
565
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
566
});
567
488
- it('switches to an inner fallback even if it expires later', async () => {
568
+ it('switches to an inner fallback after suspending for a while', async () => {
569
// Advance the virtual time so that we're closer to the edge of a bucket.
570
ReactNoop.expire(200);
571
589
'Loading inner...',
590
'Loading outer...',
591
]);
512
- // The update hasn't expired yet, so we commit nothing.
513
- expect(ReactNoop.getChildren()).toEqual([]);
514
-
515
- // Expire the outer timeout, but don't expire the inner one.
516
- // We should see the outer loading placeholder.
517
- ReactNoop.expire(250);
518
- await advanceTimers(250);
519
- expect(Scheduler).toFlushWithoutYielding();
592
+ // The outer loading state finishes immediately.
593
expect(ReactNoop.getChildren()).toEqual([
594
span('Sync'),
595
span('Loading outer...'),
596
]);
597
598
// Resolve the outer promise.
526
- ReactNoop.expire(50);
527
- await advanceTimers(50);
528
- // At this point, 250ms have elapsed total. The outer placeholder
529
- // timed out at around 150-200ms. So, 50-100ms have elapsed since the
530
- // placeholder timed out. That means we still haven't reached the 150ms
531
- // threshold of the inner placeholder.
599
+ ReactNoop.expire(300);
600
+ await advanceTimers(300);
601
expect(Scheduler).toHaveYielded(['Promise resolved [Outer content]']);
602
expect(Scheduler).toFlushAndYield([
603
'Outer content',
687
});
688
689
it('expires early by default', async () => {
690
+ ReactNoop.render(
691
+ <Fragment>
692
+ <Suspense fallback={<Text text="Loading..." />} />
693
+ </Fragment>,
694
+ );
695
+ expect(Scheduler).toFlushAndYield([]);
696
+
697
ReactNoop.render(
698
<Fragment>
699
<Suspense fallback={<Text text="Loading..." />}>
730
731
it('resolves successfully even if fallback render is pending', async () => {
732
ReactNoop.render(
657
- <Suspense fallback={<Text text="Loading..." />}>
658
- <AsyncText text="Async" ms={3000} />
659
- </Suspense>,
733
+ <React.Fragment>
734
+ <Suspense fallback={<Text text="Loading..." />} />
735
+ </React.Fragment>,
736
+ );
737
+ expect(Scheduler).toFlushAndYield([]);
738
+ expect(ReactNoop.getChildren()).toEqual([]);
739
+ ReactNoop.render(
740
+ <React.Fragment>
741
+ <Suspense fallback={<Text text="Loading..." />}>
742
+ <AsyncText text="Async" ms={3000} />
743
+ </Suspense>
744
+ </React.Fragment>,
745
);
746
expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
747
await advanceTimers(1500);
748
expect(Scheduler).toHaveYielded([]);
749
+ expect(ReactNoop.getChildren()).toEqual([]);
750
// Before we have a chance to flush, the promise resolves.
751
await advanceTimers(2000);
752
expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
667
- expect(Scheduler).toFlushAndYield(['Async']);
753
+ expect(Scheduler).toFlushAndYield([
754
+ // We've now pinged the boundary but we don't know if we should restart yet,
755
+ // because we haven't completed the suspense boundary.
756
+ 'Loading...',
757
+ // Once we've completed the boundary we restarted.
758
+ 'Async',
759
+ ]);
760
expect(ReactNoop.getChildren()).toEqual([span('Async')]);
761
});
762
794
});
795
796
it('can resume rendering earlier than a timeout', async () => {
797
+ ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
798
+ expect(Scheduler).toFlushAndYield([]);
799
+
800
ReactNoop.render(
801
<Suspense fallback={<Text text="Loading..." />}>
802
<AsyncText text="Async" ms={100} />
1409
});
1410
1411
it('suspends for longer if something took a long (CPU bound) time to render', async () => {
1317
- function Foo() {
1412
+ function Foo({renderContent}) {
1413
Scheduler.yieldValue('Foo');
1414
return (
1415
<Suspense fallback={<Text text="Loading..." />}>
1321
- <AsyncText text="A" ms={5000} />
1416
+ {renderContent ? <AsyncText text="A" ms={5000} /> : null}
1417
</Suspense>
1418
);
1419
}
1420
1421
ReactNoop.render(<Foo />);
1422
+ expect(Scheduler).toFlushAndYield(['Foo']);
1423
+
1424
+ ReactNoop.render(<Foo renderContent={true} />);
1425
Scheduler.advanceTime(100);
1426
await advanceTimers(100);
1427
// Start rendering
1461
expect(ReactNoop.getChildren()).toEqual([span('A')]);
1462
});
1463
1366
- it('suspends for longer if a fallback has been shown for a long time', async () => {
1464
+ it('does not suspends if a fallback has been shown for a long time', async () => {
1465
function Foo() {
1466
Scheduler.yieldValue('Foo');
1467
return (
1485
'Loading more...',
1486
'Loading...',
1487
]);
1390
- // We're now suspended and we haven't shown anything yet.
1391
- expect(ReactNoop.getChildren()).toEqual([]);
1392
-
1393
- // Show the fallback.
1394
- Scheduler.advanceTime(400);
1395
- await advanceTimers(400);
1396
- expect(Scheduler).toFlushWithoutYielding();
1488
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1489
1490
// Wait a long time.
1499
'Suspend! [B]',
1500
'Loading more...',
1501
]);
1411
- // Because we've already been waiting for so long we can
1412
- // wait a bit longer. Still nothing...
1413
- Scheduler.advanceTime(600);
1414
- await advanceTimers(600);
1415
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1416
-
1417
- // Eventually we'll show more content with inner fallback.
1418
- Scheduler.advanceTime(3000);
1419
- await advanceTimers(3000);
1420
- // No need to rerender.
1421
- expect(Scheduler).toFlushWithoutYielding();
1502
+ // Because we've already been waiting for so long we've exceeded
1503
+ // our threshold and we show the next level immediately.
1504
expect(ReactNoop.getChildren()).toEqual([
1505
span('A'),
1506
span('Loading more...'),
1507
]);
1508
1509
// Flush the last promise completely
1428
- Scheduler.advanceTime(4500);
1429
- await advanceTimers(4500);
1510
+ Scheduler.advanceTime(5000);
1511
+ await advanceTimers(5000);
1512
// Renders successfully
1513
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1514
expect(Scheduler).toFlushAndYield(['B']);
1515
expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
1516
});
1517
1436
- it('does not suspend for very long after a higher priority update', async () => {
1518
+ it('does suspend if a fallback has been shown for a short time', async () => {
1519
function Foo() {
1520
Scheduler.yieldValue('Foo');
1521
return (
1522
<Suspense fallback={<Text text="Loading..." />}>
1441
- <AsyncText text="A" ms={5000} />
1523
+ <AsyncText text="A" ms={200} />
1524
+ <Suspense fallback={<Text text="Loading more..." />}>
1525
+ <AsyncText text="B" ms={450} />
1526
+ </Suspense>
1527
</Suspense>
1528
);
1529
}
1530
1446
- ReactNoop.discreteUpdates(() => ReactNoop.render(<Foo />));
1531
+ ReactNoop.render(<Foo />);
1532
+ // Start rendering
1533
+ expect(Scheduler).toFlushAndYield([
1534
+ 'Foo',
1535
+ // A suspends
1536
+ 'Suspend! [A]',
1537
+ // B suspends
1538
+ 'Suspend! [B]',
1539
+ 'Loading more...',
1540
+ 'Loading...',
1541
+ ]);
1542
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1543
+
1544
+ // Wait a short time.
1545
+ Scheduler.advanceTime(250);
1546
+ await advanceTimers(250);
1547
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1548
+
1549
+ // Retry with the new content.
1550
+ expect(Scheduler).toFlushAndYield([
1551
+ 'A',
1552
+ // B still suspends
1553
+ 'Suspend! [B]',
1554
+ 'Loading more...',
1555
+ ]);
1556
+ // Because we've already been waiting for so long we can
1557
+ // wait a bit longer. Still nothing...
1558
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1559
+
1560
+ Scheduler.advanceTime(200);
1561
+ await advanceTimers(200);
1562
+
1563
+ // Before we commit another Promise resolves.
1564
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1565
+ // We're still showing the first loading state.
1566
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1567
+ // Restart and render the complete content.
1568
+ expect(Scheduler).toFlushAndYield(['A', 'B']);
1569
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
1570
+ });
1571
+
1572
+ it('does not suspend for very long after a higher priority update', async () => {
1573
+ function Foo({renderContent}) {
1574
+ Scheduler.yieldValue('Foo');
1575
+ return (
1576
+ <Suspense fallback={<Text text="Loading..." />}>
1577
+ {renderContent ? <AsyncText text="A" ms={5000} /> : null}
1578
+ </Suspense>
1579
+ );
1580
+ }
1581
+
1582
+ ReactNoop.render(<Foo />);
1583
+ expect(Scheduler).toFlushAndYield(['Foo']);
1584
+
1585
+ ReactNoop.discreteUpdates(() =>
1586
+ ReactNoop.render(<Foo renderContent={true} />),
1587
+ );
1588
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
1589
1590
// Advance some time.
1619
1620
it('warns when suspending inside discrete update', async () => {
1621
function A() {
1622
+ Scheduler.yieldValue('A');
1623
TextResource.read(['A', 1000]);
1624
return 'A';
1625
}
1647
}
1648
1649
ReactNoop.discreteUpdates(() => ReactNoop.render(<App />));
1508
- Scheduler.flushAll();
1650
+ expect(Scheduler).toFlushAndYieldThrough(['A']);
1651
1652
// Warning is not flushed until the commit phase
1653
1654
// Timeout and commit the fallback
1655
expect(() => {
1514
- jest.advanceTimersByTime(1000);
1656
+ Scheduler.flushAll();
1657
}).toWarnDev(
1658
'The following components suspended during a user-blocking update: A, C',
1659
{withoutStack: true},
1683
'B',
1684
'Initial load...',
1685
]);
1544
- // We're still suspended.
1545
- expect(ReactNoop.getChildren()).toEqual([]);
1546
- // Flush to skip suspended time.
1547
- Scheduler.advanceTime(600);
1548
- await advanceTimers(600);
1686
expect(ReactNoop.getChildren()).toEqual([span('Initial load...')]);
1687
1688
// Eventually we resolve and show the data.
1795
});
1796
1797
it('commits a suspended idle pri render within a reasonable time', async () => {
1661
- function Foo({something}) {
1798
+ function Foo({renderContent}) {
1799
return (
1800
<Fragment>
1801
<Suspense fallback={<Text text="Loading A..." />}>
1665
- <AsyncText text="A" ms={10000} />
1802
+ {renderContent ? <AsyncText text="A" ms={10000} /> : null}
1803
</Suspense>
1804
</Fragment>
1805
);
1806
}
1807
1808
ReactNoop.render(<Foo />);
1809
+ expect(Scheduler).toFlushAndYield([]);
1810
+
1811
+ ReactNoop.render(<Foo renderContent={1} />);
1812
1813
// Took a long time to render. This is to ensure we get a long suspense time.
1814
// Could also use something like withSuspenseConfig to simulate this.
1821
1822
// Schedule an update at idle pri.
1823
Scheduler.unstable_runWithPriority(Scheduler.unstable_IdlePriority, () =>
1684
- ReactNoop.render(<Foo something={true} />),
1824
+ ReactNoop.render(<Foo renderContent={2} />),
1825
);
1826
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading A...']);
1827