main
js 407 lines 12.4 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
9 */
10
11 'use strict';
12
13 let assertConsoleErrorDev;
14 let serverExports;
15 let webpackServerMap;
16 let ReactServerDOMServer;
17 let ReactServerDOMClient;
18
19 describe('ReactFlightDOMReplyEdge', () => {
20 beforeEach(() => {
21 jest.resetModules();
22 // Simulate the condition resolution
23 jest.mock('react', () => require('react/react.react-server'));
24 jest.mock('react-server-dom-webpack/server', () =>
25 require('react-server-dom-webpack/server.edge'),
26 );
27 const WebpackMock = require('./utils/WebpackMock');
28 serverExports = WebpackMock.serverExports;
29 webpackServerMap = WebpackMock.webpackServerMap;
30 ReactServerDOMServer = require('react-server-dom-webpack/server.edge');
31 jest.resetModules();
32 ReactServerDOMClient = require('react-server-dom-webpack/client.edge');
33
34 const InternalTestUtils = require('internal-test-utils');
35 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
36 });
37
38 it('can encode a reply', async () => {
39 const body = await ReactServerDOMClient.encodeReply({some: 'object'});
40 const decoded = await ReactServerDOMServer.decodeReply(
41 body,
42 webpackServerMap,
43 );
44
45 expect(decoded).toEqual({some: 'object'});
46 });
47
48 it('should be able to serialize any kind of typed array', async () => {
49 const buffer = new Uint8Array([
50 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
51 ]).buffer;
52 const buffers = [
53 buffer,
54 new Int8Array(buffer, 1),
55 new Uint8Array(buffer, 2),
56 new Uint8ClampedArray(buffer, 2),
57 new Int16Array(buffer, 2),
58 new Uint16Array(buffer, 2),
59 new Int32Array(buffer, 4),
60 new Uint32Array(buffer, 4),
61 new Float32Array(buffer, 4),
62 new Float64Array(buffer, 0),
63 new BigInt64Array(buffer, 0),
64 new BigUint64Array(buffer, 0),
65 new DataView(buffer, 3),
66 ];
67
68 const body = await ReactServerDOMClient.encodeReply(buffers);
69 const result = await ReactServerDOMServer.decodeReply(
70 body,
71 webpackServerMap,
72 );
73
74 expect(result).toEqual(buffers);
75 // Array buffers can't use the toEqual helper.
76 expect(new Uint8Array(result[0])).toEqual(new Uint8Array(buffers[0]));
77 });
78
79 it('should be able to serialize a typed array inside a Map', async () => {
80 const array = new Uint8Array([
81 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
82 ]);
83 const map = new Map();
84 map.set('array', array);
85
86 const body = await ReactServerDOMClient.encodeReply(map);
87 const result = await ReactServerDOMServer.decodeReply(
88 body,
89 webpackServerMap,
90 );
91
92 expect(result.get('array')).toEqual(array);
93 });
94
95 it('should be able to serialize a blob', async () => {
96 const bytes = new Uint8Array([
97 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
98 ]);
99 const blob = new Blob([bytes, bytes], {
100 type: 'application/x-test',
101 });
102 const body = await ReactServerDOMClient.encodeReply(blob);
103 const result = await ReactServerDOMServer.decodeReply(
104 body,
105 webpackServerMap,
106 );
107 expect(result instanceof Blob).toBe(true);
108 expect(result.size).toBe(bytes.length * 2);
109 expect(await result.arrayBuffer()).toEqual(await blob.arrayBuffer());
110 });
111
112 it('can transport FormData (blobs)', async () => {
113 const bytes = new Uint8Array([
114 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
115 ]);
116 const blob = new Blob([bytes, bytes], {
117 type: 'application/x-test',
118 });
119
120 const formData = new FormData();
121 formData.append('hi', 'world');
122 formData.append('file', blob, 'filename.test');
123
124 expect(formData.get('file') instanceof File).toBe(true);
125 expect(formData.get('file').name).toBe('filename.test');
126
127 const body = await ReactServerDOMClient.encodeReply(formData);
128 const result = await ReactServerDOMServer.decodeReply(
129 body,
130 webpackServerMap,
131 );
132
133 expect(result instanceof FormData).toBe(true);
134 expect(result.get('hi')).toBe('world');
135 const resultBlob = result.get('file');
136 expect(resultBlob instanceof Blob).toBe(true);
137 expect(resultBlob.name).toBe('filename.test'); // In this direction we allow file name to pass through but not other direction.
138 expect(resultBlob.size).toBe(bytes.length * 2);
139 expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
140 });
141
142 it('should support ReadableStreams with typed arrays', async () => {
143 const buffer = new Uint8Array([
144 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
145 ]).buffer;
146 const buffers = [
147 buffer,
148 new Int8Array(buffer, 1),
149 new Uint8Array(buffer, 2),
150 new Uint8ClampedArray(buffer, 2),
151 new Int16Array(buffer, 2),
152 new Uint16Array(buffer, 2),
153 new Int32Array(buffer, 4),
154 new Uint32Array(buffer, 4),
155 new Float32Array(buffer, 4),
156 new Float64Array(buffer, 0),
157 new BigInt64Array(buffer, 0),
158 new BigUint64Array(buffer, 0),
159 new DataView(buffer, 3),
160 ];
161
162 // This is not a binary stream, it's a stream that contain binary chunks.
163 const s = new ReadableStream({
164 start(c) {
165 for (let i = 0; i < buffers.length; i++) {
166 c.enqueue(buffers[i]);
167 }
168 c.close();
169 },
170 });
171
172 const body = await ReactServerDOMClient.encodeReply(s);
173 const result = await ReactServerDOMServer.decodeReply(
174 body,
175 webpackServerMap,
176 );
177
178 const streamedBuffers = [];
179 const reader = result.getReader();
180 let entry;
181 while (!(entry = await reader.read()).done) {
182 streamedBuffers.push(entry.value);
183 }
184
185 expect(streamedBuffers).toEqual(buffers);
186 });
187
188 it('should support BYOB binary ReadableStreams', async () => {
189 const sourceBytes = [
190 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
191 ];
192
193 // Create separate buffers for each typed array to avoid ArrayBuffer
194 // transfer issues. Each view needs its own buffer because enqueue()
195 // transfers ownership.
196 const buffers = [
197 new Int8Array(sourceBytes.slice(1)),
198 new Uint8Array(sourceBytes.slice(2)),
199 new Uint8ClampedArray(sourceBytes.slice(2)),
200 new Int16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
201 new Uint16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
202 new Int32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
203 new Uint32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
204 new Float32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
205 new Float64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
206 new BigInt64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
207 new BigUint64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
208 new DataView(new Uint8Array(sourceBytes.slice(3)).buffer),
209 ];
210
211 // Save expected bytes before enqueueing (which will detach the buffers).
212 const expectedBytes = buffers.flatMap(c =>
213 Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
214 );
215
216 // This a binary stream where each chunk ends up as Uint8Array.
217 const s = new ReadableStream({
218 type: 'bytes',
219 start(c) {
220 for (let i = 0; i < buffers.length; i++) {
221 c.enqueue(buffers[i]);
222 }
223 c.close();
224 },
225 });
226
227 const body = await ReactServerDOMClient.encodeReply(s);
228 const result = await ReactServerDOMServer.decodeReply(
229 body,
230 webpackServerMap,
231 );
232
233 const streamedBuffers = [];
234 const reader = result.getReader({mode: 'byob'});
235 let entry;
236 while (!(entry = await reader.read(new Uint8Array(10))).done) {
237 expect(entry.value instanceof Uint8Array).toBe(true);
238 streamedBuffers.push(entry.value);
239 }
240
241 // The streamed buffers might be in different chunks and in Uint8Array form but
242 // the concatenated bytes should be the same.
243 expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(expectedBytes);
244 });
245
246 it('should cancel the transported ReadableStream when we are cancelled', async () => {
247 const s = new ReadableStream({
248 start(controller) {
249 controller.enqueue('hi');
250 controller.close();
251 },
252 });
253
254 const body = await ReactServerDOMClient.encodeReply(s);
255
256 const iterable = {
257 async *[Symbol.asyncIterator]() {
258 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
259 for (const entry of body) {
260 if (entry[1] === 'C') {
261 // Return before finishing the stream.
262 return;
263 }
264 yield entry;
265 }
266 },
267 };
268
269 const result = await ReactServerDOMServer.decodeReplyFromAsyncIterable(
270 iterable,
271 webpackServerMap,
272 );
273
274 const reader = result.getReader();
275
276 // We should be able to read the part we already emitted before the abort
277 expect(await reader.read()).toEqual({
278 value: 'hi',
279 done: false,
280 });
281
282 let error = null;
283 try {
284 await reader.read();
285 } catch (x) {
286 error = x;
287 }
288
289 expect(error).not.toBe(null);
290 expect(error.message).toBe('Connection closed.');
291 });
292
293 it('should abort when parsing an incomplete payload', async () => {
294 const infinitePromise = new Promise(() => {});
295 const controller = new AbortController();
296 const promiseForResult = ReactServerDOMClient.encodeReply(
297 {promise: infinitePromise},
298 {
299 signal: controller.signal,
300 },
301 );
302 controller.abort();
303 const body = await promiseForResult;
304
305 const decoded = await ReactServerDOMServer.decodeReply(
306 body,
307 webpackServerMap,
308 );
309
310 let error = null;
311 try {
312 await decoded.promise;
313 } catch (x) {
314 error = x;
315 }
316 expect(error).not.toBe(null);
317 expect(error.message).toBe('Connection closed.');
318 });
319
320 it('can stream the decoding using an async iterable', async () => {
321 let resolve;
322 const promise = new Promise(r => (resolve = r));
323
324 const buffer = new Uint8Array([
325 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
326 ]);
327
328 const formData = await ReactServerDOMClient.encodeReply({
329 a: Promise.resolve('hello'),
330 b: Promise.resolve(buffer),
331 });
332
333 const iterable = {
334 async *[Symbol.asyncIterator]() {
335 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
336 for (const entry of formData) {
337 yield entry;
338 await promise;
339 }
340 },
341 };
342
343 const decoded = await ReactServerDOMServer.decodeReplyFromAsyncIterable(
344 iterable,
345 webpackServerMap,
346 );
347
348 expect(Object.keys(decoded)).toEqual(['a', 'b']);
349
350 await resolve();
351
352 expect(await decoded.a).toBe('hello');
353 expect(Array.from(await decoded.b)).toEqual(Array.from(buffer));
354 });
355
356 it('can pass a registered server reference', async () => {
357 function greet(name) {
358 return 'hi, ' + name;
359 }
360 const ServerModule = serverExports({
361 greet,
362 });
363
364 ReactServerDOMClient.registerServerReference(
365 ServerModule.greet,
366 ServerModule.greet.$$id,
367 );
368
369 const body = await ReactServerDOMClient.encodeReply({
370 method: ServerModule.greet,
371 boundMethod: ServerModule.greet.bind(null, 'there'),
372 });
373 const replyResult = await ReactServerDOMServer.decodeReply(
374 body,
375 webpackServerMap,
376 );
377 expect(replyResult.method).toBe(greet);
378 expect(replyResult.boundMethod()).toBe('hi, there');
379 });
380
381 it('warns with a tailored message if eval is not available in dev', async () => {
382 // eslint-disable-next-line no-eval
383 const previousEval = globalThis.eval.bind(globalThis);
384 // eslint-disable-next-line no-eval
385 globalThis.eval = () => {
386 throw new Error('eval is disabled');
387 };
388
389 try {
390 const body = await ReactServerDOMClient.encodeReply({some: 'object'});
391
392 assertConsoleErrorDev([
393 'eval() is not supported in this environment. ' +
394 'React requires eval() in development mode for various debugging features ' +
395 'like reconstructing callstacks from a different environment.\n' +
396 'React will never use eval() in production mode',
397 ]);
398
399 await ReactServerDOMServer.decodeReply(body, webpackServerMap);
400
401 assertConsoleErrorDev([]);
402 } finally {
403 // eslint-disable-next-line no-eval
404 globalThis.eval = previousEval;
405 }
406 });
407 });