mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[Flight] Cleanup turbopack tests (#27552)
Renames turbopack flight test files. removes tests which are primarily testing internal implemenation behavior of Flight Client itself
This commit is contained in:
File diff suppressed because it is too large
Load Diff
-256
@@ -1,256 +0,0 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// Don't wait before processing work on the server.
|
||||
// TODO: we can replace this with FlightServer.act().
|
||||
global.setImmediate = cb => cb();
|
||||
|
||||
let clientExports;
|
||||
let turbopackMap;
|
||||
let turbopackModules;
|
||||
let turbopackModuleLoading;
|
||||
let React;
|
||||
let ReactDOMServer;
|
||||
let ReactServerDOMServer;
|
||||
let ReactServerDOMClient;
|
||||
let Stream;
|
||||
let use;
|
||||
|
||||
describe('ReactFlightDOMNode', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
// Simulate the condition resolution
|
||||
jest.mock('react', () => require('react/react.shared-subset'));
|
||||
jest.mock('react-server-dom-turbopack/server', () =>
|
||||
require('react-server-dom-turbopack/server.node'),
|
||||
);
|
||||
ReactServerDOMServer = require('react-server-dom-turbopack/server');
|
||||
|
||||
const TurbopackMock = require('./utils/TurbopackMock');
|
||||
clientExports = TurbopackMock.clientExports;
|
||||
turbopackMap = TurbopackMock.turbopackMap;
|
||||
turbopackModules = TurbopackMock.turbopackModules;
|
||||
turbopackModuleLoading = TurbopackMock.moduleLoading;
|
||||
|
||||
jest.resetModules();
|
||||
__unmockReact();
|
||||
jest.unmock('react-server-dom-turbopack/server');
|
||||
jest.mock('react-server-dom-turbopack/client', () =>
|
||||
require('react-server-dom-turbopack/client.node'),
|
||||
);
|
||||
|
||||
React = require('react');
|
||||
ReactDOMServer = require('react-dom/server.node');
|
||||
ReactServerDOMClient = require('react-server-dom-turbopack/client');
|
||||
Stream = require('stream');
|
||||
use = React.use;
|
||||
});
|
||||
|
||||
function readResult(stream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = '';
|
||||
const writable = new Stream.PassThrough();
|
||||
writable.setEncoding('utf8');
|
||||
writable.on('data', chunk => {
|
||||
buffer += chunk;
|
||||
});
|
||||
writable.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
writable.on('end', () => {
|
||||
resolve(buffer);
|
||||
});
|
||||
stream.pipe(writable);
|
||||
});
|
||||
}
|
||||
|
||||
it('should allow an alternative module mapping to be used for SSR', async () => {
|
||||
function ClientComponent() {
|
||||
return <span>Client Component</span>;
|
||||
}
|
||||
// The Client build may not have the same IDs as the Server bundles for the same
|
||||
// component.
|
||||
const ClientComponentOnTheClient = clientExports(
|
||||
ClientComponent,
|
||||
'path/to/chunk.js',
|
||||
);
|
||||
const ClientComponentOnTheServer = clientExports(ClientComponent);
|
||||
|
||||
// In the SSR bundle this module won't exist. We simulate this by deleting it.
|
||||
const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
|
||||
delete turbopackModules[clientId];
|
||||
|
||||
// Instead, we have to provide a translation from the client meta data to the SSR
|
||||
// meta data.
|
||||
const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
|
||||
const translationMap = {
|
||||
[clientId]: {
|
||||
'*': ssrMetadata,
|
||||
},
|
||||
};
|
||||
|
||||
function App() {
|
||||
return <ClientComponentOnTheClient />;
|
||||
}
|
||||
|
||||
const stream = ReactServerDOMServer.renderToPipeableStream(
|
||||
<App />,
|
||||
turbopackMap,
|
||||
);
|
||||
const readable = new Stream.PassThrough();
|
||||
|
||||
stream.pipe(readable);
|
||||
|
||||
let response;
|
||||
function ClientRoot() {
|
||||
if (!response) {
|
||||
response = ReactServerDOMClient.createFromNodeStream(readable, {
|
||||
moduleMap: translationMap,
|
||||
moduleLoading: turbopackModuleLoading,
|
||||
});
|
||||
}
|
||||
return use(response);
|
||||
}
|
||||
|
||||
const ssrStream = await ReactDOMServer.renderToPipeableStream(
|
||||
<ClientRoot />,
|
||||
);
|
||||
const result = await readResult(ssrStream);
|
||||
expect(result).toEqual(
|
||||
'<script src="/prefix/path/to/chunk.js" async=""></script><span>Client Component</span>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode long string in a compact format', async () => {
|
||||
const testString = '"\n\t'.repeat(500) + '🙃';
|
||||
|
||||
const stream = ReactServerDOMServer.renderToPipeableStream({
|
||||
text: testString,
|
||||
});
|
||||
|
||||
const readable = new Stream.PassThrough();
|
||||
|
||||
const stringResult = readResult(readable);
|
||||
const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, {
|
||||
moduleMap: turbopackMap,
|
||||
moduleLoading: turbopackModuleLoading,
|
||||
});
|
||||
|
||||
stream.pipe(readable);
|
||||
|
||||
const serializedContent = await stringResult;
|
||||
// The content should be compact an unescaped
|
||||
expect(serializedContent.length).toBeLessThan(2000);
|
||||
expect(serializedContent).not.toContain('\\n');
|
||||
expect(serializedContent).not.toContain('\\t');
|
||||
expect(serializedContent).not.toContain('\\"');
|
||||
expect(serializedContent).toContain('\t');
|
||||
|
||||
const result = await parsedResult;
|
||||
// Should still match the result when parsed
|
||||
expect(result.text).toBe(testString);
|
||||
});
|
||||
|
||||
// @gate enableBinaryFlight
|
||||
it('should be able to serialize any kind of typed array', async () => {
|
||||
const buffer = new Uint8Array([
|
||||
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
|
||||
]).buffer;
|
||||
const buffers = [
|
||||
buffer,
|
||||
new Int8Array(buffer, 1),
|
||||
new Uint8Array(buffer, 2),
|
||||
new Uint8ClampedArray(buffer, 2),
|
||||
new Int16Array(buffer, 2),
|
||||
new Uint16Array(buffer, 2),
|
||||
new Int32Array(buffer, 4),
|
||||
new Uint32Array(buffer, 4),
|
||||
new Float32Array(buffer, 4),
|
||||
new Float64Array(buffer, 0),
|
||||
new BigInt64Array(buffer, 0),
|
||||
new BigUint64Array(buffer, 0),
|
||||
new DataView(buffer, 3),
|
||||
];
|
||||
const stream = ReactServerDOMServer.renderToPipeableStream(buffers);
|
||||
const readable = new Stream.PassThrough();
|
||||
const promise = ReactServerDOMClient.createFromNodeStream(readable, {
|
||||
moduleMap: turbopackMap,
|
||||
moduleLoading: turbopackModuleLoading,
|
||||
});
|
||||
stream.pipe(readable);
|
||||
const result = await promise;
|
||||
expect(result).toEqual(buffers);
|
||||
});
|
||||
|
||||
it('should allow accept a nonce option for Flight preinitialized scripts', async () => {
|
||||
function ClientComponent() {
|
||||
return <span>Client Component</span>;
|
||||
}
|
||||
// The Client build may not have the same IDs as the Server bundles for the same
|
||||
// component.
|
||||
const ClientComponentOnTheClient = clientExports(
|
||||
ClientComponent,
|
||||
'path/to/chunk.js',
|
||||
);
|
||||
const ClientComponentOnTheServer = clientExports(ClientComponent);
|
||||
|
||||
// In the SSR bundle this module won't exist. We simulate this by deleting it.
|
||||
const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
|
||||
delete turbopackModules[clientId];
|
||||
|
||||
// Instead, we have to provide a translation from the client meta data to the SSR
|
||||
// meta data.
|
||||
const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
|
||||
const translationMap = {
|
||||
[clientId]: {
|
||||
'*': ssrMetadata,
|
||||
},
|
||||
};
|
||||
const ssrManifest = {
|
||||
moduleMap: translationMap,
|
||||
moduleLoading: turbopackModuleLoading,
|
||||
};
|
||||
|
||||
function App() {
|
||||
return <ClientComponentOnTheClient />;
|
||||
}
|
||||
|
||||
const stream = ReactServerDOMServer.renderToPipeableStream(
|
||||
<App />,
|
||||
turbopackMap,
|
||||
);
|
||||
const readable = new Stream.PassThrough();
|
||||
let response;
|
||||
|
||||
stream.pipe(readable);
|
||||
|
||||
function ClientRoot() {
|
||||
if (response) return use(response);
|
||||
response = ReactServerDOMClient.createFromNodeStream(
|
||||
readable,
|
||||
ssrManifest,
|
||||
{
|
||||
nonce: 'r4nd0m',
|
||||
},
|
||||
);
|
||||
return use(response);
|
||||
}
|
||||
|
||||
const ssrStream = await ReactDOMServer.renderToPipeableStream(
|
||||
<ClientRoot />,
|
||||
);
|
||||
const result = await readResult(ssrStream);
|
||||
expect(result).toEqual(
|
||||
'<script src="/prefix/path/to/chunk.js" async="" nonce="r4nd0m"></script><span>Client Component</span>',
|
||||
);
|
||||
});
|
||||
});
|
||||
-234
@@ -1,234 +0,0 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// 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 turbopackServerMap;
|
||||
let ReactServerDOMServer;
|
||||
let ReactServerDOMClient;
|
||||
|
||||
describe('ReactFlightDOMReply', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
// Simulate the condition resolution
|
||||
jest.mock('react', () => require('react/react.shared-subset'));
|
||||
jest.mock('react-server-dom-turbopack/server', () =>
|
||||
require('react-server-dom-turbopack/server.browser'),
|
||||
);
|
||||
const TurbopackMock = require('./utils/TurbopackMock');
|
||||
// serverExports = TurbopackMock.serverExports;
|
||||
turbopackServerMap = TurbopackMock.turbopackServerMap;
|
||||
ReactServerDOMServer = require('react-server-dom-turbopack/server.browser');
|
||||
jest.resetModules();
|
||||
ReactServerDOMClient = require('react-server-dom-turbopack/client');
|
||||
});
|
||||
|
||||
// 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,
|
||||
turbopackServerMap,
|
||||
);
|
||||
expect(missing).toBe(undefined);
|
||||
|
||||
const body2 = await ReactServerDOMClient.encodeReply({
|
||||
array: [undefined, null, undefined],
|
||||
prop: undefined,
|
||||
});
|
||||
const object = await ReactServerDOMServer.decodeReply(
|
||||
body2,
|
||||
turbopackServerMap,
|
||||
);
|
||||
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,
|
||||
turbopackServerMap,
|
||||
);
|
||||
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']);
|
||||
});
|
||||
|
||||
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,
|
||||
turbopackServerMap,
|
||||
);
|
||||
|
||||
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, turbopackServerMap);
|
||||
|
||||
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,
|
||||
turbopackServerMap,
|
||||
);
|
||||
|
||||
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,
|
||||
turbopackServerMap,
|
||||
);
|
||||
|
||||
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, turbopackServerMap);
|
||||
|
||||
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, turbopackServerMap);
|
||||
|
||||
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, turbopackServerMap);
|
||||
|
||||
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, turbopackServerMap);
|
||||
|
||||
expect(s2 instanceof Set).toBe(true);
|
||||
expect(s2.size).toBe(2);
|
||||
expect(s2.has('hi')).toBe(true);
|
||||
expect(s2).toEqual(s);
|
||||
});
|
||||
});
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
global.TextDecoder = require('util').TextDecoder;
|
||||
|
||||
// Don't wait before processing work on the server.
|
||||
// TODO: we can replace this with FlightServer.act().
|
||||
global.setImmediate = cb => cb();
|
||||
|
||||
let act;
|
||||
let use;
|
||||
let clientExports;
|
||||
let turbopackMap;
|
||||
let Stream;
|
||||
let React;
|
||||
let ReactDOMClient;
|
||||
let ReactServerDOMServer;
|
||||
let ReactServerDOMClient;
|
||||
let Suspense;
|
||||
|
||||
describe('ReactFlightDOM', () => {
|
||||
beforeEach(() => {
|
||||
// For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server
|
||||
// This can be thought of as essentially being the React Server Components scope with react-server
|
||||
// condition
|
||||
jest.resetModules();
|
||||
|
||||
// Simulate the condition resolution
|
||||
jest.mock('react-server-dom-turbopack/server', () =>
|
||||
require('react-server-dom-turbopack/server.node.unbundled'),
|
||||
);
|
||||
jest.mock('react', () => require('react/react.shared-subset'));
|
||||
|
||||
const TurbopackMock = require('./utils/TurbopackMock');
|
||||
clientExports = TurbopackMock.clientExports;
|
||||
turbopackMap = TurbopackMock.turbopackMap;
|
||||
|
||||
ReactServerDOMServer = require('react-server-dom-turbopack/server');
|
||||
|
||||
// This reset is to load modules for the SSR/Browser scope.
|
||||
jest.resetModules();
|
||||
__unmockReact();
|
||||
act = require('internal-test-utils').act;
|
||||
Stream = require('stream');
|
||||
React = require('react');
|
||||
use = React.use;
|
||||
Suspense = React.Suspense;
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
ReactServerDOMClient = require('react-server-dom-turbopack/client');
|
||||
});
|
||||
|
||||
function getTestStream() {
|
||||
const writable = new Stream.PassThrough();
|
||||
const readable = new ReadableStream({
|
||||
start(controller) {
|
||||
writable.on('data', chunk => {
|
||||
controller.enqueue(chunk);
|
||||
});
|
||||
writable.on('end', () => {
|
||||
controller.close();
|
||||
});
|
||||
},
|
||||
});
|
||||
return {
|
||||
readable,
|
||||
writable,
|
||||
};
|
||||
}
|
||||
|
||||
it('should resolve HTML using Node streams', async () => {
|
||||
function Text({children}) {
|
||||
return <span>{children}</span>;
|
||||
}
|
||||
function HTML() {
|
||||
return (
|
||||
<div>
|
||||
<Text>hello</Text>
|
||||
<Text>world</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const model = {
|
||||
html: <HTML />,
|
||||
};
|
||||
return model;
|
||||
}
|
||||
|
||||
const {writable, readable} = getTestStream();
|
||||
const {pipe} = ReactServerDOMServer.renderToPipeableStream(
|
||||
<App />,
|
||||
turbopackMap,
|
||||
);
|
||||
pipe(writable);
|
||||
const response = ReactServerDOMClient.createFromReadableStream(readable);
|
||||
const model = await response;
|
||||
expect(model).toEqual({
|
||||
html: (
|
||||
<div>
|
||||
<span>hello</span>
|
||||
<span>world</span>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve the root', async () => {
|
||||
// Model
|
||||
function Text({children}) {
|
||||
return <span>{children}</span>;
|
||||
}
|
||||
function HTML() {
|
||||
return (
|
||||
<div>
|
||||
<Text>hello</Text>
|
||||
<Text>world</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function RootModel() {
|
||||
return {
|
||||
html: <HTML />,
|
||||
};
|
||||
}
|
||||
|
||||
// View
|
||||
function Message({response}) {
|
||||
return <section>{use(response).html}</section>;
|
||||
}
|
||||
function App({response}) {
|
||||
return (
|
||||
<Suspense fallback={<h1>Loading...</h1>}>
|
||||
<Message response={response} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const {writable, readable} = getTestStream();
|
||||
const {pipe} = ReactServerDOMServer.renderToPipeableStream(
|
||||
<RootModel />,
|
||||
turbopackMap,
|
||||
);
|
||||
pipe(writable);
|
||||
const response = ReactServerDOMClient.createFromReadableStream(readable);
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => {
|
||||
root.render(<App response={response} />);
|
||||
});
|
||||
expect(container.innerHTML).toBe(
|
||||
'<section><div><span>hello</span><span>world</span></div></section>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should unwrap async module references', async () => {
|
||||
const AsyncModule = Promise.resolve(function AsyncModule({text}) {
|
||||
return 'Async: ' + text;
|
||||
});
|
||||
|
||||
const AsyncModule2 = Promise.resolve({
|
||||
exportName: 'Module',
|
||||
});
|
||||
|
||||
function Print({response}) {
|
||||
return <p>{use(response)}</p>;
|
||||
}
|
||||
|
||||
function App({response}) {
|
||||
return (
|
||||
<Suspense fallback={<h1>Loading...</h1>}>
|
||||
<Print response={response} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const AsyncModuleRef = await clientExports(AsyncModule);
|
||||
const AsyncModuleRef2 = await clientExports(AsyncModule2);
|
||||
|
||||
const {writable, readable} = getTestStream();
|
||||
const {pipe} = ReactServerDOMServer.renderToPipeableStream(
|
||||
<AsyncModuleRef text={AsyncModuleRef2.exportName} />,
|
||||
turbopackMap,
|
||||
);
|
||||
pipe(writable);
|
||||
const response = ReactServerDOMClient.createFromReadableStream(readable);
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => {
|
||||
root.render(<App response={response} />);
|
||||
});
|
||||
expect(container.innerHTML).toBe('<p>Async: Module</p>');
|
||||
});
|
||||
});
|
||||
-95
@@ -54,37 +54,6 @@ describe('ReactFlightDOMEdge', () => {
|
||||
use = React.use;
|
||||
});
|
||||
|
||||
function passThrough(stream) {
|
||||
// Simulate more realistic network by splitting up and rejoining some chunks.
|
||||
// This lets us test that we don't accidentally rely on particular bounds of the chunks.
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
const reader = stream.getReader();
|
||||
let prevChunk = new Uint8Array(0);
|
||||
function push() {
|
||||
reader.read().then(({done, value}) => {
|
||||
if (done) {
|
||||
controller.enqueue(prevChunk);
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
const chunk = new Uint8Array(prevChunk.length + value.length);
|
||||
chunk.set(prevChunk, 0);
|
||||
chunk.set(value, prevChunk.length);
|
||||
if (chunk.length > 50) {
|
||||
controller.enqueue(chunk.subarray(0, chunk.length - 50));
|
||||
prevChunk = chunk.subarray(chunk.length - 50);
|
||||
} else {
|
||||
prevChunk = chunk;
|
||||
}
|
||||
push();
|
||||
});
|
||||
}
|
||||
push();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function readResult(stream) {
|
||||
const reader = stream.getReader();
|
||||
let result = '';
|
||||
@@ -144,68 +113,4 @@ describe('ReactFlightDOMEdge', () => {
|
||||
const result = await readResult(ssrStream);
|
||||
expect(result).toEqual('<span>Client Component</span>');
|
||||
});
|
||||
|
||||
it('should encode long string in a compact format', async () => {
|
||||
const testString = '"\n\t'.repeat(500) + '🙃';
|
||||
const testString2 = 'hello'.repeat(400);
|
||||
|
||||
const stream = ReactServerDOMServer.renderToReadableStream({
|
||||
text: testString,
|
||||
text2: testString2,
|
||||
});
|
||||
const [stream1, stream2] = passThrough(stream).tee();
|
||||
|
||||
const serializedContent = await readResult(stream1);
|
||||
// The content should be compact an unescaped
|
||||
expect(serializedContent.length).toBeLessThan(4000);
|
||||
expect(serializedContent).not.toContain('\\n');
|
||||
expect(serializedContent).not.toContain('\\t');
|
||||
expect(serializedContent).not.toContain('\\"');
|
||||
expect(serializedContent).toContain('\t');
|
||||
|
||||
const result = await ReactServerDOMClient.createFromReadableStream(
|
||||
stream2,
|
||||
{
|
||||
ssrManifest: {
|
||||
moduleMap: null,
|
||||
moduleLoading: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
// Should still match the result when parsed
|
||||
expect(result.text).toBe(testString);
|
||||
expect(result.text2).toBe(testString2);
|
||||
});
|
||||
|
||||
// @gate enableBinaryFlight
|
||||
it('should be able to serialize any kind of typed array', async () => {
|
||||
const buffer = new Uint8Array([
|
||||
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
|
||||
]).buffer;
|
||||
const buffers = [
|
||||
buffer,
|
||||
new Int8Array(buffer, 1),
|
||||
new Uint8Array(buffer, 2),
|
||||
new Uint8ClampedArray(buffer, 2),
|
||||
new Int16Array(buffer, 2),
|
||||
new Uint16Array(buffer, 2),
|
||||
new Int32Array(buffer, 4),
|
||||
new Uint32Array(buffer, 4),
|
||||
new Float32Array(buffer, 4),
|
||||
new Float64Array(buffer, 0),
|
||||
new BigInt64Array(buffer, 0),
|
||||
new BigUint64Array(buffer, 0),
|
||||
new DataView(buffer, 3),
|
||||
];
|
||||
const stream = passThrough(
|
||||
ReactServerDOMServer.renderToReadableStream(buffers),
|
||||
);
|
||||
const result = await ReactServerDOMClient.createFromReadableStream(stream, {
|
||||
ssrManifest: {
|
||||
moduleMap: null,
|
||||
moduleLoading: null,
|
||||
},
|
||||
});
|
||||
expect(result).toEqual(buffers);
|
||||
});
|
||||
});
|
||||
-109
@@ -145,113 +145,4 @@ describe('ReactFlightDOMForm', () => {
|
||||
expect(result).toBe('hello');
|
||||
expect(foo).toBe('bar');
|
||||
});
|
||||
|
||||
// @gate enableFormActions
|
||||
it('can submit an imported server action without hydrating it', async () => {
|
||||
let foo = null;
|
||||
|
||||
const ServerModule = serverExports(function action(formData) {
|
||||
foo = formData.get('foo');
|
||||
return 'hi';
|
||||
});
|
||||
const serverAction = ReactServerDOMClient.createServerReference(
|
||||
ServerModule.$$id,
|
||||
);
|
||||
function App() {
|
||||
return (
|
||||
<form action={serverAction}>
|
||||
<input type="text" name="foo" defaultValue="bar" />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
const ssrStream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
await readIntoContainer(ssrStream);
|
||||
|
||||
const form = container.firstChild;
|
||||
|
||||
expect(foo).toBe(null);
|
||||
|
||||
const result = await submit(form);
|
||||
|
||||
expect(result).toBe('hi');
|
||||
|
||||
expect(foo).toBe('bar');
|
||||
});
|
||||
|
||||
// @gate enableFormActions
|
||||
it('can submit a complex closure server action without hydrating it', async () => {
|
||||
let foo = null;
|
||||
|
||||
const serverAction = serverExports(function action(bound, formData) {
|
||||
foo = formData.get('foo') + bound.complex;
|
||||
return 'hello';
|
||||
});
|
||||
function App() {
|
||||
return (
|
||||
<form action={serverAction.bind(null, {complex: 'object'})}>
|
||||
<input type="text" name="foo" defaultValue="bar" />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
|
||||
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
|
||||
ssrManifest: {
|
||||
moduleMap: null,
|
||||
moduleLoading: null,
|
||||
},
|
||||
});
|
||||
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
|
||||
await readIntoContainer(ssrStream);
|
||||
|
||||
const form = container.firstChild;
|
||||
|
||||
expect(foo).toBe(null);
|
||||
|
||||
const result = await submit(form);
|
||||
|
||||
expect(result).toBe('hello');
|
||||
expect(foo).toBe('barobject');
|
||||
});
|
||||
|
||||
// @gate enableFormActions
|
||||
it('can submit a multiple complex closure server action without hydrating it', async () => {
|
||||
let foo = null;
|
||||
|
||||
const serverAction = serverExports(function action(bound, formData) {
|
||||
foo = formData.get('foo') + bound.complex;
|
||||
return 'hello' + bound.complex;
|
||||
});
|
||||
function App() {
|
||||
return (
|
||||
<form action={serverAction.bind(null, {complex: 'a'})}>
|
||||
<input type="text" name="foo" defaultValue="bar" />
|
||||
<button formAction={serverAction.bind(null, {complex: 'b'})} />
|
||||
<button formAction={serverAction.bind(null, {complex: 'c'})} />
|
||||
<input
|
||||
type="submit"
|
||||
formAction={serverAction.bind(null, {complex: 'd'})}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
|
||||
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
|
||||
ssrManifest: {
|
||||
moduleMap: null,
|
||||
moduleLoading: null,
|
||||
},
|
||||
});
|
||||
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
|
||||
await readIntoContainer(ssrStream);
|
||||
|
||||
const form = container.firstChild;
|
||||
|
||||
expect(foo).toBe(null);
|
||||
|
||||
const result = await submit(form.getElementsByTagName('button')[1]);
|
||||
|
||||
expect(result).toBe('helloc');
|
||||
expect(foo).toBe('barc');
|
||||
});
|
||||
});
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// Don't wait before processing work on the server.
|
||||
// TODO: we can replace this with FlightServer.act().
|
||||
global.setImmediate = cb => cb();
|
||||
|
||||
let clientExports;
|
||||
let turbopackMap;
|
||||
let turbopackModules;
|
||||
let turbopackModuleLoading;
|
||||
let React;
|
||||
let ReactDOMServer;
|
||||
let ReactServerDOMServer;
|
||||
let ReactServerDOMClient;
|
||||
let Stream;
|
||||
let use;
|
||||
|
||||
describe('ReactFlightDOMNode', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
// Simulate the condition resolution
|
||||
jest.mock('react', () => require('react/react.shared-subset'));
|
||||
jest.mock('react-server-dom-turbopack/server', () =>
|
||||
require('react-server-dom-turbopack/server.node'),
|
||||
);
|
||||
ReactServerDOMServer = require('react-server-dom-turbopack/server');
|
||||
|
||||
const TurbopackMock = require('./utils/TurbopackMock');
|
||||
clientExports = TurbopackMock.clientExports;
|
||||
turbopackMap = TurbopackMock.turbopackMap;
|
||||
turbopackModules = TurbopackMock.turbopackModules;
|
||||
turbopackModuleLoading = TurbopackMock.moduleLoading;
|
||||
|
||||
jest.resetModules();
|
||||
__unmockReact();
|
||||
jest.unmock('react-server-dom-turbopack/server');
|
||||
jest.mock('react-server-dom-turbopack/client', () =>
|
||||
require('react-server-dom-turbopack/client.node'),
|
||||
);
|
||||
|
||||
React = require('react');
|
||||
ReactDOMServer = require('react-dom/server.node');
|
||||
ReactServerDOMClient = require('react-server-dom-turbopack/client');
|
||||
Stream = require('stream');
|
||||
use = React.use;
|
||||
});
|
||||
|
||||
function readResult(stream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = '';
|
||||
const writable = new Stream.PassThrough();
|
||||
writable.setEncoding('utf8');
|
||||
writable.on('data', chunk => {
|
||||
buffer += chunk;
|
||||
});
|
||||
writable.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
writable.on('end', () => {
|
||||
resolve(buffer);
|
||||
});
|
||||
stream.pipe(writable);
|
||||
});
|
||||
}
|
||||
|
||||
it('should allow an alternative module mapping to be used for SSR', async () => {
|
||||
function ClientComponent() {
|
||||
return <span>Client Component</span>;
|
||||
}
|
||||
// The Client build may not have the same IDs as the Server bundles for the same
|
||||
// component.
|
||||
const ClientComponentOnTheClient = clientExports(
|
||||
ClientComponent,
|
||||
'path/to/chunk.js',
|
||||
);
|
||||
const ClientComponentOnTheServer = clientExports(ClientComponent);
|
||||
|
||||
// In the SSR bundle this module won't exist. We simulate this by deleting it.
|
||||
const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id;
|
||||
delete turbopackModules[clientId];
|
||||
|
||||
// Instead, we have to provide a translation from the client meta data to the SSR
|
||||
// meta data.
|
||||
const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id];
|
||||
const translationMap = {
|
||||
[clientId]: {
|
||||
'*': ssrMetadata,
|
||||
},
|
||||
};
|
||||
|
||||
function App() {
|
||||
return <ClientComponentOnTheClient />;
|
||||
}
|
||||
|
||||
const stream = ReactServerDOMServer.renderToPipeableStream(
|
||||
<App />,
|
||||
turbopackMap,
|
||||
);
|
||||
const readable = new Stream.PassThrough();
|
||||
|
||||
stream.pipe(readable);
|
||||
|
||||
let response;
|
||||
function ClientRoot() {
|
||||
if (!response) {
|
||||
response = ReactServerDOMClient.createFromNodeStream(readable, {
|
||||
moduleMap: translationMap,
|
||||
moduleLoading: turbopackModuleLoading,
|
||||
});
|
||||
}
|
||||
return use(response);
|
||||
}
|
||||
|
||||
const ssrStream = await ReactDOMServer.renderToPipeableStream(
|
||||
<ClientRoot />,
|
||||
);
|
||||
const result = await readResult(ssrStream);
|
||||
expect(result).toEqual(
|
||||
'<script src="/prefix/path/to/chunk.js" async=""></script><span>Client Component</span>',
|
||||
);
|
||||
});
|
||||
});
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// 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 turbopackServerMap;
|
||||
let ReactServerDOMServer;
|
||||
let ReactServerDOMClient;
|
||||
|
||||
describe('ReactFlightDOMReply', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
// Simulate the condition resolution
|
||||
jest.mock('react', () => require('react/react.shared-subset'));
|
||||
jest.mock('react-server-dom-turbopack/server', () =>
|
||||
require('react-server-dom-turbopack/server.browser'),
|
||||
);
|
||||
const TurbopackMock = require('./utils/TurbopackMock');
|
||||
// serverExports = TurbopackMock.serverExports;
|
||||
turbopackServerMap = TurbopackMock.turbopackServerMap;
|
||||
ReactServerDOMServer = require('react-server-dom-turbopack/server.browser');
|
||||
jest.resetModules();
|
||||
ReactServerDOMClient = require('react-server-dom-turbopack/client');
|
||||
});
|
||||
|
||||
it('can encode a reply', async () => {
|
||||
const body = await ReactServerDOMClient.encodeReply({some: 'object'});
|
||||
const decoded = await ReactServerDOMServer.decodeReply(
|
||||
body,
|
||||
turbopackServerMap,
|
||||
);
|
||||
|
||||
expect(decoded).toEqual({some: 'object'});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user