-
Notifications
You must be signed in to change notification settings - Fork 47.5k
/
Copy pathReactFlightDOMReply-test.js
621 lines (543 loc) · 18.4 KB
/
ReactFlightDOMReply-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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
/**
* Copyright (c) Meta Platforms, Inc. and 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';
import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
// Polyfills for test environment
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
// let serverExports;
let webpackServerMap;
let React;
let ReactServerDOMServer;
let ReactServerDOMClient;
let ReactServerScheduler;
let reactServerAct;
describe('ReactFlightDOMReply', () => {
beforeEach(() => {
jest.resetModules();
ReactServerScheduler = require('scheduler');
patchMessageChannel(ReactServerScheduler);
reactServerAct = require('internal-test-utils').act;
// Simulate the condition resolution
jest.mock('react', () => require('react/react.react-server'));
jest.mock('react-server-dom-webpack/server', () =>
require('react-server-dom-webpack/server.browser'),
);
const WebpackMock = require('./utils/WebpackMock');
// serverExports = WebpackMock.serverExports;
webpackServerMap = WebpackMock.webpackServerMap;
React = require('react');
ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
jest.resetModules();
__unmockReact();
ReactServerDOMClient = require('react-server-dom-webpack/client');
});
async function serverAct(callback) {
let maybePromise;
await reactServerAct(() => {
maybePromise = callback();
if (maybePromise && typeof maybePromise.catch === 'function') {
maybePromise.catch(() => {});
}
});
return maybePromise;
}
// This method should exist on File but is not implemented in JSDOM
async function arrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function () {
return resolve(reader.result);
};
reader.onerror = function () {
return reject(reader.error);
};
reader.readAsArrayBuffer(file);
});
}
it('can pass undefined as a reply', async () => {
const body = await ReactServerDOMClient.encodeReply(undefined);
const missing = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
expect(missing).toBe(undefined);
const body2 = await ReactServerDOMClient.encodeReply({
array: [undefined, null, undefined],
prop: undefined,
});
const object = await ReactServerDOMServer.decodeReply(
body2,
webpackServerMap,
);
expect(object.array.length).toBe(3);
expect(object.array[0]).toBe(undefined);
expect(object.array[1]).toBe(null);
expect(object.array[3]).toBe(undefined);
expect(object.prop).toBe(undefined);
// These should really be true but our deserialization doesn't currently deal with it.
expect('3' in object.array).toBe(false);
expect('prop' in object).toBe(false);
});
it('can pass an iterable as a reply', async () => {
const body = await ReactServerDOMClient.encodeReply({
[Symbol.iterator]: function* () {
yield 'A';
yield 'B';
yield 'C';
},
});
const iterable = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
const items = [];
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const item of iterable) {
items.push(item);
}
expect(items).toEqual(['A', 'B', 'C']);
// Multipass
const items2 = [];
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const item of iterable) {
items2.push(item);
}
expect(items2).toEqual(['A', 'B', 'C']);
});
it('can pass an iterator as a reply', async () => {
const iterator = (function* () {
yield 'A';
yield 'B';
yield 'C';
})();
const body = await ReactServerDOMClient.encodeReply(iterator);
const result = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
// The iterator should be the same as itself.
expect(result[Symbol.iterator]()).toBe(result);
expect(Array.from(result)).toEqual(['A', 'B', 'C']);
// We've already consumed this iterator.
expect(Array.from(result)).toEqual([]);
});
it('can pass weird numbers as a reply', async () => {
const nums = [0, -0, Infinity, -Infinity, NaN];
const body = await ReactServerDOMClient.encodeReply(nums);
const nums2 = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
expect(nums).toEqual(nums2);
expect(nums.every((n, i) => Object.is(n, nums2[i]))).toBe(true);
});
it('can pass a BigInt as a reply', async () => {
const body = await ReactServerDOMClient.encodeReply(90071992547409910000n);
const n = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
expect(n).toEqual(90071992547409910000n);
});
it('can pass FormData as a reply', async () => {
const formData = new FormData();
formData.set('hello', 'world');
formData.append('list', '1');
formData.append('list', '2');
formData.append('list', '3');
const typedArray = new Uint8Array([0, 1, 2, 3]);
const blob = new Blob([typedArray]);
formData.append('blob', blob, 'filename.blob');
const body = await ReactServerDOMClient.encodeReply(formData);
const formData2 = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
expect(formData2).not.toBe(formData);
expect(Array.from(formData2).length).toBe(5);
expect(formData2.get('hello')).toBe('world');
expect(formData2.getAll('list')).toEqual(['1', '2', '3']);
const blob2 = formData.get('blob');
expect(blob2.size).toBe(4);
expect(blob2.name).toBe('filename.blob');
expect(blob2.type).toBe('');
const typedArray2 = new Uint8Array(await arrayBuffer(blob2));
expect(typedArray2).toEqual(typedArray);
});
it('can pass multiple Files in FormData', async () => {
const typedArrayA = new Uint8Array([0, 1, 2, 3]);
const typedArrayB = new Uint8Array([4, 5]);
const blobA = new Blob([typedArrayA]);
const blobB = new Blob([typedArrayB]);
const formData = new FormData();
formData.append('filelist', 'string');
formData.append('filelist', blobA);
formData.append('filelist', blobB);
const body = await ReactServerDOMClient.encodeReply(formData);
const formData2 = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
const filelist2 = formData2.getAll('filelist');
expect(filelist2.length).toBe(3);
expect(filelist2[0]).toBe('string');
const blobA2 = filelist2[1];
expect(blobA2.size).toBe(4);
expect(blobA2.name).toBe('blob');
expect(blobA2.type).toBe('');
const typedArrayA2 = new Uint8Array(await arrayBuffer(blobA2));
expect(typedArrayA2).toEqual(typedArrayA);
const blobB2 = filelist2[2];
expect(blobB2.size).toBe(2);
expect(blobB2.name).toBe('blob');
expect(blobB2.type).toBe('');
const typedArrayB2 = new Uint8Array(await arrayBuffer(blobB2));
expect(typedArrayB2).toEqual(typedArrayB);
});
it('can pass two independent FormData with same keys', async () => {
const formDataA = new FormData();
formDataA.set('greeting', 'hello');
const formDataB = new FormData();
formDataB.set('greeting', 'hi');
const body = await ReactServerDOMClient.encodeReply({
a: formDataA,
b: formDataB,
});
const {a: formDataA2, b: formDataB2} =
await ReactServerDOMServer.decodeReply(body, webpackServerMap);
expect(Array.from(formDataA2).length).toBe(1);
expect(Array.from(formDataB2).length).toBe(1);
expect(formDataA2.get('greeting')).toBe('hello');
expect(formDataB2.get('greeting')).toBe('hi');
});
it('can pass a Date as a reply', async () => {
const d = new Date(1234567890123);
const body = await ReactServerDOMClient.encodeReply(d);
const d2 = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
expect(d).toEqual(d2);
expect(d % 1000).toEqual(123); // double-check the milliseconds made it through
});
it('can pass a Map as a reply', async () => {
const objKey = {obj: 'key'};
const m = new Map([
['hi', {greet: 'world'}],
[objKey, 123],
]);
const body = await ReactServerDOMClient.encodeReply(m);
const m2 = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
expect(m2 instanceof Map).toBe(true);
expect(m2.size).toBe(2);
expect(m2.get('hi').greet).toBe('world');
expect(m2).toEqual(m);
});
it('can pass a Set as a reply', async () => {
const objKey = {obj: 'key'};
const s = new Set(['hi', objKey]);
const body = await ReactServerDOMClient.encodeReply(s);
const s2 = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
expect(s2 instanceof Set).toBe(true);
expect(s2.size).toBe(2);
expect(s2.has('hi')).toBe(true);
expect(s2).toEqual(s);
});
it('does not hang indefinitely when calling decodeReply with FormData', async () => {
let error;
try {
await ReactServerDOMServer.decodeReply(new FormData(), webpackServerMap);
} catch (e) {
error = e;
}
expect(error.message).toBe('Connection closed.');
});
it('resolves a promise and includes its value', async () => {
let resolve;
const promise = new Promise(r => (resolve = r));
const bodyPromise = ReactServerDOMClient.encodeReply({promise: promise});
resolve('Hi');
const result = await ReactServerDOMServer.decodeReply(await bodyPromise);
expect(await result.promise).toBe('Hi');
});
it('resolves a React.lazy and includes its value', async () => {
let resolve;
const lazy = React.lazy(() => new Promise(r => (resolve = r)));
const bodyPromise = ReactServerDOMClient.encodeReply({lazy: lazy});
resolve({default: 'Hi'});
const result = await ReactServerDOMServer.decodeReply(await bodyPromise);
expect(result.lazy).toBe('Hi');
});
it('resolves a proxy throwing a promise inside React.lazy', async () => {
let resolve1;
let resolve2;
const lazy = React.lazy(() => new Promise(r => (resolve1 = r)));
const promise = new Promise(r => (resolve2 = r));
const bodyPromise1 = ReactServerDOMClient.encodeReply({lazy: lazy});
const target = {value: ''};
let loaded = false;
const proxy = new Proxy(target, {
get(targetObj, prop, receiver) {
if (prop === 'value') {
if (!loaded) {
throw promise;
}
return 'Hello';
}
return targetObj[prop];
},
});
await resolve1({default: proxy});
// Encode it again so that we have an already initialized lazy
// This is now already resolved but the proxy inside isn't. This ensures
// we trigger the retry code path.
const bodyPromise2 = ReactServerDOMClient.encodeReply({lazy: lazy});
// Then resolve the inner thrown promise.
loaded = true;
await resolve2('Hello');
const result1 = await ReactServerDOMServer.decodeReply(await bodyPromise1);
expect(await result1.lazy.value).toBe('Hello');
const result2 = await ReactServerDOMServer.decodeReply(await bodyPromise2);
expect(await result2.lazy.value).toBe('Hello');
});
it('errors when called with JSX by default', async () => {
let error;
try {
await ReactServerDOMClient.encodeReply(<div />);
} catch (x) {
error = x;
}
expect(error).toEqual(
expect.objectContaining({
message: __DEV__
? expect.stringContaining(
'React Element cannot be passed to Server Functions from the Client without a temporary reference set.',
)
: expect.stringContaining(''),
}),
);
});
it('can pass JSX through a round trip using temporary references', async () => {
function Component() {
return <div />;
}
const children = <Component />;
const temporaryReferences =
ReactServerDOMClient.createTemporaryReferenceSet();
const body = await ReactServerDOMClient.encodeReply(
{children},
{
temporaryReferences,
},
);
const temporaryReferencesServer =
ReactServerDOMServer.createTemporaryReferenceSet();
const serverPayload = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
{temporaryReferences: temporaryReferencesServer},
);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(serverPayload, null, {
temporaryReferences: temporaryReferencesServer,
}),
);
const response = await ReactServerDOMClient.createFromReadableStream(
stream,
{
temporaryReferences,
},
);
// This should've been the same reference that we already saw.
expect(response.children).toBe(children);
});
it('can return the same object using temporary references', async () => {
const obj = {
this: {is: 'a large object'},
with: {many: 'properties in it'},
};
const root = {obj};
const temporaryReferences =
ReactServerDOMClient.createTemporaryReferenceSet();
const body = await ReactServerDOMClient.encodeReply(root, {
temporaryReferences,
});
const temporaryReferencesServer =
ReactServerDOMServer.createTemporaryReferenceSet();
const serverPayload = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
{temporaryReferences: temporaryReferencesServer},
);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{
root: serverPayload,
obj: serverPayload.obj,
},
null,
{temporaryReferences: temporaryReferencesServer},
),
);
const response = await ReactServerDOMClient.createFromReadableStream(
stream,
{
temporaryReferences,
},
);
// This should've been the same reference that we already saw because
// we returned it by reference.
expect(response.root).toBe(root);
expect(response.obj).toBe(obj);
});
// @gate enableFlightReadableStream
it('should supports streaming ReadableStream with objects', async () => {
let controller1;
let controller2;
const s1 = new ReadableStream({
start(c) {
controller1 = c;
},
});
const s2 = new ReadableStream({
start(c) {
controller2 = c;
},
});
const promise = ReactServerDOMClient.encodeReply({s1, s2});
controller1.enqueue({hello: 'world'});
controller2.enqueue({hi: 'there'});
controller1.enqueue('text1');
controller2.enqueue('text2');
controller1.close();
controller2.close();
const body = await promise;
const result = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
const reader1 = result.s1.getReader();
const reader2 = result.s2.getReader();
expect(await reader1.read()).toEqual({
value: {hello: 'world'},
done: false,
});
expect(await reader2.read()).toEqual({
value: {hi: 'there'},
done: false,
});
expect(await reader1.read()).toEqual({
value: 'text1',
done: false,
});
expect(await reader1.read()).toEqual({
value: undefined,
done: true,
});
expect(await reader2.read()).toEqual({
value: 'text2',
done: false,
});
expect(await reader2.read()).toEqual({
value: undefined,
done: true,
});
});
// @gate enableFlightReadableStream
it('should supports streaming AsyncIterables with objects', async () => {
let resolve;
const wait = new Promise(r => (resolve = r));
const multiShotIterable = {
async *[Symbol.asyncIterator]() {
const next = yield {hello: 'A'};
expect(next).toBe(undefined);
await wait;
yield {hi: 'B'};
return 'C';
},
};
const singleShotIterator = (async function* () {
const next = yield {hello: 'D'};
expect(next).toBe(undefined);
await wait;
yield {hi: 'E'};
return 'F';
})();
await resolve();
const body = await ReactServerDOMClient.encodeReply({
multiShotIterable,
singleShotIterator,
});
const result = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
const iterator1 = result.multiShotIterable[Symbol.asyncIterator]();
const iterator2 = result.singleShotIterator[Symbol.asyncIterator]();
expect(iterator1).not.toBe(result.multiShotIterable);
expect(iterator2).toBe(result.singleShotIterator);
expect(await iterator1.next()).toEqual({
value: {hello: 'A'},
done: false,
});
expect(await iterator2.next()).toEqual({
value: {hello: 'D'},
done: false,
});
expect(await iterator1.next()).toEqual({
value: {hi: 'B'},
done: false,
});
expect(await iterator2.next()).toEqual({
value: {hi: 'E'},
done: false,
});
expect(await iterator1.next()).toEqual({
value: 'C', // Return value
done: true,
});
expect(await iterator1.next()).toEqual({
value: undefined,
done: true,
});
expect(await iterator2.next()).toEqual({
value: 'F', // Return value
done: true,
});
// Multi-shot iterables should be able to do the same thing again
const iterator3 = result.multiShotIterable[Symbol.asyncIterator]();
expect(iterator3).not.toBe(iterator1);
// We should be able to iterate over the iterable again and it should be
// synchronously available using instrumented promises so that React can
// rerender it synchronously.
expect(iterator3.next().value).toEqual({
value: {hello: 'A'},
done: false,
});
expect(iterator3.next().value).toEqual({
value: {hi: 'B'},
done: false,
});
expect(iterator3.next().value).toEqual({
value: 'C', // Return value
done: true,
});
expect(iterator3.next().value).toEqual({
value: undefined,
done: true,
});
expect(() => iterator3.next('this is not allowed')).toThrow(
'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
);
});
it('can transport cyclic objects', async () => {
const cyclic = {obj: null};
cyclic.obj = cyclic;
const body = await ReactServerDOMClient.encodeReply({prop: cyclic});
const root = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
expect(root.prop.obj).toBe(root.prop);
});
});