react/packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMEdge-test.js
Hendrik Liebau cacc20e37c
[Flight] Wait for both streams to end before closing the response (#34301)
When a debug channel is defined, we must ensure that we don't close the
Flight Client's response when the debug channel's readable is done, but
the RSC stream is still flowing. Now, we wait for both streams to end
before closing the response.
2025-08-26 17:15:25 +02:00

245 lines
6.7 KiB
JavaScript

/**
* 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.WritableStream =
require('web-streams-polyfill/ponyfill/es6').WritableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
let clientExports;
let turbopackMap;
let turbopackModules;
let React;
let ReactServer;
let ReactDOMServer;
let ReactServerDOMServer;
let ReactServerDOMClient;
let use;
let serverAct;
describe('ReactFlightTurbopackDOMEdge', () => {
beforeEach(() => {
jest.resetModules();
serverAct = require('internal-test-utils').serverAct;
// Simulate the condition resolution
jest.mock('react', () => require('react/react.react-server'));
jest.mock('react-server-dom-turbopack/server', () =>
require('react-server-dom-turbopack/server.edge'),
);
const TurbopackMock = require('./utils/TurbopackMock');
clientExports = TurbopackMock.clientExports;
turbopackMap = TurbopackMock.turbopackMap;
turbopackModules = TurbopackMock.turbopackModules;
ReactServer = require('react');
ReactServerDOMServer = require('react-server-dom-turbopack/server.edge');
jest.resetModules();
__unmockReact();
React = require('react');
ReactDOMServer = require('react-dom/server.edge');
ReactServerDOMClient = require('react-server-dom-turbopack/client.edge');
use = React.use;
});
async function readResult(stream) {
const reader = stream.getReader();
let result = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
return result;
}
result += Buffer.from(value).toString('utf8');
}
}
function normalizeCodeLocInfo(str) {
return (
str &&
str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
})
);
}
function createDelayedStream(
stream: ReadableStream<Uint8Array>,
): ReadableStream<Uint8Array> {
return new ReadableStream({
async start(controller) {
const reader = stream.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) {
controller.close();
} else {
// Artificially delay between enqueuing chunks.
await new Promise(resolve => setTimeout(resolve));
controller.enqueue(value);
}
}
},
});
}
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);
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 = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(<App />, turbopackMap),
);
const response = ReactServerDOMClient.createFromReadableStream(stream, {
serverConsumerManifest: {
moduleMap: translationMap,
moduleLoading: null,
},
});
function ClientRoot() {
return use(response);
}
const ssrStream = await serverAct(() =>
ReactDOMServer.renderToReadableStream(<ClientRoot />),
);
const result = await readResult(ssrStream);
expect(result).toEqual('<span>Client Component</span>');
});
// @gate __DEV__
it('can transport debug info through a separate debug channel', async () => {
function Thrower() {
throw new Error('ssr-throw');
}
const ClientComponentOnTheClient = clientExports(
Thrower,
123,
'path/to/chunk.js',
);
const ClientComponentOnTheServer = clientExports(Thrower);
function App() {
return ReactServer.createElement(
ReactServer.Suspense,
null,
ReactServer.createElement(ClientComponentOnTheClient, null),
);
}
let debugReadableStreamController;
const debugReadableStream = new ReadableStream({
start(controller) {
debugReadableStreamController = controller;
},
});
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
ReactServer.createElement(App, null),
turbopackMap,
{
debugChannel: {
writable: new WritableStream({
write(chunk) {
debugReadableStreamController.enqueue(chunk);
},
close() {
debugReadableStreamController.close();
},
}),
},
},
),
);
function ClientRoot({response}) {
return use(response);
}
const serverConsumerManifest = {
moduleMap: {
[turbopackMap[ClientComponentOnTheClient.$$id].id]: {
'*': turbopackMap[ClientComponentOnTheServer.$$id],
},
},
moduleLoading: null,
};
const response = ReactServerDOMClient.createFromReadableStream(
// Create a delayed stream to simulate that the RSC stream might be
// transported slower than the debug channel, which must not lead to a
// `Connection closed` error in the Flight client.
createDelayedStream(rscStream),
{
serverConsumerManifest,
debugChannel: {readable: debugReadableStream},
},
);
let ownerStack;
const ssrStream = await serverAct(() =>
ReactDOMServer.renderToReadableStream(
<ClientRoot response={response} />,
{
onError(err, errorInfo) {
ownerStack = React.captureOwnerStack
? React.captureOwnerStack()
: null;
},
},
),
);
const result = await readResult(ssrStream);
expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
expect(result).toContain(
'Switched to client rendering because the server rendering errored:\n\nssr-throw',
);
});
});