/
ReactDOMFizzServerBrowser-test.js
310 lines (274 loc) · 10.7 KB
/
ReactDOMFizzServerBrowser-test.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
// Polyfills for test environment
global.ReadableStream = require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
let React;
let ReactDOMFizzServer;
let Suspense;
describe('ReactDOMFizzServer', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
if (__EXPERIMENTAL__) {
ReactDOMFizzServer = require('react-dom/server.browser');
}
Suspense = React.Suspense;
});
const theError = new Error('This is an error');
function Throw() {
throw theError;
}
const theInfinitePromise = new Promise(() => {});
function InfiniteSuspend() {
throw theInfinitePromise;
}
async function readResult(stream) {
const reader = stream.getReader();
let result = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
return result;
}
result += Buffer.from(value).toString('utf8');
}
}
// @gate experimental
it('should call renderToReadableStream', async () => {
const stream = await ReactDOMFizzServer.renderToReadableStream(
<div>hello world</div>,
);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(`"<div>hello world</div>"`);
});
// @gate experimental
it('should emit DOCTYPE at the root of the document', async () => {
const stream = await ReactDOMFizzServer.renderToReadableStream(
<html>
<body>hello world</body>
</html>,
);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<!DOCTYPE html><html><body>hello world</body></html>"`,
);
});
// @gate experimental
it('should emit bootstrap script src at the end', async () => {
const stream = await ReactDOMFizzServer.renderToReadableStream(
<div>hello world</div>,
{
bootstrapScriptContent: 'INIT();',
bootstrapScripts: ['init.js'],
bootstrapModules: ['init.mjs'],
},
);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<div>hello world</div><script>INIT();</script><script src=\\"init.js\\" async=\\"\\"></script><script type=\\"module\\" src=\\"init.mjs\\" async=\\"\\"></script>"`,
);
});
// @gate experimental
it('emits all HTML as one unit if we wait until the end to start', async () => {
let hasLoaded = false;
let resolve;
const promise = new Promise(r => (resolve = r));
function Wait() {
if (!hasLoaded) {
throw promise;
}
return 'Done';
}
let isComplete = false;
const stream = await ReactDOMFizzServer.renderToReadableStream(
<div>
<Suspense fallback="Loading">
<Wait />
</Suspense>
</div>,
);
stream.allReady.then(() => (isComplete = true));
await jest.runAllTimers();
expect(isComplete).toBe(false);
// Resolve the loading.
hasLoaded = true;
await resolve();
await jest.runAllTimers();
expect(isComplete).toBe(true);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<div><!--$-->Done<!-- --><!--/$--></div>"`,
);
});
// @gate experimental
it('should reject the promise when an error is thrown at the root', async () => {
const reportedErrors = [];
let caughtError = null;
try {
await ReactDOMFizzServer.renderToReadableStream(
<div>
<Throw />
</div>,
{
onError(x) {
reportedErrors.push(x);
},
},
);
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(theError);
expect(reportedErrors).toEqual([theError]);
});
// @gate experimental
it('should reject the promise when an error is thrown inside a fallback', async () => {
const reportedErrors = [];
let caughtError = null;
try {
await ReactDOMFizzServer.renderToReadableStream(
<div>
<Suspense fallback={<Throw />}>
<InfiniteSuspend />
</Suspense>
</div>,
{
onError(x) {
reportedErrors.push(x);
},
},
);
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(theError);
expect(reportedErrors).toEqual([theError]);
});
// @gate experimental
it('should not error the stream when an error is thrown inside suspense boundary', async () => {
const reportedErrors = [];
const stream = await ReactDOMFizzServer.renderToReadableStream(
<div>
<Suspense fallback={<div>Loading</div>}>
<Throw />
</Suspense>
</div>,
{
onError(x) {
reportedErrors.push(x);
},
},
);
const result = await readResult(stream);
expect(result).toContain('Loading');
expect(reportedErrors).toEqual([theError]);
});
// @gate experimental
it('should be able to complete by aborting even if the promise never resolves', async () => {
const errors = [];
const controller = new AbortController();
const stream = await ReactDOMFizzServer.renderToReadableStream(
<div>
<Suspense fallback={<div>Loading</div>}>
<InfiniteSuspend />
</Suspense>
</div>,
{
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
},
);
controller.abort();
const result = await readResult(stream);
expect(result).toContain('Loading');
expect(errors).toEqual([
'This Suspense boundary was aborted by the server.',
]);
});
// @gate experimental
it('should not continue rendering after the reader cancels', async () => {
let hasLoaded = false;
let resolve;
let isComplete = false;
let rendered = false;
const promise = new Promise(r => (resolve = r));
function Wait() {
if (!hasLoaded) {
throw promise;
}
rendered = true;
return 'Done';
}
const errors = [];
const stream = await ReactDOMFizzServer.renderToReadableStream(
<div>
<Suspense fallback={<div>Loading</div>}>
<Wait /> />
</Suspense>
</div>,
{
onError(x) {
errors.push(x.message);
},
},
);
stream.allReady.then(() => (isComplete = true));
expect(rendered).toBe(false);
expect(isComplete).toBe(false);
const reader = stream.getReader();
reader.cancel();
expect(errors).toEqual([
'This Suspense boundary was aborted by the server.',
]);
hasLoaded = true;
resolve();
await jest.runAllTimers();
expect(rendered).toBe(false);
expect(isComplete).toBe(true);
});
// @gate experimental
it('should stream large contents that might overlow individual buffers', async () => {
const str492 = `(492) This string is intentionally 492 bytes long because we want to make sure we process chunks that will overflow buffer boundaries. It will repeat to fill out the bytes required (inclusive of this prompt):: foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux q :: total count (492)`;
const str2049 = `(2049) This string is intentionally 2049 bytes long because we want to make sure we process chunks that will overflow buffer boundaries. It will repeat to fill out the bytes required (inclusive of this prompt):: foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy thud foo bar qux quux corge grault garply waldo fred plugh xyzzy :: total count (2049)`;
// this specific layout is somewhat contrived to exercise the landing on
// an exact view boundary. it's not critical to test this edge case but
// since we are setting up a test in general for larger chunks I contrived it
// as such for now. I don't think it needs to be maintained if in the future
// the view sizes change or become dynamic becasue of the use of byobRequest
let stream;
stream = await ReactDOMFizzServer.renderToReadableStream(
<>
<div>
<span>{''}</span>
</div>
<div>{str492}</div>
<div>{str492}</div>
</>,
);
let result;
result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<div><span></span></div><div>${str492}</div><div>${str492}</div>"`,
);
// this size 2049 was chosen to be a couple base 2 orders larger than the current view
// size. if the size changes in the future hopefully this will still exercise
// a chunk that is too large for the view size.
stream = await ReactDOMFizzServer.renderToReadableStream(
<>
<div>{str2049}</div>
</>,
);
result = await readResult(stream);
expect(result).toMatchInlineSnapshot(`"<div>${str2049}</div>"`);
});
});