'use strict';
import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
let clientExports;
let webpackMap;
let webpackModules;
let webpackModuleLoading;
let React;
let ReactDOMServer;
let ReactServer;
let ReactServerDOMServer;
let ReactServerDOMStaticServer;
let ReactServerDOMClient;
let Stream;
let use;
let ReactServerScheduler;
let reactServerAct;
const streamOptions = {
objectMode: true,
};
describe('ReactFlightDOMNode', () => {
beforeEach(() => {
jest.resetModules();
ReactServerScheduler = require('scheduler');
patchSetImmediate(ReactServerScheduler);
reactServerAct = require('internal-test-utils').act;
jest.mock('react', () => require('react/react.react-server'));
jest.mock('react-server-dom-webpack/server', () =>
require('react-server-dom-webpack/server.node'),
);
ReactServer = require('react');
ReactServerDOMServer = require('react-server-dom-webpack/server');
if (__EXPERIMENTAL__) {
jest.mock('react-server-dom-webpack/static', () =>
require('react-server-dom-webpack/static.node'),
);
ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
}
const WebpackMock = require('./utils/WebpackMock');
clientExports = WebpackMock.clientExports;
webpackMap = WebpackMock.webpackMap;
webpackModules = WebpackMock.webpackModules;
webpackModuleLoading = WebpackMock.moduleLoading;
jest.resetModules();
__unmockReact();
jest.unmock('react-server-dom-webpack/server');
jest.mock('react-server-dom-webpack/client', () =>
require('react-server-dom-webpack/client.node'),
);
React = require('react');
ReactDOMServer = require('react-dom/server.node');
ReactServerDOMClient = require('react-server-dom-webpack/client');
Stream = require('stream');
use = React.use;
});
async function serverAct(callback) {
let maybePromise;
await reactServerAct(() => {
maybePromise = callback();
if (maybePromise && typeof maybePromise.catch === 'function') {
maybePromise.catch(() => {});
}
});
return maybePromise;
}
function readResult(stream) {
return new Promise((resolve, reject) => {
let buffer = '';
const writable = new Stream.PassThrough(streamOptions);
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>;
}
const ClientComponentOnTheClient = clientExports(
ClientComponent,
123,
'path/to/chunk.js',
);
const ClientComponentOnTheServer = clientExports(ClientComponent);
const clientId = webpackMap[ClientComponentOnTheClient.$$id].id;
delete webpackModules[clientId];
const ssrMetadata = webpackMap[ClientComponentOnTheServer.$$id];
const translationMap = {
[clientId]: {
'*': ssrMetadata,
},
};
const ssrManifest = {
moduleMap: translationMap,
moduleLoading: webpackModuleLoading,
};
function App() {
return <ClientComponentOnTheClient />;
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
);
const readable = new Stream.PassThrough(streamOptions);
let response;
stream.pipe(readable);
function ClientRoot() {
if (response) return use(response);
response = ReactServerDOMClient.createFromNodeStream(
readable,
ssrManifest,
);
return use(response);
}
const ssrStream = await serverAct(() =>
ReactDOMServer.renderToPipeableStream(<ClientRoot />),
);
const result = await readResult(ssrStream);
expect(result).toEqual(
'<script src="/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 = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream({
text: testString,
}),
);
const readable = new Stream.PassThrough(streamOptions);
const stringResult = readResult(readable);
const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: webpackModuleLoading,
});
stream.pipe(readable);
const serializedContent = await stringResult;
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;
expect(result.text).toBe(testString);
});
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 = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(buffers),
);
const readable = new Stream.PassThrough(streamOptions);
const promise = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: webpackModuleLoading,
});
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>;
}
const ClientComponentOnTheClient = clientExports(
ClientComponent,
123,
'path/to/chunk.js',
);
const ClientComponentOnTheServer = clientExports(ClientComponent);
const clientId = webpackMap[ClientComponentOnTheClient.$$id].id;
delete webpackModules[clientId];
const ssrMetadata = webpackMap[ClientComponentOnTheServer.$$id];
const translationMap = {
[clientId]: {
'*': ssrMetadata,
},
};
const ssrManifest = {
moduleMap: translationMap,
moduleLoading: webpackModuleLoading,
};
function App() {
return <ClientComponentOnTheClient />;
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
);
const readable = new Stream.PassThrough(streamOptions);
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 serverAct(() =>
ReactDOMServer.renderToPipeableStream(<ClientRoot />),
);
const result = await readResult(ssrStream);
expect(result).toEqual(
'<script src="/path/to/chunk.js" async="" nonce="r4nd0m"></script><span>Client Component</span>',
);
});
it('should cancels the underlying ReadableStream when we are cancelled', async () => {
let controller;
let cancelReason;
const s = new ReadableStream({
start(c) {
controller = c;
},
cancel(r) {
cancelReason = r;
},
});
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
s,
{},
{
onError(error) {
return error.message;
},
},
),
);
const writable = new Stream.PassThrough(streamOptions);
rscStream.pipe(writable);
controller.enqueue('hi');
const reason = new Error('aborted');
writable.destroy(reason);
await new Promise(resolve => {
writable.on('error', () => {
resolve();
});
});
expect(cancelReason.message).toBe(
'The destination stream errored while writing data.',
);
});
it('should cancels the underlying ReadableStream when we abort', async () => {
const errors = [];
let controller;
let cancelReason;
const s = new ReadableStream({
start(c) {
controller = c;
},
cancel(r) {
cancelReason = r;
},
});
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
s,
{},
{
onError(x) {
errors.push(x);
return x.message;
},
},
),
);
const readable = new Stream.PassThrough(streamOptions);
rscStream.pipe(readable);
const result = await ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: webpackModuleLoading,
});
const reader = result.getReader();
controller.enqueue('hi');
const reason = new Error('aborted');
rscStream.abort(reason);
expect(cancelReason).toBe(reason);
let error = null;
try {
await reader.read();
} catch (x) {
error = x;
}
expect(error.digest).toBe('aborted');
expect(errors).toEqual([reason]);
});
it('can prerender', async () => {
let resolveGreeting;
const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve;
});
function App() {
return (
<div>
<Greeting />
</div>
);
}
async function Greeting() {
await greetingPromise;
return 'hello world';
}
const {pendingResult} = await serverAct(async () => {
return {
pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
<App />,
webpackMap,
),
};
});
resolveGreeting();
const {prelude} = await pendingResult;
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromNodeStream(prelude, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await serverAct(() =>
ReactDOMServer.renderToPipeableStream(
React.createElement(ClientRoot, {response}),
),
);
const result = await readResult(ssrStream);
expect(result).toBe('<div>hello world</div>');
});
it('does not propagate abort reasons errors when aborting a prerender', async () => {
let resolveGreeting;
const greetingPromise = new Promise(resolve => {
resolveGreeting = resolve;
});
function App() {
return (
<div>
<ReactServer.Suspense fallback="loading...">
<Greeting />
</ReactServer.Suspense>
</div>
);
}
async function Greeting() {
await greetingPromise;
return 'hello world';
}
const controller = new AbortController();
const errors = [];
const {pendingResult} = await serverAct(async () => {
return {
pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
<App />,
webpackMap,
{
signal: controller.signal,
onError(err) {
errors.push(err);
},
},
),
};
});
controller.abort('boom');
resolveGreeting();
const {prelude} = await pendingResult;
expect(errors).toEqual(['boom']);
function ClientRoot({response}) {
return use(response);
}
const response = ReactServerDOMClient.createFromNodeStream(prelude, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
errors.length = 0;
const ssrStream = await serverAct(() =>
ReactDOMServer.renderToPipeableStream(
React.createElement(ClientRoot, {response}),
{
onError(error) {
errors.push(error);
},
},
),
);
ssrStream.abort('bam');
if (__DEV__) {
expect(errors).toEqual([new Error('Connection closed.')]);
} else {
expect(errors).toEqual(['bam']);
}
const result = await readResult(ssrStream);
const div = document.createElement('div');
div.innerHTML = result;
expect(div.textContent).toBe('loading...');
});
});