36
let JSDOM;
37
let ReactServerScheduler;
38
let reactServerAct;
39
+let assertConsoleErrorDev;
40
41
describe('ReactFlightDOM', () => {
42
beforeEach(() => {
71
__unmockReact();
72
jest.resetModules();
73
act = require('internal-test-utils').act;
74
+ assertConsoleErrorDev =
75
+ require('internal-test-utils').assertConsoleErrorDev;
76
Stream = require('stream');
77
React = require('react');
78
use = React.use;
110
return maybePromise;
111
}
112
113
+ async function readInto(
114
+ container: Document | HTMLElement,
115
+ stream: ReadableStream,
116
+ ) {
117
+ const reader = stream.getReader();
118
+ const decoder = new TextDecoder();
119
+ let content = '';
120
+ while (true) {
121
+ const {done, value} = await reader.read();
122
+ if (done) {
123
+ content += decoder.decode();
124
+ break;
125
+ }
126
+ content += decoder.decode(value, {stream: true});
127
+ }
128
+ if (container.nodeType === 9 /* DOCUMENT */) {
129
+ const doc = new JSDOM(content).window.document;
130
+ container.documentElement.innerHTML = doc.documentElement.innerHTML;
131
+ while (container.documentElement.attributes.length > 0) {
132
+ container.documentElement.removeAttribute(
133
+ container.documentElement.attributes[0].name,
134
+ );
135
+ }
136
+ const attrs = doc.documentElement.attributes;
137
+ for (let i = 0; i < attrs.length; i++) {
138
+ container.documentElement.setAttribute(attrs[i].name, attrs[i].value);
139
+ }
140
+ } else {
141
+ container.innerHTML = content;
142
+ }
143
+ }
144
+
145
function getTestStream() {
146
const writable = new Stream.PassThrough();
147
const readable = new ReadableStream({
1668
ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
1669
});
1670
1636
- const decoder = new TextDecoder();
1637
- const reader = fizzReadable.getReader();
1638
- let content = '';
1639
- while (true) {
1640
- const {done, value} = await reader.read();
1641
- if (done) {
1642
- content += decoder.decode();
1643
- break;
1644
- }
1645
- content += decoder.decode(value, {stream: true});
1646
- }
1647
-
1648
- const doc = new JSDOM(content).window.document;
1649
- expect(getMeaningfulChildren(doc)).toEqual(
1671
+ await readInto(document, fizzReadable);
1672
+ expect(getMeaningfulChildren(document)).toEqual(
1673
<html>
1674
<head>
1675
<link rel="dns-prefetch" href="d before" />
1935
});
1936
expect(container.innerHTML).toBe('Hello World');
1937
});
1938
+
1939
+ it('can abort synchronously during render', async () => {
1940
+ function Sibling() {
1941
+ return <p>sibling</p>;
1942
+ }
1943
+
1944
+ function App() {
1945
+ return (
1946
+ <div>
1947
+ <Suspense fallback={<p>loading 1...</p>}>
1948
+ <ComponentThatAborts />
1949
+ <Sibling />
1950
+ </Suspense>
1951
+ <Suspense fallback={<p>loading 2...</p>}>
1952
+ <Sibling />
1953
+ </Suspense>
1954
+ <div>
1955
+ <Suspense fallback={<p>loading 3...</p>}>
1956
+ <div>
1957
+ <Sibling />
1958
+ </div>
1959
+ </Suspense>
1960
+ </div>
1961
+ </div>
1962
+ );
1963
+ }
1964
+
1965
+ const abortRef = {current: null};
1966
+ function ComponentThatAborts() {
1967
+ abortRef.current();
1968
+ return <p>hello world</p>;
1969
+ }
1970
+
1971
+ const {writable: flightWritable, readable: flightReadable} =
1972
+ getTestStream();
1973
+
1974
+ await serverAct(() => {
1975
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
1976
+ <App />,
1977
+ webpackMap,
1978
+ );
1979
+ abortRef.current = abort;
1980
+ pipe(flightWritable);
1981
+ });
1982
+ assertConsoleErrorDev([
1983
+ 'The render was aborted by the server without a reason.',
1984
+ ]);
1985
+
1986
+ const response =
1987
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
1988
+
1989
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
1990
+
1991
+ function ClientApp() {
1992
+ return use(response);
1993
+ }
1994
+
1995
+ const shellErrors = [];
1996
+ await serverAct(async () => {
1997
+ ReactDOMFizzServer.renderToPipeableStream(
1998
+ React.createElement(ClientApp),
1999
+ {
2000
+ onShellError(error) {
2001
+ shellErrors.push(error.message);
2002
+ },
2003
+ },
2004
+ ).pipe(fizzWritable);
2005
+ });
2006
+ assertConsoleErrorDev([
2007
+ 'The render was aborted by the server without a reason.',
2008
+ 'The render was aborted by the server without a reason.',
2009
+ 'The render was aborted by the server without a reason.',
2010
+ ]);
2011
+
2012
+ expect(shellErrors).toEqual([]);
2013
+
2014
+ const container = document.createElement('div');
2015
+ await readInto(container, fizzReadable);
2016
+ expect(getMeaningfulChildren(container)).toEqual(
2017
+ <div>
2018
+ <p>loading 1...</p>
2019
+ <p>loading 2...</p>
2020
+ <div>
2021
+ <p>loading 3...</p>
2022
+ </div>
2023
+ </div>,
2024
+ );
2025
+ });
2026
+
2027
+ it('can abort during render in an async tick', async () => {
2028
+ async function Sibling() {
2029
+ return <p>sibling</p>;
2030
+ }
2031
+
2032
+ function App() {
2033
+ return (
2034
+ <div>
2035
+ <Suspense fallback={<p>loading 1...</p>}>
2036
+ <ComponentThatAborts />
2037
+ <Sibling />
2038
+ </Suspense>
2039
+ <Suspense fallback={<p>loading 2...</p>}>
2040
+ <Sibling />
2041
+ </Suspense>
2042
+ <div>
2043
+ <Suspense fallback={<p>loading 3...</p>}>
2044
+ <div>
2045
+ <Sibling />
2046
+ </div>
2047
+ </Suspense>
2048
+ </div>
2049
+ </div>
2050
+ );
2051
+ }
2052
+
2053
+ const abortRef = {current: null};
2054
+ async function ComponentThatAborts() {
2055
+ await 1;
2056
+ abortRef.current();
2057
+ return <p>hello world</p>;
2058
+ }
2059
+
2060
+ const {writable: flightWritable, readable: flightReadable} =
2061
+ getTestStream();
2062
+
2063
+ await serverAct(() => {
2064
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2065
+ <App />,
2066
+ webpackMap,
2067
+ );
2068
+ abortRef.current = abort;
2069
+ pipe(flightWritable);
2070
+ });
2071
+
2072
+ assertConsoleErrorDev([
2073
+ 'The render was aborted by the server without a reason.',
2074
+ ]);
2075
+
2076
+ const response =
2077
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
2078
+
2079
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2080
+
2081
+ function ClientApp() {
2082
+ return use(response);
2083
+ }
2084
+
2085
+ const shellErrors = [];
2086
+ await serverAct(async () => {
2087
+ ReactDOMFizzServer.renderToPipeableStream(
2088
+ React.createElement(ClientApp),
2089
+ {
2090
+ onShellError(error) {
2091
+ shellErrors.push(error.message);
2092
+ },
2093
+ },
2094
+ ).pipe(fizzWritable);
2095
+ });
2096
+
2097
+ assertConsoleErrorDev([
2098
+ 'The render was aborted by the server without a reason.',
2099
+ 'The render was aborted by the server without a reason.',
2100
+ 'The render was aborted by the server without a reason.',
2101
+ ]);
2102
+
2103
+ expect(shellErrors).toEqual([]);
2104
+
2105
+ const container = document.createElement('div');
2106
+ await readInto(container, fizzReadable);
2107
+ expect(getMeaningfulChildren(container)).toEqual(
2108
+ <div>
2109
+ <p>loading 1...</p>
2110
+ <p>loading 2...</p>
2111
+ <div>
2112
+ <p>loading 3...</p>
2113
+ </div>
2114
+ </div>,
2115
+ );
2116
+ });
2117
+
2118
+ it('can abort during render in a lazy initializer for a component', async () => {
2119
+ function Sibling() {
2120
+ return <p>sibling</p>;
2121
+ }
2122
+
2123
+ function App() {
2124
+ return (
2125
+ <div>
2126
+ <Suspense fallback={<p>loading 1...</p>}>
2127
+ <LazyAbort />
2128
+ </Suspense>
2129
+ <Suspense fallback={<p>loading 2...</p>}>
2130
+ <Sibling />
2131
+ </Suspense>
2132
+ <div>
2133
+ <Suspense fallback={<p>loading 3...</p>}>
2134
+ <div>
2135
+ <Sibling />
2136
+ </div>
2137
+ </Suspense>
2138
+ </div>
2139
+ </div>
2140
+ );
2141
+ }
2142
+
2143
+ const abortRef = {current: null};
2144
+ const LazyAbort = React.lazy(() => {
2145
+ abortRef.current();
2146
+ return {
2147
+ then(cb) {
2148
+ cb({default: 'div'});
2149
+ },
2150
+ };
2151
+ });
2152
+
2153
+ const {writable: flightWritable, readable: flightReadable} =
2154
+ getTestStream();
2155
+
2156
+ await serverAct(() => {
2157
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2158
+ <App />,
2159
+ webpackMap,
2160
+ );
2161
+ abortRef.current = abort;
2162
+ pipe(flightWritable);
2163
+ });
2164
+ assertConsoleErrorDev([
2165
+ 'The render was aborted by the server without a reason.',
2166
+ ]);
2167
+
2168
+ const response =
2169
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
2170
+
2171
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2172
+
2173
+ function ClientApp() {
2174
+ return use(response);
2175
+ }
2176
+
2177
+ const shellErrors = [];
2178
+ await serverAct(async () => {
2179
+ ReactDOMFizzServer.renderToPipeableStream(
2180
+ React.createElement(ClientApp),
2181
+ {
2182
+ onShellError(error) {
2183
+ shellErrors.push(error.message);
2184
+ },
2185
+ },
2186
+ ).pipe(fizzWritable);
2187
+ });
2188
+ assertConsoleErrorDev([
2189
+ 'The render was aborted by the server without a reason.',
2190
+ 'The render was aborted by the server without a reason.',
2191
+ 'The render was aborted by the server without a reason.',
2192
+ ]);
2193
+
2194
+ expect(shellErrors).toEqual([]);
2195
+
2196
+ const container = document.createElement('div');
2197
+ await readInto(container, fizzReadable);
2198
+ expect(getMeaningfulChildren(container)).toEqual(
2199
+ <div>
2200
+ <p>loading 1...</p>
2201
+ <p>loading 2...</p>
2202
+ <div>
2203
+ <p>loading 3...</p>
2204
+ </div>
2205
+ </div>,
2206
+ );
2207
+ });
2208
+
2209
+ it('can abort during render in a lazy initializer for an element', async () => {
2210
+ function Sibling() {
2211
+ return <p>sibling</p>;
2212
+ }
2213
+
2214
+ function App() {
2215
+ return (
2216
+ <div>
2217
+ <Suspense fallback={<p>loading 1...</p>}>{lazyAbort}</Suspense>
2218
+ <Suspense fallback={<p>loading 2...</p>}>
2219
+ <Sibling />
2220
+ </Suspense>
2221
+ <div>
2222
+ <Suspense fallback={<p>loading 3...</p>}>
2223
+ <div>
2224
+ <Sibling />
2225
+ </div>
2226
+ </Suspense>
2227
+ </div>
2228
+ </div>
2229
+ );
2230
+ }
2231
+
2232
+ const abortRef = {current: null};
2233
+ const lazyAbort = React.lazy(() => {
2234
+ abortRef.current();
2235
+ return {
2236
+ then(cb) {
2237
+ cb({default: 'hello world'});
2238
+ },
2239
+ };
2240
+ });
2241
+
2242
+ const {writable: flightWritable, readable: flightReadable} =
2243
+ getTestStream();
2244
+
2245
+ await serverAct(() => {
2246
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2247
+ <App />,
2248
+ webpackMap,
2249
+ );
2250
+ abortRef.current = abort;
2251
+ pipe(flightWritable);
2252
+ });
2253
+ assertConsoleErrorDev([
2254
+ 'The render was aborted by the server without a reason.',
2255
+ ]);
2256
+
2257
+ const response =
2258
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
2259
+
2260
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2261
+
2262
+ function ClientApp() {
2263
+ return use(response);
2264
+ }
2265
+
2266
+ const shellErrors = [];
2267
+ await serverAct(async () => {
2268
+ ReactDOMFizzServer.renderToPipeableStream(
2269
+ React.createElement(ClientApp),
2270
+ {
2271
+ onShellError(error) {
2272
+ shellErrors.push(error.message);
2273
+ },
2274
+ },
2275
+ ).pipe(fizzWritable);
2276
+ });
2277
+ assertConsoleErrorDev([
2278
+ 'The render was aborted by the server without a reason.',
2279
+ 'The render was aborted by the server without a reason.',
2280
+ 'The render was aborted by the server without a reason.',
2281
+ ]);
2282
+
2283
+ expect(shellErrors).toEqual([]);
2284
+
2285
+ const container = document.createElement('div');
2286
+ await readInto(container, fizzReadable);
2287
+ expect(getMeaningfulChildren(container)).toEqual(
2288
+ <div>
2289
+ <p>loading 1...</p>
2290
+ <p>loading 2...</p>
2291
+ <div>
2292
+ <p>loading 3...</p>
2293
+ </div>
2294
+ </div>,
2295
+ );
2296
+ });
2297
+
2298
+ it('can abort during a synchronous thenable resolution', async () => {
2299
+ function Sibling() {
2300
+ return <p>sibling</p>;
2301
+ }
2302
+
2303
+ function App() {
2304
+ return (
2305
+ <div>
2306
+ <Suspense fallback={<p>loading 1...</p>}>{thenable}</Suspense>
2307
+ <Suspense fallback={<p>loading 2...</p>}>
2308
+ <Sibling />
2309
+ </Suspense>
2310
+ <div>
2311
+ <Suspense fallback={<p>loading 3...</p>}>
2312
+ <div>
2313
+ <Sibling />
2314
+ </div>
2315
+ </Suspense>
2316
+ </div>
2317
+ </div>
2318
+ );
2319
+ }
2320
+
2321
+ const abortRef = {current: null};
2322
+ const thenable = {
2323
+ then(cb) {
2324
+ abortRef.current();
2325
+ cb(thenable.value);
2326
+ },
2327
+ };
2328
+
2329
+ const {writable: flightWritable, readable: flightReadable} =
2330
+ getTestStream();
2331
+
2332
+ await serverAct(() => {
2333
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2334
+ <App />,
2335
+ webpackMap,
2336
+ );
2337
+ abortRef.current = abort;
2338
+ pipe(flightWritable);
2339
+ });
2340
+
2341
+ assertConsoleErrorDev([
2342
+ 'The render was aborted by the server without a reason.',
2343
+ ]);
2344
+
2345
+ const response =
2346
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
2347
+
2348
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2349
+
2350
+ function ClientApp() {
2351
+ return use(response);
2352
+ }
2353
+
2354
+ const shellErrors = [];
2355
+ await serverAct(async () => {
2356
+ ReactDOMFizzServer.renderToPipeableStream(
2357
+ React.createElement(ClientApp),
2358
+ {
2359
+ onShellError(error) {
2360
+ shellErrors.push(error.message);
2361
+ },
2362
+ },
2363
+ ).pipe(fizzWritable);
2364
+ });
2365
+ assertConsoleErrorDev([
2366
+ 'The render was aborted by the server without a reason.',
2367
+ 'The render was aborted by the server without a reason.',
2368
+ 'The render was aborted by the server without a reason.',
2369
+ ]);
2370
+
2371
+ expect(shellErrors).toEqual([]);
2372
+
2373
+ const container = document.createElement('div');
2374
+ await readInto(container, fizzReadable);
2375
+ expect(getMeaningfulChildren(container)).toEqual(
2376
+ <div>
2377
+ <p>loading 1...</p>
2378
+ <p>loading 2...</p>
2379
+ <div>
2380
+ <p>loading 3...</p>
2381
+ </div>
2382
+ </div>,
2383
+ );
2384
+ });
2385
+
2386
+ it('wont serialize thenables that were not already settled by the time an abort happens', async () => {
2387
+ function App() {
2388
+ return (
2389
+ <div>
2390
+ <Suspense fallback={<p>loading 1...</p>}>
2391
+ <ComponentThatAborts />
2392
+ </Suspense>
2393
+ <Suspense fallback={<p>loading 2...</p>}>{thenable1}</Suspense>
2394
+ <div>
2395
+ <Suspense fallback={<p>loading 3...</p>}>{thenable2}</Suspense>
2396
+ </div>
2397
+ </div>
2398
+ );
2399
+ }
2400
+
2401
+ const abortRef = {current: null};
2402
+ const thenable1 = {
2403
+ then(cb) {
2404
+ cb('hello world');
2405
+ },
2406
+ };
2407
+
2408
+ const thenable2 = {
2409
+ then(cb) {
2410
+ cb('hello world');
2411
+ },
2412
+ status: 'fulfilled',
2413
+ value: 'hello world',
2414
+ };
2415
+
2416
+ function ComponentThatAborts() {
2417
+ abortRef.current();
2418
+ return thenable1;
2419
+ }
2420
+
2421
+ const {writable: flightWritable, readable: flightReadable} =
2422
+ getTestStream();
2423
+
2424
+ await serverAct(() => {
2425
+ const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2426
+ <App />,
2427
+ webpackMap,
2428
+ );
2429
+ abortRef.current = abort;
2430
+ pipe(flightWritable);
2431
+ });
2432
+
2433
+ assertConsoleErrorDev([
2434
+ 'The render was aborted by the server without a reason.',
2435
+ ]);
2436
+
2437
+ const response =
2438
+ ReactServerDOMClient.createFromReadableStream(flightReadable);
2439
+
2440
+ const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2441
+
2442
+ function ClientApp() {
2443
+ return use(response);
2444
+ }
2445
+
2446
+ const shellErrors = [];
2447
+ await serverAct(async () => {
2448
+ ReactDOMFizzServer.renderToPipeableStream(
2449
+ React.createElement(ClientApp),
2450
+ {
2451
+ onShellError(error) {
2452
+ shellErrors.push(error.message);
2453
+ },
2454
+ },
2455
+ ).pipe(fizzWritable);
2456
+ });
2457
+ assertConsoleErrorDev([
2458
+ 'The render was aborted by the server without a reason.',
2459
+ 'The render was aborted by the server without a reason.',
2460
+ ]);
2461
+
2462
+ expect(shellErrors).toEqual([]);
2463
+
2464
+ const container = document.createElement('div');
2465
+ await readInto(container, fizzReadable);
2466
+ expect(getMeaningfulChildren(container)).toEqual(
2467
+ <div>
2468
+ <p>loading 1...</p>
2469
+ <p>loading 2...</p>
2470
+ <div>hello world</div>
2471
+ </div>,
2472
+ );
2473
+ });
2474
});