[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:
Josh Story
2023-10-20 23:21:31 -07:00
committed by GitHub
parent b9244f79ba
commit 90172d12e8
10 changed files with 388 additions and 2266 deletions
File diff suppressed because it is too large Load Diff
@@ -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>',
);
});
});
@@ -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);
});
});
@@ -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>');
});
});
@@ -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);
});
});
@@ -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');
});
});
@@ -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>',
);
});
});
@@ -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'});
});
});