1282
1283
function App({showMore}) {
1284
return (
1285
- <SuspenseList revealOrder="forwards">
1286
- {a}
1287
- {b}
1288
- {showMore ? (
1289
- <Suspense fallback="Loading C">
1290
- <span>C</span>
1291
- </Suspense>
1292
- ) : null}
1293
- </SuspenseList>
1285
+ <div>
1286
+ <SuspenseList revealOrder="forwards">
1287
+ {a}
1288
+ {b}
1289
+ {showMore ? (
1290
+ <Suspense fallback="Loading C">
1291
+ <span>C</span>
1292
+ </Suspense>
1293
+ ) : null}
1294
+ </SuspenseList>
1295
+ </div>
1296
);
1297
}
1298
1310
1311
// We're not hydrated yet.
1312
expect(ref.current).toBe(null);
1311
- expect(getVisibleChildren(container)).toEqual([
1312
- 'Loading A',
1313
- // TODO: This is incorrect. It should be "Loading B" but Fizz SuspenseList
1314
- // isn't implemented fully yet.
1315
- <span>B</span>,
1316
- ]);
1313
+ expect(getVisibleChildren(container)).toEqual(
1314
+ <div>
1315
+ Loading A
1316
+ {/* // TODO: This is incorrect. It should be "Loading B" but Fizz SuspenseList
1317
+ // isn't implemented fully yet. */}
1318
+ <span>B</span>
1319
+ </div>,
1320
+ );
1321
1322
// Add more rows before we've hydrated the first two.
1323
root.render(<App showMore={true} />);
1327
expect(ref.current).toBe(null);
1328
1329
// We haven't resolved yet.
1326
- expect(getVisibleChildren(container)).toEqual([
1327
- 'Loading A',
1328
- // TODO: This is incorrect. It should be "Loading B" but Fizz SuspenseList
1329
- // isn't implemented fully yet.
1330
- <span>B</span>,
1331
- 'Loading C',
1332
- ]);
1330
+ expect(getVisibleChildren(container)).toEqual(
1331
+ <div>
1332
+ Loading A
1333
+ {/* // TODO: This is incorrect. It should be "Loading B" but Fizz SuspenseList
1334
+ // isn't implemented fully yet. */}
1335
+ <span>B</span>
1336
+ Loading C
1337
+ </div>,
1338
+ );
1339
1340
await act(async () => {
1341
await resolveText('A');
1343
1344
await waitForAll([]);
1345
1340
- expect(getVisibleChildren(container)).toEqual([
1341
- <span>A</span>,
1342
- <span>B</span>,
1343
- <span>C</span>,
1344
- ]);
1346
+ expect(getVisibleChildren(container)).toEqual(
1347
+ <div>
1348
+ <span>A</span>
1349
+ <span>B</span>
1350
+ <span>C</span>
1351
+ </div>,
1352
+ );
1353
1354
const span = container.getElementsByTagName('span')[0];
1355
expect(ref.current).toBe(span);
1478
await act(() => {
1479
const {pipe} = renderToPipeableStream(
1480
// We use two nested boundaries to flush out coverage of an old reentrancy bug.
1473
- <Suspense fallback="Loading...">
1474
- <Suspense fallback={<Text text="Loading A..." />}>
1475
- <>
1476
- <Text text="This will show A: " />
1477
- <div>
1478
- <AsyncText text="A" />
1479
- </div>
1480
- </>
1481
+ <div>
1482
+ <Suspense fallback="Loading...">
1483
+ <Suspense fallback={<Text text="Loading A..." />}>
1484
+ <>
1485
+ <Text text="This will show A: " />
1486
+ <div>
1487
+ <AsyncText text="A" />
1488
+ </div>
1489
+ </>
1490
+ </Suspense>
1491
</Suspense>
1482
- </Suspense>,
1492
+ </div>,
1493
{
1494
identifierPrefix: 'A_',
1495
onShellReady() {
1503
1504
await act(() => {
1505
const {pipe} = renderToPipeableStream(
1496
- <Suspense fallback={<Text text="Loading B..." />}>
1497
- <Text text="This will show B: " />
1498
- <div>
1499
- <AsyncText text="B" />
1500
- </div>
1501
- </Suspense>,
1506
+ <div>
1507
+ <Suspense fallback={<Text text="Loading B..." />}>
1508
+ <Text text="This will show B: " />
1509
+ <div>
1510
+ <AsyncText text="B" />
1511
+ </div>
1512
+ </Suspense>
1513
+ </div>,
1514
{
1515
identifierPrefix: 'B_',
1516
onShellReady() {
1523
});
1524
1525
expect(getVisibleChildren(container)).toEqual([
1514
- <div id="container-A">Loading A...</div>,
1515
- <div id="container-B">Loading B...</div>,
1526
+ <div id="container-A">
1527
+ <div>Loading A...</div>
1528
+ </div>,
1529
+ <div id="container-B">
1530
+ <div>Loading B...</div>
1531
+ </div>,
1532
]);
1533
1534
await act(() => {
1536
});
1537
1538
expect(getVisibleChildren(container)).toEqual([
1523
- <div id="container-A">Loading A...</div>,
1539
+ <div id="container-A">
1540
+ <div>Loading A...</div>
1541
+ </div>,
1542
<div id="container-B">
1525
- This will show B: <div>B</div>
1543
+ <div>
1544
+ This will show B: <div>B</div>
1545
+ </div>
1546
</div>,
1547
]);
1548
1555
1556
expect(getVisibleChildren(container)).toEqual([
1557
<div id="container-A">
1538
- This will show A: <div>A</div>
1558
+ <div>
1559
+ This will show A: <div>A</div>
1560
+ </div>
1561
</div>,
1562
<div id="container-B">
1541
- This will show B: <div>B</div>
1563
+ <div>
1564
+ This will show B: <div>B</div>
1565
+ </div>
1566
</div>,
1567
]);
1568
});
2111
it('client renders a boundary if it errors before finishing the fallback', async () => {
2112
function App({isClient}) {
2113
return (
2090
- <Suspense fallback="Loading root...">
2091
- <div>
2092
- <Suspense fallback={<AsyncText text="Loading..." />}>
2093
- <h1>
2094
- {isClient ? <Text text="Hello" /> : <AsyncText text="Hello" />}
2095
- </h1>
2096
- </Suspense>
2097
- </div>
2098
- </Suspense>
2114
+ <div>
2115
+ <Suspense fallback="Loading root...">
2116
+ <div>
2117
+ <Suspense fallback={<AsyncText text="Loading..." />}>
2118
+ <h1>
2119
+ {isClient ? (
2120
+ <Text text="Hello" />
2121
+ ) : (
2122
+ <AsyncText text="Hello" />
2123
+ )}
2124
+ </h1>
2125
+ </Suspense>
2126
+ </div>
2127
+ </Suspense>
2128
+ </div>
2129
);
2130
}
2131
2162
await waitForAll([]);
2163
2164
// We're still loading because we're waiting for the server to stream more content.
2135
- expect(getVisibleChildren(container)).toEqual('Loading root...');
2165
+ expect(getVisibleChildren(container)).toEqual(<div>Loading root...</div>);
2166
2167
expect(loggedErrors).toEqual([]);
2168
2175
2176
// We still can't render it on the client because we haven't unblocked the parent.
2177
await waitForAll([]);
2148
- expect(getVisibleChildren(container)).toEqual('Loading root...');
2178
+ expect(getVisibleChildren(container)).toEqual(<div>Loading root...</div>);
2179
2180
// Unblock the loading state
2181
await act(() => {
2183
});
2184
2185
// Now we're able to show the inner boundary.
2156
- expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
2186
+ expect(getVisibleChildren(container)).toEqual(
2187
+ <div>
2188
+ <div>Loading...</div>
2189
+ </div>,
2190
+ );
2191
2192
// That will let us client render it instead.
2193
await waitForAll([]);
2204
'Suspense',
2205
'div',
2206
'Suspense',
2207
+ 'div',
2208
'App',
2209
]),
2210
],
2220
// The client rendered HTML is now in place.
2221
expect(getVisibleChildren(container)).toEqual(
2222
<div>
2188
- <h1>Hello</h1>
2223
+ <div>
2224
+ <h1>Hello</h1>
2225
+ </div>
2226
</div>,
2227
);
2228
2232
it('should be able to abort the fallback if the main content finishes first', async () => {
2233
await act(() => {
2234
const {pipe} = renderToPipeableStream(
2198
- <Suspense fallback={<Text text="Loading Outer" />}>
2199
- <div>
2200
- <Suspense
2201
- fallback={
2202
- <div>
2203
- <AsyncText text="Loading" />
2204
- Inner
2205
- </div>
2206
- }>
2207
- <AsyncText text="Hello" />
2208
- </Suspense>
2209
- </div>
2210
- </Suspense>,
2235
+ <div>
2236
+ <Suspense fallback={<Text text="Loading Outer" />}>
2237
+ <div>
2238
+ <Suspense
2239
+ fallback={
2240
+ <div>
2241
+ <AsyncText text="Loading" />
2242
+ Inner
2243
+ </div>
2244
+ }>
2245
+ <AsyncText text="Hello" />
2246
+ </Suspense>
2247
+ </div>
2248
+ </Suspense>
2249
+ </div>,
2250
);
2251
pipe(writable);
2252
});
2214
- expect(getVisibleChildren(container)).toEqual('Loading Outer');
2253
+ expect(getVisibleChildren(container)).toEqual(<div>Loading Outer</div>);
2254
// We should have received a partial segment containing the a partial of the fallback.
2255
expect(container.innerHTML).toContain('Inner');
2256
await act(() => {
2257
resolveText('Hello');
2258
});
2259
// We should've been able to display the content without waiting for the rest of the fallback.
2221
- expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
2260
+ expect(getVisibleChildren(container)).toEqual(
2261
+ <div>
2262
+ <div>Hello</div>
2263
+ </div>,
2264
+ );
2265
});
2266
2267
it('calls getServerSnapshot instead of getSnapshot', async () => {
5328
it('does not insert text separators even when adjacent text is in a delayed segment', async () => {
5329
function App({name}) {
5330
return (
5288
- <Suspense fallback={'loading...'}>
5289
- <div id="app-div">
5290
- hello
5291
- <b>
5292
- world, <AsyncText text={name} />
5293
- </b>
5294
- !
5295
- </div>
5296
- </Suspense>
5331
+ <div>
5332
+ <Suspense fallback={'loading...'}>
5333
+ <div id="app-div">
5334
+ hello
5335
+ <b>
5336
+ world, <AsyncText text={name} />
5337
+ </b>
5338
+ !
5339
+ </div>
5340
+ </Suspense>
5341
+ </div>
5342
);
5343
}
5344
5356
const div = stripExternalRuntimeInNodes(
5357
container.children,
5358
renderOptions.unstable_externalRuntimeSrc,
5314
- )[0];
5359
+ )[0].children[0];
5360
expect(div.outerHTML).toEqual(
5361
'<div id="app-div">hello<b>world, Foo</b>!</div>',
5362
);
5363
5319
- // there may be either:
5320
- // - an external runtime script and deleted nodes with data attributes
5321
- // - extra script nodes containing fizz instructions at the end of container
5322
- expect(
5323
- Array.from(container.childNodes).filter(e => e.tagName !== 'SCRIPT')
5324
- .length,
5325
- ).toBe(3);
5326
-
5364
expect(div.childNodes.length).toBe(3);
5365
const b = div.childNodes[1];
5366
expect(b.childNodes.length).toBe(2);
5376
await waitForAll([]);
5377
expect(errors).toEqual([]);
5378
expect(getVisibleChildren(container)).toEqual(
5342
- <div id="app-div">
5343
- hello<b>world, {'Foo'}</b>!
5379
+ <div>
5380
+ <div id="app-div">
5381
+ hello<b>world, {'Foo'}</b>!
5382
+ </div>
5383
</div>,
5384
);
5385
});
5387
it('works with multiple adjacent segments', async () => {
5388
function App() {
5389
return (
5351
- <Suspense fallback={'loading...'}>
5352
- <div id="app-div">
5353
- h<AsyncText text={'ello'} />
5354
- w<AsyncText text={'orld'} />
5355
- </div>
5356
- </Suspense>
5390
+ <div>
5391
+ <Suspense fallback={'loading...'}>
5392
+ <div id="app-div">
5393
+ h<AsyncText text={'ello'} />
5394
+ w<AsyncText text={'orld'} />
5395
+ </div>
5396
+ </Suspense>
5397
+ </div>
5398
);
5399
}
5400
5418
stripExternalRuntimeInNodes(
5419
container.children,
5420
renderOptions.unstable_externalRuntimeSrc,
5380
- )[0].outerHTML,
5421
+ )[0].children[0].outerHTML,
5422
).toEqual('<div id="app-div">helloworld</div>');
5423
5424
const errors = [];
5430
await waitForAll([]);
5431
expect(errors).toEqual([]);
5432
expect(getVisibleChildren(container)).toEqual(
5392
- <div id="app-div">{['h', 'ello', 'w', 'orld']}</div>,
5433
+ <div>
5434
+ <div id="app-div">{['h', 'ello', 'w', 'orld']}</div>
5435
+ </div>,
5436
);
5437
});
5438
5439
it('works when some segments are flushed and others are patched', async () => {
5440
function App() {
5441
return (
5399
- <Suspense fallback={'loading...'}>
5400
- <div id="app-div">
5401
- h<AsyncText text={'ello'} />
5402
- w<AsyncText text={'orld'} />
5403
- </div>
5404
- </Suspense>
5442
+ <div>
5443
+ <Suspense fallback={'loading...'}>
5444
+ <div id="app-div">
5445
+ h<AsyncText text={'ello'} />
5446
+ w<AsyncText text={'orld'} />
5447
+ </div>
5448
+ </Suspense>
5449
+ </div>
5450
);
5451
}
5452
5467
stripExternalRuntimeInNodes(
5468
container.children,
5469
renderOptions.unstable_externalRuntimeSrc,
5425
- )[0].outerHTML,
5470
+ )[0].children[0].outerHTML,
5471
).toEqual('<div id="app-div">h<!-- -->ello<!-- -->world</div>');
5472
5473
const errors = [];
5482
await waitForAll([]);
5483
expect(errors).toEqual([]);
5484
expect(getVisibleChildren(container)).toEqual(
5440
- <div id="app-div">{['h', 'ello', 'w', 'orld']}</div>,
5485
+ <div>
5486
+ <div id="app-div">{['h', 'ello', 'w', 'orld']}</div>
5487
+ </div>,
5488
);
5489
});
5490
6120
6121
function App() {
6122
return (
6076
- <Suspense fallback="Loading...">
6077
- <ErrorBoundary>
6078
- <Async />
6079
- </ErrorBoundary>
6080
- </Suspense>
6123
+ <div>
6124
+ <Suspense fallback="Loading...">
6125
+ <ErrorBoundary>
6126
+ <Async />
6127
+ </ErrorBoundary>
6128
+ </Suspense>
6129
+ </div>
6130
);
6131
}
6132
6156
await promiseC;
6157
});
6158
6110
- expect(getVisibleChildren(container)).toEqual('Loading...');
6159
+ expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
6160
expect(reportedServerErrors.length).toBe(1);
6161
expect(reportedServerErrors[0].message).toBe('Oops!');
6162
6171
},
6172
});
6173
await waitForAll([]);
6125
- expect(getVisibleChildren(container)).toEqual('Oops!');
6174
+ expect(getVisibleChildren(container)).toEqual(<div>Oops!</div>);
6175
// Because this is rethrown on the client, it is not a recoverable error.
6176
expect(reportedClientErrors.length).toBe(0);
6177
// It is caught by the error boundary.
8841
),
8842
]);
8843
});
8844
+
8845
+ it('can suspend inside the <head /> tag', async () => {
8846
+ function BlockedOn({value, children}) {
8847
+ readText(value);
8848
+ return children;
8849
+ }
8850
+
8851
+ function App() {
8852
+ return (
8853
+ <html>
8854
+ <head>
8855
+ <Suspense fallback={<meta itemProp="head loading" />}>
8856
+ <BlockedOn value="head">
8857
+ <meta itemProp="" content="head" />
8858
+ </BlockedOn>
8859
+ </Suspense>
8860
+ </head>
8861
+ <body>
8862
+ <div>hello world</div>
8863
+ </body>
8864
+ </html>
8865
+ );
8866
+ }
8867
+
8868
+ await act(() => {
8869
+ const {pipe} = renderToPipeableStream(<App />);
8870
+ pipe(writable);
8871
+ });
8872
+
8873
+ expect(getVisibleChildren(document)).toEqual(
8874
+ <html>
8875
+ <head>
8876
+ <meta itemprop="head loading" />
8877
+ </head>
8878
+ <body>
8879
+ <div>hello world</div>
8880
+ </body>
8881
+ </html>,
8882
+ );
8883
+
8884
+ await act(() => {
8885
+ resolveText('head');
8886
+ });
8887
+
8888
+ expect(getVisibleChildren(document)).toEqual(
8889
+ <html>
8890
+ <head>
8891
+ <meta itemprop="" content="head" />
8892
+ </head>
8893
+ <body>
8894
+ <div>hello world</div>
8895
+ </body>
8896
+ </html>,
8897
+ );
8898
+
8899
+ const root = ReactDOMClient.hydrateRoot(document, <App />);
8900
+ await waitForAll([]);
8901
+
8902
+ expect(getVisibleChildren(document)).toEqual(
8903
+ <html>
8904
+ <head>
8905
+ <meta itemprop="" content="head" />
8906
+ </head>
8907
+ <body>
8908
+ <div>hello world</div>
8909
+ </body>
8910
+ </html>,
8911
+ );
8912
+
8913
+ await act(() => {
8914
+ root.unmount();
8915
+ });
8916
+ await waitForAll([]);
8917
+
8918
+ expect(getVisibleChildren(document)).toEqual(
8919
+ <html>
8920
+ <head />
8921
+ <body />
8922
+ </html>,
8923
+ );
8924
+ });
8925
+
8926
+ it('can server render Suspense before, after, and around <html>', async () => {
8927
+ function BlockedOn({value, children}) {
8928
+ readText(value);
8929
+ return children;
8930
+ }
8931
+
8932
+ function App() {
8933
+ return (
8934
+ <>
8935
+ <Suspense fallback="this fallback never renders">
8936
+ <div>before</div>
8937
+ </Suspense>
8938
+ <Suspense fallback="this fallback never renders">
8939
+ <BlockedOn value="html">
8940
+ <html lang="en">
8941
+ <head>
8942
+ <meta itemProp="" content="non-floaty meta" />
8943
+ </head>
8944
+ <body>
8945
+ <div>hello world</div>
8946
+ </body>
8947
+ </html>
8948
+ </BlockedOn>
8949
+ </Suspense>
8950
+ <Suspense fallback="this fallback never renders">
8951
+ <div>after</div>
8952
+ </Suspense>
8953
+ </>
8954
+ );
8955
+ }
8956
+
8957
+ let content = '';
8958
+ writable.on('data', chunk => (content += chunk));
8959
+
8960
+ let shellReady = false;
8961
+ await act(() => {
8962
+ const {pipe} = renderToPipeableStream(<App />, {
8963
+ onShellReady: () => {
8964
+ shellReady = true;
8965
+ },
8966
+ });
8967
+ pipe(writable);
8968
+ });
8969
+
8970
+ // When we Suspend above the body we block the shell because the root HTML scope
8971
+ // is considered "reconciliation" mode whereby we should stay on the prior view
8972
+ // (the prior page for instance) rather than showing the fallback (semantically)
8973
+ expect(shellReady).toBe(true);
8974
+ expect(content).toBe('');
8975
+
8976
+ await act(() => {
8977
+ resolveText('html');
8978
+ });
8979
+ expect(content).toMatch(/^<!DOCTYPE html>/);
8980
+ expect(getVisibleChildren(document)).toEqual(
8981
+ <html lang="en">
8982
+ <head>
8983
+ <meta itemprop="" content="non-floaty meta" />
8984
+ </head>
8985
+ <body>
8986
+ <div>before</div>
8987
+ <div>hello world</div>
8988
+ <div>after</div>
8989
+ </body>
8990
+ </html>,
8991
+ );
8992
+ });
8993
+
8994
+ it('can server render Suspense before, after, and around <body>', async () => {
8995
+ function BlockedOn({value, children}) {
8996
+ readText(value);
8997
+ return children;
8998
+ }
8999
+
9000
+ function App() {
9001
+ return (
9002
+ <html>
9003
+ <Suspense fallback="this fallback never renders">
9004
+ <meta content="before" />
9005
+ <meta itemProp="" content="before" />
9006
+ </Suspense>
9007
+ <Suspense fallback="this fallback never renders">
9008
+ <BlockedOn value="body">
9009
+ <body lang="en">
9010
+ <div>hello world</div>
9011
+ </body>
9012
+ </BlockedOn>
9013
+ </Suspense>
9014
+ <Suspense fallback="this fallback never renders">
9015
+ <meta content="after" />
9016
+ <meta itemProp="" content="after" />
9017
+ </Suspense>
9018
+ </html>
9019
+ );
9020
+ }
9021
+
9022
+ let content = '';
9023
+ writable.on('data', chunk => (content += chunk));
9024
+
9025
+ let shellReady = false;
9026
+ await act(() => {
9027
+ const {pipe} = renderToPipeableStream(<App />, {
9028
+ onShellReady() {
9029
+ shellReady = true;
9030
+ },
9031
+ });
9032
+ pipe(writable);
9033
+ });
9034
+
9035
+ expect(shellReady).toBe(true);
9036
+ expect(content).toBe('');
9037
+
9038
+ await act(() => {
9039
+ resolveText('body');
9040
+ });
9041
+ expect(content).toMatch(/^<!DOCTYPE html>/);
9042
+ expect(getVisibleChildren(document)).toEqual(
9043
+ <html>
9044
+ <head>
9045
+ <meta content="before" />
9046
+ <meta content="after" />
9047
+ </head>
9048
+ <body lang="en">
9049
+ <meta itemprop="" content="before" />
9050
+ <div>hello world</div>
9051
+ <meta itemprop="" content="after" />
9052
+ </body>
9053
+ </html>,
9054
+ );
9055
+ });
9056
+
9057
+ it('can server render Suspense before, after, and around <head>', async () => {
9058
+ function BlockedOn({value, children}) {
9059
+ readText(value);
9060
+ return children;
9061
+ }
9062
+
9063
+ function App() {
9064
+ return (
9065
+ <html>
9066
+ <Suspense fallback="this fallback never renders">
9067
+ <meta content="before" />
9068
+ <meta itemProp="" content="before" />
9069
+ </Suspense>
9070
+ <Suspense fallback="this fallback never renders">
9071
+ <BlockedOn value="head">
9072
+ <head lang="en">
9073
+ <meta itemProp="" />
9074
+ </head>
9075
+ </BlockedOn>
9076
+ </Suspense>
9077
+ <Suspense fallback="this fallback never renders">
9078
+ <meta content="after" />
9079
+ <meta itemProp="" content="after" />
9080
+ </Suspense>
9081
+ <body>
9082
+ <div>hello world</div>
9083
+ </body>
9084
+ </html>
9085
+ );
9086
+ }
9087
+
9088
+ let content = '';
9089
+ writable.on('data', chunk => (content += chunk));
9090
+
9091
+ let shellReady = false;
9092
+ await act(() => {
9093
+ const {pipe} = renderToPipeableStream(<App />, {
9094
+ onShellReady() {
9095
+ shellReady = true;
9096
+ },
9097
+ });
9098
+ pipe(writable);
9099
+ });
9100
+
9101
+ expect(shellReady).toBe(true);
9102
+ expect(content).toBe('');
9103
+
9104
+ await act(() => {
9105
+ resolveText('head');
9106
+ });
9107
+ expect(content).toMatch(/^<!DOCTYPE html>/);
9108
+ expect(getVisibleChildren(document)).toEqual(
9109
+ <html>
9110
+ <head lang="en">
9111
+ <meta content="before" />
9112
+ <meta content="after" />
9113
+ <meta itemprop="" />
9114
+ </head>
9115
+ <body>
9116
+ <meta itemprop="" content="before" />
9117
+ <meta itemprop="" content="after" />
9118
+ <div>hello world</div>
9119
+ </body>
9120
+ </html>,
9121
+ );
9122
+ });
9123
+
9124
+ it('will render fallback Document when erroring a boundary above the body', async () => {
9125
+ function Boom() {
9126
+ throw new Error('Boom!');
9127
+ }
9128
+
9129
+ function App() {
9130
+ return (
9131
+ <Suspense
9132
+ fallback={
9133
+ <html data-error-html="">
9134
+ <body data-error-body="">
9135
+ <span>hello error</span>
9136
+ </body>
9137
+ </html>
9138
+ }>
9139
+ <html data-content-html="">
9140
+ <body data-content-body="">
9141
+ <Boom />
9142
+ <span>hello world</span>
9143
+ </body>
9144
+ </html>
9145
+ </Suspense>
9146
+ );
9147
+ }
9148
+
9149
+ let content = '';
9150
+ writable.on('data', chunk => (content += chunk));
9151
+
9152
+ let shellReady = false;
9153
+ const errors = [];
9154
+ await act(() => {
9155
+ const {pipe} = renderToPipeableStream(<App />, {
9156
+ onShellReady() {
9157
+ shellReady = true;
9158
+ },
9159
+ onError(e) {
9160
+ errors.push(e);
9161
+ },
9162
+ });
9163
+ pipe(writable);
9164
+ });
9165
+
9166
+ expect(shellReady).toBe(true);
9167
+ expect(content).toMatch(/^<!DOCTYPE html>/);
9168
+ expect(errors).toEqual([new Error('Boom!')]);
9169
+ expect(getVisibleChildren(document)).toEqual(
9170
+ <html data-error-html="">
9171
+ <head />
9172
+ <body data-error-body="">
9173
+ <span>hello error</span>
9174
+ </body>
9175
+ </html>,
9176
+ );
9177
+ });
9178
+
9179
+ it('will hoist resources and hositables from a primary tree into the <head> of a client rendered fallback', async () => {
9180
+ function Boom() {
9181
+ throw new Error('Boom!');
9182
+ }
9183
+
9184
+ function App() {
9185
+ return (
9186
+ <>
9187
+ <meta content="hoistable before" />
9188
+ <link rel="stylesheet" href="hoistable before" precedence="default" />
9189
+ <Suspense
9190
+ fallback={
9191
+ <html data-error-html="">
9192
+ <head data-error-head="">
9193
+ {/* we have to make this a non-hoistable because we don't current emit
9194
+ hoistables inside fallbacks because we have no way to clean them up
9195
+ on hydration */}
9196
+ <meta itemProp="" content="error document" />
9197
+ </head>
9198
+ <body data-error-body="">
9199
+ <span>hello error</span>
9200
+ </body>
9201
+ </html>
9202
+ }>
9203
+ <html data-content-html="">
9204
+ <body data-content-body="">
9205
+ <Boom />
9206
+ <span>hello world</span>
9207
+ </body>
9208
+ </html>
9209
+ </Suspense>
9210
+ <meta content="hoistable after" />
9211
+ <link rel="stylesheet" href="hoistable after" precedence="default" />
9212
+ </>
9213
+ );
9214
+ }
9215
+
9216
+ let content = '';
9217
+ writable.on('data', chunk => (content += chunk));
9218
+
9219
+ let shellReady = false;
9220
+ const errors = [];
9221
+ await act(() => {
9222
+ const {pipe} = renderToPipeableStream(<App />, {
9223
+ onShellReady() {
9224
+ shellReady = true;
9225
+ },
9226
+ onError(e) {
9227
+ errors.push(e);
9228
+ },
9229
+ });
9230
+ pipe(writable);
9231
+ });
9232
+
9233
+ expect(shellReady).toBe(true);
9234
+ expect(content).toMatch(/^<!DOCTYPE html>/);
9235
+ expect(errors).toEqual([new Error('Boom!')]);
9236
+ expect(getVisibleChildren(document)).toEqual(
9237
+ <html data-error-html="">
9238
+ <head data-error-head="">
9239
+ <link
9240
+ rel="stylesheet"
9241
+ href="hoistable before"
9242
+ data-precedence="default"
9243
+ />
9244
+ <link
9245
+ rel="stylesheet"
9246
+ href="hoistable after"
9247
+ data-precedence="default"
9248
+ />
9249
+ <meta content="hoistable before" />
9250
+ <meta content="hoistable after" />
9251
+ <meta itemprop="" content="error document" />
9252
+ </head>
9253
+ <body data-error-body="">
9254
+ <span>hello error</span>
9255
+ </body>
9256
+ </html>,
9257
+ );
9258
+ });
9259
+
9260
+ it('Will wait to flush Document chunks until all boundaries which might contain a preamble are errored or resolved', async () => {
9261
+ let rejectFirst;
9262
+ const firstPromise = new Promise((_, reject) => {
9263
+ rejectFirst = reject;
9264
+ });
9265
+ function First({children}) {
9266
+ use(firstPromise);
9267
+ return children;
9268
+ }
9269
+
9270
+ let resolveSecond;
9271
+ const secondPromise = new Promise(resolve => {
9272
+ resolveSecond = resolve;
9273
+ });
9274
+ function Second({children}) {
9275
+ use(secondPromise);
9276
+ return children;
9277
+ }
9278
+
9279
+ const hangingPromise = new Promise(() => {});
9280
+ function Hanging({children}) {
9281
+ use(hangingPromise);
9282
+ return children;
9283
+ }
9284
+
9285
+ function App() {
9286
+ return (
9287
+ <>
9288
+ <Suspense fallback={<span>loading...</span>}>
9289
+ <Suspense fallback={<span>inner loading...</span>}>
9290
+ <First>
9291
+ <span>first</span>
9292
+ </First>
9293
+ </Suspense>
9294
+ </Suspense>
9295
+ <Suspense fallback={<span>loading...</span>}>
9296
+ <main>
9297
+ <Second>
9298
+ <span>second</span>
9299
+ </Second>
9300
+ </main>
9301
+ </Suspense>
9302
+ <div>
9303
+ <Suspense fallback={<span>loading...</span>}>
9304
+ <Hanging>
9305
+ <span>third</span>
9306
+ </Hanging>
9307
+ </Suspense>
9308
+ </div>
9309
+ </>
9310
+ );
9311
+ }
9312
+
9313
+ let content = '';
9314
+ writable.on('data', chunk => (content += chunk));
9315
+
9316
+ let shellReady = false;
9317
+ const errors = [];
9318
+ await act(() => {
9319
+ const {pipe} = renderToPipeableStream(<App />, {
9320
+ onShellReady() {
9321
+ shellReady = true;
9322
+ },
9323
+ onError(e) {
9324
+ errors.push(e);
9325
+ },
9326
+ });
9327
+ pipe(writable);
9328
+ });
9329
+
9330
+ expect(shellReady).toBe(true);
9331
+ expect(content).toBe('');
9332
+
9333
+ await act(() => {
9334
+ resolveSecond();
9335
+ });
9336
+ expect(content).toBe('');
9337
+
9338
+ await act(() => {
9339
+ rejectFirst('Boom!');
9340
+ });
9341
+ expect(content.length).toBeGreaterThan(0);
9342
+ expect(errors).toEqual(['Boom!']);
9343
+
9344
+ expect(getVisibleChildren(container)).toEqual([
9345
+ <span>inner loading...</span>,
9346
+ <main>
9347
+ <span>second</span>
9348
+ </main>,
9349
+ <div>
9350
+ <span>loading...</span>
9351
+ </div>,
9352
+ ]);
9353
+ });
9354
+
9355
+ it('Can render a fallback <head> alongside a non-fallback body', async () => {
9356
+ function Boom() {
9357
+ throw new Error('Boom!');
9358
+ }
9359
+
9360
+ function App() {
9361
+ return (
9362
+ <html>
9363
+ <Suspense
9364
+ fallback={
9365
+ <head data-fallback="">
9366
+ <meta itemProp="" content="fallback" />
9367
+ </head>
9368
+ }>
9369
+ <Boom />
9370
+ <head data-primary="">
9371
+ <meta itemProp="" content="primary" />
9372
+ </head>
9373
+ </Suspense>
9374
+ <Suspense
9375
+ fallback={
9376
+ <body data-fallback="">
9377
+ <div>fallback body</div>
9378
+ </body>
9379
+ }>
9380
+ <body data-primary="">
9381
+ <div>primary body</div>
9382
+ </body>
9383
+ </Suspense>
9384
+ </html>
9385
+ );
9386
+ }
9387
+
9388
+ let content = '';
9389
+ writable.on('data', chunk => (content += chunk));
9390
+
9391
+ let shellReady = false;
9392
+ const errors = [];
9393
+ await act(() => {
9394
+ const {pipe} = renderToPipeableStream(<App />, {
9395
+ onShellReady() {
9396
+ shellReady = true;
9397
+ },
9398
+ onError(e) {
9399
+ errors.push(e);
9400
+ },
9401
+ });
9402
+ pipe(writable);
9403
+ });
9404
+
9405
+ expect(shellReady).toBe(true);
9406
+ expect(content).toMatch(/^<!DOCTYPE html>/);
9407
+ expect(errors).toEqual([new Error('Boom!')]);
9408
+
9409
+ expect(getVisibleChildren(document)).toEqual(
9410
+ <html>
9411
+ <head data-fallback="">
9412
+ <meta itemprop="" content="fallback" />
9413
+ </head>
9414
+ <body data-primary="">
9415
+ <div>primary body</div>
9416
+ </body>
9417
+ </html>,
9418
+ );
9419
+ });
9420
+
9421
+ it('Can render a fallback <body> alongside a non-fallback head', async () => {
9422
+ function Boom() {
9423
+ throw new Error('Boom!');
9424
+ }
9425
+
9426
+ function App() {
9427
+ return (
9428
+ <html>
9429
+ <Suspense
9430
+ fallback={
9431
+ <head data-fallback="">
9432
+ <meta itemProp="" content="fallback" />
9433
+ </head>
9434
+ }>
9435
+ <head data-primary="">
9436
+ <meta itemProp="" content="primary" />
9437
+ </head>
9438
+ </Suspense>
9439
+ <Suspense
9440
+ fallback={
9441
+ <body data-fallback="">
9442
+ <div>fallback body</div>
9443
+ </body>
9444
+ }>
9445
+ <Boom />
9446
+ <body data-primary="">
9447
+ <div>primary body</div>
9448
+ </body>
9449
+ </Suspense>
9450
+ </html>
9451
+ );
9452
+ }
9453
+
9454
+ let content = '';
9455
+ writable.on('data', chunk => (content += chunk));
9456
+
9457
+ let shellReady = false;
9458
+ const errors = [];
9459
+ await act(() => {
9460
+ const {pipe} = renderToPipeableStream(<App />, {
9461
+ onShellReady() {
9462
+ shellReady = true;
9463
+ },
9464
+ onError(e) {
9465
+ errors.push(e);
9466
+ },
9467
+ });
9468
+ pipe(writable);
9469
+ });
9470
+
9471
+ expect(shellReady).toBe(true);
9472
+ expect(content).toMatch(/^<!DOCTYPE html>/);
9473
+ expect(errors).toEqual([new Error('Boom!')]);
9474
+
9475
+ expect(getVisibleChildren(document)).toEqual(
9476
+ <html>
9477
+ <head data-primary="">
9478
+ <meta itemprop="" content="primary" />
9479
+ </head>
9480
+ <body data-fallback="">
9481
+ <div>fallback body</div>
9482
+ </body>
9483
+ </html>,
9484
+ );
9485
+ });
9486
+
9487
+ it('Can render a <head> outside of a containing <html>', async () => {
9488
+ function App() {
9489
+ return (
9490
+ <>
9491
+ <Suspense>
9492
+ <html data-x="">
9493
+ <body data-x="">
9494
+ <span>hello world</span>
9495
+ </body>
9496
+ </html>
9497
+ </Suspense>
9498
+ <head data-y="">
9499
+ <meta itemProp="" />
9500
+ </head>
9501
+ </>
9502
+ );
9503
+ }
9504
+
9505
+ let content = '';
9506
+ writable.on('data', chunk => (content += chunk));
9507
+
9508
+ let shellReady = false;
9509
+ await act(() => {
9510
+ const {pipe} = renderToPipeableStream(<App />, {
9511
+ onShellReady() {
9512
+ shellReady = true;
9513
+ },
9514
+ });
9515
+ pipe(writable);
9516
+ });
9517
+
9518
+ expect(shellReady).toBe(true);
9519
+ expect(content).toMatch(/^<!DOCTYPE html>/);
9520
+
9521
+ expect(getVisibleChildren(document)).toEqual(
9522
+ <html data-x="">
9523
+ <head data-y="">
9524
+ <meta itemprop="" />
9525
+ </head>
9526
+ <body data-x="">
9527
+ <span>hello world</span>
9528
+ </body>
9529
+ </html>,
9530
+ );
9531
+ });
9532
+
9533
+ it('can render preamble tags in deeply nested indirect component trees', async () => {
9534
+ function App() {
9535
+ return (
9536
+ <Html>
9537
+ <DocumentMetadata />
9538
+ <Main />
9539
+ </Html>
9540
+ );
9541
+ }
9542
+
9543
+ let loadLanguage;
9544
+ const langPromise = new Promise(r => {
9545
+ loadLanguage = r;
9546
+ });
9547
+ function Html({children}) {
9548
+ return (
9549
+ <Suspense fallback={<FallbackHtml>{children}</FallbackHtml>}>
9550
+ <MainHtml>{children}</MainHtml>
9551
+ </Suspense>
9552
+ );
9553
+ }
9554
+ function FallbackHtml({children}) {
9555
+ return <html lang="default">{children}</html>;
9556
+ }
9557
+ function MainHtml({children}) {
9558
+ const lang = use(langPromise);
9559
+ return <html lang={lang}>{children}</html>;
9560
+ }
9561
+
9562
+ let loadMetadata;
9563
+ const metadataPromise = new Promise(r => {
9564
+ loadMetadata = r;
9565
+ });
9566
+ function DocumentMetadata() {
9567
+ return (
9568
+ <Suspense fallback={<FallbackDocumentMetadata />}>
9569
+ <MainDocumentMetadata />
9570
+ </Suspense>
9571
+ );
9572
+ }
9573
+ function FallbackDocumentMetadata() {
9574
+ return (
9575
+ <head data-fallback="">
9576
+ <meta content="fallback metadata" />
9577
+ </head>
9578
+ );
9579
+ }
9580
+ function MainDocumentMetadata() {
9581
+ const metadata = use(metadataPromise);
9582
+ return (
9583
+ <head data-main="">
9584
+ {metadata.map(m => (
9585
+ <meta content={m} key={m} />
9586
+ ))}
9587
+ </head>
9588
+ );
9589
+ }
9590
+
9591
+ let loadMainContent;
9592
+ const mainContentPromise = new Promise(r => {
9593
+ loadMainContent = r;
9594
+ });
9595
+ function Main() {
9596
+ return (
9597
+ <Suspense fallback={<Skeleton />}>
9598
+ <PrimaryContent />
9599
+ </Suspense>
9600
+ );
9601
+ }
9602
+ function Skeleton() {
9603
+ return (
9604
+ <body data-fallback="">
9605
+ <div>Skeleton UI</div>
9606
+ </body>
9607
+ );
9608
+ }
9609
+ function PrimaryContent() {
9610
+ const content = use(mainContentPromise);
9611
+ return (
9612
+ <body data-main="">
9613
+ <div>{content}</div>
9614
+ </body>
9615
+ );
9616
+ }
9617
+
9618
+ let content = '';
9619
+ writable.on('data', chunk => (content += chunk));
9620
+
9621
+ let shellReady = false;
9622
+ const errors = [];
9623
+ await act(() => {
9624
+ const {pipe} = renderToPipeableStream(<App />, {
9625
+ onShellReady() {
9626
+ shellReady = true;
9627
+ },
9628
+ onError(e) {
9629
+ errors.push(e);
9630
+ },
9631
+ });
9632
+ pipe(writable);
9633
+ });
9634
+
9635
+ expect(shellReady).toBe(true);
9636
+ expect(content).toBe('');
9637
+
9638
+ await act(() => {
9639
+ loadLanguage('es');
9640
+ });
9641
+ expect(content).toBe('');
9642
+
9643
+ await act(() => {
9644
+ loadMainContent('This is soooo cool!');
9645
+ });
9646
+ expect(content).toBe('');
9647
+
9648
+ await act(() => {
9649
+ loadMetadata(['author', 'published date']);
9650
+ });
9651
+ expect(content).toMatch(/^<!DOCTYPE html>/);
9652
+
9653
+ expect(getVisibleChildren(document)).toEqual(
9654
+ <html lang="es">
9655
+ <head data-main="">
9656
+ <meta content="author" />
9657
+ <meta content="published date" />
9658
+ </head>
9659
+ <body data-main="">
9660
+ <div>This is soooo cool!</div>
9661
+ </body>
9662
+ </html>,
9663
+ );
9664
+ });
9665
+
9666
+ it('will flush the preamble as soon as a complete preamble is available', async () => {
9667
+ function BlockedOn({value, children}) {
9668
+ readText(value);
9669
+ return children;
9670
+ }
9671
+
9672
+ function App() {
9673
+ return (
9674
+ <>
9675
+ <Suspense fallback="loading before...">
9676
+ <div>
9677
+ <AsyncText text="before" />
9678
+ </div>
9679
+ </Suspense>
9680
+ <Suspense fallback="loading document...">
9681
+ <html>
9682
+ <body>
9683
+ <div>
9684
+ <AsyncText text="body" />
9685
+ </div>
9686
+ </body>
9687
+ </html>
9688
+ </Suspense>
9689
+ <Suspense fallback="loading head...">
9690
+ <head>
9691
+ <BlockedOn value="head">
9692
+ <meta content="head" />
9693
+ </BlockedOn>
9694
+ </head>
9695
+ </Suspense>
9696
+ <Suspense fallback="loading after...">
9697
+ <div>
9698
+ <AsyncText text="after" />
9699
+ </div>
9700
+ </Suspense>
9701
+ </>
9702
+ );
9703
+ }
9704
+
9705
+ let content = '';
9706
+ writable.on('data', chunk => (content += chunk));
9707
+
9708
+ let shellReady = false;
9709
+ await act(() => {
9710
+ const {pipe} = renderToPipeableStream(<App />, {
9711
+ onShellReady() {
9712
+ shellReady = true;
9713
+ },
9714
+ });
9715
+ pipe(writable);
9716
+ });
9717
+
9718
+ expect(shellReady).toBe(true);
9719
+ expect(content).toBe('');
9720
+
9721
+ await act(() => {
9722
+ resolveText('body');
9723
+ });
9724
+ expect(content).toBe('');
9725
+
9726
+ await act(() => {
9727
+ resolveText('head');
9728
+ });
9729
+ expect(content).toMatch(/^<!DOCTYPE html>/);
9730
+
9731
+ expect(getVisibleChildren(document)).toEqual(
9732
+ <html>
9733
+ <head>
9734
+ <meta content="head" />
9735
+ </head>
9736
+ <body>
9737
+ loading before...
9738
+ <div>body</div>
9739
+ loading after...
9740
+ </body>
9741
+ </html>,
9742
+ );
9743
+ });
9744
});