'use strict';
const path = require('path');
if (typeof Blob === 'undefined') {
global.Blob = require('buffer').Blob;
}
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
global.File = require('undici').File;
global.FormData = require('undici').FormData;
}
function normalizeCodeLocInfo(str) {
return (
str &&
str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
const dot = name.lastIndexOf('.');
if (dot !== -1) {
name = name.slice(dot + 1);
}
return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
})
);
}
function formatV8Stack(stack) {
let v8StyleStack = '';
if (stack) {
for (let i = 0; i < stack.length; i++) {
const [name] = stack[i];
if (v8StyleStack !== '') {
v8StyleStack += '\n';
}
v8StyleStack += ' in ' + name + ' (at **)';
}
}
return v8StyleStack;
}
const repoRoot = path.resolve(__dirname, '../../../../');
function normalizeReactCodeLocInfo(str) {
const repoRootForRegexp = repoRoot.replace(/\//g, '\\/');
const repoFileLocMatch = new RegExp(`${repoRootForRegexp}.+?:\\d+:\\d+`, 'g');
return str && str.replace(repoFileLocMatch, '**');
}
function getErrorForJestMatcher(error) {
return {
...error,
message: error.message,
stack: normalizeReactCodeLocInfo(error.stack),
};
}
function normalizeComponentInfo(debugInfo) {
if (Array.isArray(debugInfo.stack)) {
const {debugTask, debugStack, ...copy} = debugInfo;
copy.stack = formatV8Stack(debugInfo.stack);
if (debugInfo.owner) {
copy.owner = normalizeComponentInfo(debugInfo.owner);
}
return copy;
} else {
return debugInfo;
}
}
function getDebugInfo(obj) {
const debugInfo = obj._debugInfo;
if (debugInfo) {
const copy = [];
for (let i = 0; i < debugInfo.length; i++) {
copy.push(normalizeComponentInfo(debugInfo[i]));
}
return copy;
}
return debugInfo;
}
const heldValues = [];
let finalizationCallback;
function FinalizationRegistryMock(callback) {
finalizationCallback = callback;
}
FinalizationRegistryMock.prototype.register = function (target, heldValue) {
heldValues.push(heldValue);
};
global.FinalizationRegistry = FinalizationRegistryMock;
function gc() {
for (let i = 0; i < heldValues.length; i++) {
finalizationCallback(heldValues[i]);
}
heldValues.length = 0;
}
let act;
let use;
let startTransition;
let React;
let ReactServer;
let ReactNoop;
let ReactNoopFlightServer;
let ReactNoopFlightClient;
let ErrorBoundary;
let NoErrorExpected;
let Scheduler;
let assertLog;
let assertConsoleErrorDev;
describe('ReactFlight', () => {
beforeEach(() => {
jest.resetModules();
jest.mock('react', () => require('react/react.react-server'));
ReactServer = require('react');
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
const flightModules = require('react-noop-renderer/flight-modules');
jest.resetModules();
__unmockReact();
jest.mock('react-noop-renderer/flight-modules', () => flightModules);
React = require('react');
startTransition = React.startTransition;
use = React.use;
ReactNoop = require('react-noop-renderer');
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
act = require('internal-test-utils').act;
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
ErrorBoundary = class extends React.Component {
state = {hasError: false, error: null};
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
componentDidCatch(error, errorInfo) {
expect(error).toBe(this.state.error);
if (this.props.expectedStack !== undefined) {
expect(normalizeCodeLocInfo(errorInfo.componentStack)).toBe(
this.props.expectedStack,
);
}
}
componentDidMount() {
expect(this.state.hasError).toBe(true);
expect(this.state.error).toBeTruthy();
if (__DEV__) {
expect(this.state.error.message).toContain(
this.props.expectedMessage,
);
expect(this.state.error.digest).toBe('a dev digest');
expect(this.state.error.environmentName).toBe(
this.props.expectedEnviromentName || 'Server',
);
if (this.props.expectedErrorStack !== undefined) {
expect(this.state.error.stack).toContain(
this.props.expectedErrorStack,
);
}
} else {
expect(this.state.error.message).toBe(
'An error occurred in the Server Components render. The specific message is omitted in production' +
' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
' may provide additional details about the nature of the error.',
);
let expectedDigest = this.props.expectedMessage;
if (
expectedDigest.startsWith('{') ||
expectedDigest.startsWith('<')
) {
expectedDigest = '{}';
} else if (expectedDigest.startsWith('[')) {
expectedDigest = '[]';
}
expect(this.state.error.digest).toContain(expectedDigest);
expect(this.state.error.environmentName).toBe(undefined);
expect(this.state.error.stack).toBe(
'Error: ' + this.state.error.message,
);
}
}
render() {
if (this.state.hasError) {
return this.state.error.message;
}
return this.props.children;
}
};
NoErrorExpected = class extends React.Component {
state = {hasError: false, error: null};
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
componentDidMount() {
expect(this.state.error).toBe(null);
expect(this.state.hasError).toBe(false);
}
render() {
if (this.state.hasError) {
return this.state.error.message;
}
return this.props.children;
}
};
});
afterEach(() => {
jest.restoreAllMocks();
});
function clientReference(value) {
return Object.defineProperties(
function () {
throw new Error('Cannot call a client function from the server.');
},
{
$$typeof: {value: Symbol.for('react.client.reference')},
value: {value: value},
},
);
}
it('can render a Server Component', async () => {
function Bar({text}) {
return text.toUpperCase();
}
function Foo() {
return {
bar: (
<div>
<Bar text="a" />, <Bar text="b" />
</div>
),
};
}
const transport = ReactNoopFlightServer.render({
foo: <Foo />,
});
const model = await ReactNoopFlightClient.read(transport);
expect(model).toEqual({
foo: {
bar: (
<div>
{'A'}
{', '}
{'B'}
</div>
),
},
});
});
it('can render a Client Component using a module reference and render there', async () => {
function UserClient(props) {
return (
<span>
{props.greeting}, {props.name}
</span>
);
}
const User = clientReference(UserClient);
function Greeting({firstName, lastName}) {
return <User greeting="Hello" name={firstName + ' ' + lastName} />;
}
const model = {
greeting: <Greeting firstName="Seb" lastName="Smith" />,
};
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport);
const greeting = rootModel.greeting;
expect(getDebugInfo(greeting)).toEqual(
__DEV__
? [
{
name: 'Greeting',
env: 'Server',
key: null,
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
props: {
firstName: 'Seb',
lastName: 'Smith',
},
},
]
: undefined,
);
ReactNoop.render(greeting);
});
expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
});
it('can render a shared forwardRef Component', async () => {
const Greeting = React.forwardRef(function Greeting(
{firstName, lastName},
ref,
) {
return (
<span ref={ref}>
Hello, {firstName} {lastName}
</span>
);
});
const root = <Greeting firstName="Seb" lastName="Smith" />;
const transport = ReactNoopFlightServer.render(root);
await act(async () => {
const promise = ReactNoopFlightClient.read(transport);
expect(getDebugInfo(promise)).toEqual(
__DEV__
? [
{
name: 'Greeting',
env: 'Server',
key: null,
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
props: {
firstName: 'Seb',
lastName: 'Smith',
},
},
]
: undefined,
);
ReactNoop.render(await promise);
});
expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
});
it('can render an iterable as an array', async () => {
function ItemListClient(props) {
return <span>{props.items}</span>;
}
const ItemList = clientReference(ItemListClient);
function Items() {
const iterable = {
[Symbol.iterator]: function* () {
yield 'A';
yield 'B';
yield 'C';
},
};
return <ItemList items={iterable} />;
}
const model = <Items />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(<span>ABC</span>);
});
it('can render an iterator as a single shot iterator', async () => {
const iterator = (function* () {
yield 'A';
yield 'B';
yield 'C';
})();
const transport = ReactNoopFlightServer.render(iterator);
const result = await ReactNoopFlightClient.read(transport);
expect(result[Symbol.iterator]()).toBe(result);
expect(Array.from(result)).toEqual(['A', 'B', 'C']);
expect(Array.from(result)).toEqual([]);
});
it('can render a Generator Server Component as a fragment', async () => {
function ItemListClient(props) {
return <span>{props.children}</span>;
}
const ItemList = clientReference(ItemListClient);
function* Items() {
yield 'A';
yield 'B';
yield 'C';
}
const model = (
<ItemList>
<Items />
</ItemList>
);
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(<span>ABC</span>);
});
it('can render undefined', async () => {
function Undefined() {
return undefined;
}
const model = <Undefined />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(null);
});
it('should transport undefined object values', async () => {
function ServerComponent(props) {
return 'prop' in props
? `\`prop\` in props as '${props.prop}'`
: '`prop` not in props';
}
const ClientComponent = clientReference(ServerComponent);
const model = (
<>
<div>
Server: <ServerComponent prop={undefined} />
</div>
<div>
Client: <ClientComponent prop={undefined} />
</div>
</>
);
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div>Server: `prop` in props as 'undefined'</div>
<div>Client: `prop` in props as 'undefined'</div>
</>,
);
});
it('can render an empty fragment', async () => {
function Empty() {
return <React.Fragment />;
}
const model = <Empty />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(null);
});
it('can transport weird numbers', async () => {
const nums = [0, -0, Infinity, -Infinity, NaN];
function ComponentClient({prop}) {
expect(prop).not.toBe(nums);
expect(prop).toEqual(nums);
expect(prop.every((p, i) => Object.is(p, nums[i]))).toBe(true);
return `prop: ${prop}`;
}
const Component = clientReference(ComponentClient);
const model = <Component prop={nums} />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
'prop: 0,0,Infinity,-Infinity,NaN',
);
});
it('can transport BigInt', async () => {
function ComponentClient({prop}) {
return `prop: ${prop} (${typeof prop})`;
}
const Component = clientReference(ComponentClient);
const model = <Component prop={90071992547409910000n} />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
'prop: 90071992547409910000 (bigint)',
);
});
it('can transport Date', async () => {
function ComponentClient({prop}) {
return `prop: ${prop.toISOString()}`;
}
const Component = clientReference(ComponentClient);
const model = <Component prop={new Date(1234567890123)} />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput('prop: 2009-02-13T23:31:30.123Z');
});
it('can transport Map', async () => {
function ComponentClient({prop, selected}) {
return `
map: ${prop instanceof Map}
size: ${prop.size}
greet: ${prop.get('hi').greet}
content: ${JSON.stringify(Array.from(prop))}
selected: ${prop.get(selected)}
`;
}
const Component = clientReference(ComponentClient);
const objKey = {obj: 'key'};
const map = new Map([
['hi', {greet: 'world'}],
[objKey, 123],
]);
const model = <Component prop={map} selected={objKey} />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(`
map: true
size: 2
greet: world
content: [["hi",{"greet":"world"}],[{"obj":"key"},123]]
selected: 123
`);
});
it('can transport Set', async () => {
function ComponentClient({prop, selected}) {
return `
set: ${prop instanceof Set}
size: ${prop.size}
hi: ${prop.has('hi')}
content: ${JSON.stringify(Array.from(prop))}
selected: ${prop.has(selected)}
`;
}
const Component = clientReference(ComponentClient);
const objKey = {obj: 'key'};
const set = new Set(['hi', objKey]);
const model = <Component prop={set} selected={objKey} />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(`
set: true
size: 2
hi: true
content: ["hi",{"obj":"key"}]
selected: true
`);
});
it('can transport FormData (no blobs)', async () => {
function ComponentClient({prop}) {
return `
formData: ${prop instanceof FormData}
hi: ${prop.get('hi')}
multiple: ${prop.getAll('multiple')}
content: ${JSON.stringify(Array.from(prop))}
`;
}
const Component = clientReference(ComponentClient);
const formData = new FormData();
formData.append('hi', 'world');
formData.append('multiple', 1);
formData.append('multiple', 2);
const model = <Component prop={formData} />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(`
formData: true
hi: world
multiple: 1,2
content: [["hi","world"],["multiple","1"],["multiple","2"]]
`);
});
it('can transport Error objects as values', async () => {
function ComponentClient({prop}) {
return `
is error: ${prop instanceof Error}
message: ${prop.message}
stack: ${normalizeCodeLocInfo(prop.stack).split('\n').slice(0, 2).join('\n')}
environmentName: ${prop.environmentName}
`;
}
const Component = clientReference(ComponentClient);
function ServerComponent() {
const error = new Error('hello');
return <Component prop={error} />;
}
const transport = ReactNoopFlightServer.render(<ServerComponent />);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
if (__DEV__) {
expect(ReactNoop).toMatchRenderedOutput(`
is error: true
message: hello
stack: Error: hello
in ServerComponent (at **)
environmentName: Server
`);
} else {
expect(ReactNoop).toMatchRenderedOutput(`
is error: true
message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
environmentName: undefined
`);
}
});
it('can transport cyclic objects', async () => {
function ComponentClient({prop}) {
expect(prop.obj.obj.obj).toBe(prop.obj.obj);
}
const Component = clientReference(ComponentClient);
const cyclic = {obj: null};
cyclic.obj = cyclic;
const model = <Component prop={cyclic} />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
});
it('can render a lazy component as a shared component on the server', async () => {
function SharedComponent({text}) {
return (
<div>
shared<span>{text}</span>
</div>
);
}
let load = null;
const loadSharedComponent = () => {
return new Promise(res => {
load = () => res({default: SharedComponent});
});
};
const LazySharedComponent = React.lazy(loadSharedComponent);
function ServerComponent() {
return (
<React.Suspense fallback={'Loading...'}>
<LazySharedComponent text={'a'} />
</React.Suspense>
);
}
const transport = ReactNoopFlightServer.render(<ServerComponent />);
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport);
ReactNoop.render(rootModel);
});
expect(ReactNoop).toMatchRenderedOutput('Loading...');
await load();
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport);
ReactNoop.render(rootModel);
});
expect(ReactNoop).toMatchRenderedOutput(
<div>
shared<span>a</span>
</div>,
);
});
it('errors on a Lazy element being used in Component position', async () => {
function SharedComponent({text}) {
return (
<div>
shared<span>{text}</span>
</div>
);
}
let load = null;
const LazyElementDisguisedAsComponent = React.lazy(() => {
return new Promise(res => {
load = () => res({default: <SharedComponent text={'a'} />});
});
});
function ServerComponent() {
return (
<React.Suspense fallback={'Loading...'}>
<LazyElementDisguisedAsComponent text={'b'} />
</React.Suspense>
);
}
const transport = ReactNoopFlightServer.render(<ServerComponent />);
await load();
await expect(async () => {
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport);
ReactNoop.render(rootModel);
});
}).rejects.toThrow(
__DEV__
? 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
'(for composite components) but got: <div />. ' +
'Did you accidentally export a JSX literal instead of a component?'
: 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
'(for composite components) but got: object.',
);
expect(ReactNoop).toMatchRenderedOutput(null);
});
it('can render a lazy element', async () => {
function SharedComponent({text}) {
return (
<div>
shared<span>{text}</span>
</div>
);
}
let load = null;
const lazySharedElement = React.lazy(() => {
return new Promise(res => {
load = () => res({default: <SharedComponent text={'a'} />});
});
});
function ServerComponent() {
return (
<React.Suspense fallback={'Loading...'}>
{lazySharedElement}
</React.Suspense>
);
}
const transport = ReactNoopFlightServer.render(<ServerComponent />);
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport);
ReactNoop.render(rootModel);
});
expect(ReactNoop).toMatchRenderedOutput('Loading...');
await load();
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport);
ReactNoop.render(rootModel);
});
expect(ReactNoop).toMatchRenderedOutput(
<div>
shared<span>a</span>
</div>,
);
});
it('errors with lazy value in element position that resolves to Component', async () => {
function SharedComponent({text}) {
return (
<div>
shared<span>{text}</span>
</div>
);
}
let load = null;
const componentDisguisedAsElement = React.lazy(() => {
return new Promise(res => {
load = () => res({default: SharedComponent});
});
});
function ServerComponent() {
return (
<React.Suspense fallback={'Loading...'}>
{componentDisguisedAsElement}
</React.Suspense>
);
}
const transport = ReactNoopFlightServer.render(<ServerComponent />);
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport);
ReactNoop.render(rootModel);
});
expect(ReactNoop).toMatchRenderedOutput('Loading...');
spyOnDevAndProd(console, 'error').mockImplementation(() => {});
await load();
expect(console.error).toHaveBeenCalledTimes(1);
});
it('can render a lazy module reference', async () => {
function ClientComponent() {
return <div>I am client</div>;
}
const ClientComponentReference = clientReference(ClientComponent);
let load = null;
const loadClientComponentReference = () => {
return new Promise(res => {
load = () => res({default: ClientComponentReference});
});
};
const LazyClientComponentReference = React.lazy(
loadClientComponentReference,
);
function ServerComponent() {
return (
<React.Suspense fallback={'Loading...'}>
<LazyClientComponentReference />
</React.Suspense>
);
}
const transport = ReactNoopFlightServer.render(<ServerComponent />);
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport);
ReactNoop.render(rootModel);
});
expect(ReactNoop).toMatchRenderedOutput('Loading...');
await load();
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport);
ReactNoop.render(rootModel);
});
expect(ReactNoop).toMatchRenderedOutput(<div>I am client</div>);
});
it('should error if a non-serializable value is passed to a host component', async () => {
function ClientImpl({children}) {
return children;
}
const Client = clientReference(ClientImpl);
function EventHandlerProp() {
return (
<div className="foo" onClick={function () {}}>
Test
</div>
);
}
function FunctionProp() {
return <div>{function fn() {}}</div>;
}
function SymbolProp() {
return <div foo={Symbol('foo')} />;
}
const ref = React.createRef();
function RefProp() {
return <div ref={ref} />;
}
function EventHandlerPropClient() {
return (
<Client className="foo" onClick={function () {}}>
Test
</Client>
);
}
function FunctionChildrenClient() {
return <Client>{function Component() {}}</Client>;
}
function FunctionPropClient() {
return <Client foo={() => {}} />;
}
function SymbolPropClient() {
return <Client foo={Symbol('foo')} />;
}
function RefPropClient() {
return <Client ref={ref} />;
}
const options = {
onError(x) {
return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
},
};
const event = ReactNoopFlightServer.render(<EventHandlerProp />, options);
const fn = ReactNoopFlightServer.render(<FunctionProp />, options);
const symbol = ReactNoopFlightServer.render(<SymbolProp />, options);
const refs = ReactNoopFlightServer.render(<RefProp />, options);
const eventClient = ReactNoopFlightServer.render(
<EventHandlerPropClient />,
options,
);
const fnChildrenClient = ReactNoopFlightServer.render(
<FunctionChildrenClient />,
options,
);
const fnClient = ReactNoopFlightServer.render(
<FunctionPropClient />,
options,
);
const symbolClient = ReactNoopFlightServer.render(
<SymbolPropClient />,
options,
);
const refsClient = ReactNoopFlightServer.render(<RefPropClient />, options);
function Render({promise}) {
return use(promise);
}
await act(() => {
startTransition(() => {
ReactNoop.render(
<>
<ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
<Render promise={ReactNoopFlightClient.read(event)} />
</ErrorBoundary>
<ErrorBoundary
expectedMessage={
__DEV__
? 'Functions are not valid as a child of Client Components. This may happen if you return fn instead of <fn /> from render. Or maybe you meant to call this function rather than return it.'
: 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
}>
<Render promise={ReactNoopFlightClient.read(fn)} />
</ErrorBoundary>
<ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to Client Components.">
<Render promise={ReactNoopFlightClient.read(symbol)} />
</ErrorBoundary>
<ErrorBoundary expectedMessage="Refs cannot be used in Server Components, nor passed to Client Components.">
<Render promise={ReactNoopFlightClient.read(refs)} />
</ErrorBoundary>
<ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
<Render promise={ReactNoopFlightClient.read(eventClient)} />
</ErrorBoundary>
<ErrorBoundary
expectedMessage={
__DEV__
? 'Functions are not valid as a child of Client Components. This may happen if you return Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.'
: 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
}>
<Render promise={ReactNoopFlightClient.read(fnChildrenClient)} />
</ErrorBoundary>
<ErrorBoundary
expectedMessage={
'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
}>
<Render promise={ReactNoopFlightClient.read(fnClient)} />
</ErrorBoundary>
<ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to Client Components.">
<Render promise={ReactNoopFlightClient.read(symbolClient)} />
</ErrorBoundary>
<ErrorBoundary expectedMessage="Refs cannot be used in Server Components, nor passed to Client Components.">
<Render promise={ReactNoopFlightClient.read(refsClient)} />
</ErrorBoundary>
</>,
);
});
});
});
it('should emit descriptions of errors in dev', async () => {
const ClientErrorBoundary = clientReference(ErrorBoundary);
function Throw({value}) {
throw value;
}
function RenderInlined() {
const inlinedElement = {
$$typeof: Symbol.for('react.element'),
type: () => {},
key: null,
ref: null,
props: {},
_owner: null,
};
return inlinedElement;
}
const LazyInlined = React.lazy(async () => ({default: RenderInlined}));
const testCases = (
<>
<ClientErrorBoundary expectedMessage="This is a real Error.">
<Throw value={new TypeError('This is a real Error.')} />
</ClientErrorBoundary>
<ClientErrorBoundary expectedMessage="This is a string error.">
<Throw value="This is a string error." />
</ClientErrorBoundary>
<ClientErrorBoundary expectedMessage="{message: ..., extra: ..., nested: ...}">
<Throw
value={{
message: 'This is a long message',
extra: 'properties',
nested: {more: 'prop'},
}}
/>
</ClientErrorBoundary>
<ClientErrorBoundary
expectedMessage={'{message: "Short", extra: ..., nested: ...}'}>
<Throw
value={{
message: 'Short',
extra: 'properties',
nested: {more: 'prop'},
}}
/>
</ClientErrorBoundary>
<ClientErrorBoundary expectedMessage="Symbol(hello)">
<Throw value={Symbol('hello')} />
</ClientErrorBoundary>
<ClientErrorBoundary expectedMessage="123">
<Throw value={123} />
</ClientErrorBoundary>
<ClientErrorBoundary expectedMessage="undefined">
<Throw value={undefined} />
</ClientErrorBoundary>
<ClientErrorBoundary expectedMessage="<div/>">
<Throw value={<div />} />
</ClientErrorBoundary>
<ClientErrorBoundary expectedMessage="function Foo() {}">
<Throw value={function Foo() {}} />
</ClientErrorBoundary>
<ClientErrorBoundary expectedMessage={'["array"]'}>
<Throw value={['array']} />
</ClientErrorBoundary>
<ClientErrorBoundary
expectedMessage={
'A React Element from an older version of React was rendered. ' +
'This is not supported. It can happen if:\n' +
'- Multiple copies of the "react" package is used.\n' +
'- A library pre-bundled an old copy of "react" or "react/jsx-runtime".\n' +
'- A compiler tries to "inline" JSX instead of using the runtime.'
}>
<LazyInlined />
</ClientErrorBoundary>
</>
);
const transport = ReactNoopFlightServer.render(testCases, {
onError(x) {
if (__DEV__) {
return 'a dev digest';
}
if (x instanceof Error) {
return `digest("${x.message}")`;
} else if (Array.isArray(x)) {
return `digest([])`;
} else if (typeof x === 'object' && x !== null) {
return `digest({})`;
}
return `digest(${String(x)})`;
},
});
await act(() => {
startTransition(() => {
ReactNoop.render(ReactNoopFlightClient.read(transport));
});
});
});
it('should include server components in error boundary stacks in dev', async () => {
const ClientErrorBoundary = clientReference(ErrorBoundary);
function Throw({value}) {
throw value;
}
const expectedStack = __DEV__
? '\n in Throw' +
'\n in div' +
'\n in ErrorBoundary (at **)' +
'\n in App'
: '\n in div' + '\n in ErrorBoundary (at **)';
function App() {
return (
<ClientErrorBoundary
expectedMessage="This is a real Error."
expectedStack={expectedStack}>
<div>
<Throw value={new TypeError('This is a real Error.')} />
</div>
</ClientErrorBoundary>
);
}
const transport = ReactNoopFlightServer.render(<App />, {
onError(x) {
if (__DEV__) {
return 'a dev digest';
}
if (x instanceof Error) {
return `digest("${x.message}")`;
} else if (Array.isArray(x)) {
return `digest([])`;
} else if (typeof x === 'object' && x !== null) {
return `digest({})`;
}
return `digest(${String(x)})`;
},
});
await act(() => {
startTransition(() => {
ReactNoop.render(ReactNoopFlightClient.read(transport));
});
});
});
it('should handle serialization errors in element inside error boundary', async () => {
const ClientErrorBoundary = clientReference(ErrorBoundary);
const expectedStack = __DEV__
? '\n in div' + '\n in ErrorBoundary (at **)' + '\n in App'
: '\n in ErrorBoundary (at **)';
function App() {
return (
<ClientErrorBoundary
expectedMessage="Event handlers cannot be passed to Client Component props."
expectedStack={expectedStack}>
<div onClick={function () {}} />
</ClientErrorBoundary>
);
}
const transport = ReactNoopFlightServer.render(<App />, {
onError(x) {
if (__DEV__) {
return 'a dev digest';
}
if (x instanceof Error) {
return `digest("${x.message}")`;
} else if (Array.isArray(x)) {
return `digest([])`;
} else if (typeof x === 'object' && x !== null) {
return `digest({})`;
}
return `digest(${String(x)})`;
},
});
await act(() => {
startTransition(() => {
ReactNoop.render(ReactNoopFlightClient.read(transport));
});
});
});
it('should handle exotic stack frames', async () => {
function ServerComponent() {
const error = new Error('This is an error');
const originalStackLines = error.stack.split('\n');
error.stack = [
originalStackLines[0],
' at eval (eval at testFunction (inspected-page.html:29:11), <anonymous>:1:10)',
' at ServerComponentError (file://~/(some)(really)(exotic-directory)/ReactFlight-test.js:1166:19)',
' at file:///testing.js:42:3',
' at async file:///testing.js:42:3',
...originalStackLines.slice(2),
].join('\n');
throw error;
}
const findSourceMapURL = jest.fn(() => null);
const errors = [];
class MyErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
componentDidCatch(error, componentInfo) {
errors.push(error);
}
render() {
if (this.state.error) {
return null;
}
return this.props.children;
}
}
const ClientErrorBoundary = clientReference(MyErrorBoundary);
function App() {
return ReactServer.createElement(
ClientErrorBoundary,
null,
ReactServer.createElement(ServerComponent),
);
}
const transport = ReactNoopFlightServer.render(<App />, {
onError(x) {
if (__DEV__) {
return 'a dev digest';
}
if (x instanceof Error) {
return `digest("${x.message}")`;
} else if (Array.isArray(x)) {
return `digest([])`;
} else if (typeof x === 'object' && x !== null) {
return `digest({})`;
}
return `digest(${String(x)})`;
},
});
await act(() => {
startTransition(() => {
ReactNoop.render(
ReactNoopFlightClient.read(transport, {findSourceMapURL}),
);
});
});
if (__DEV__) {
expect({
errors: errors.map(getErrorForJestMatcher),
findSourceMapURLCalls: findSourceMapURL.mock.calls,
}).toEqual({
errors: [
{
message: 'This is an error',
stack: gate(flags => flags.enableOwnerStacks)
? expect.stringContaining(
'Error: This is an error\n' +
' at eval (eval at testFunction (eval at createFakeFunction (**), <anonymous>:1:35)\n' +
' at ServerComponentError (file://~/(some)(really)(exotic-directory)/ReactFlight-test.js:1166:19)\n' +
' at <anonymous> (file:///testing.js:42:3)\n' +
' at <anonymous> (file:///testing.js:42:3)\n',
)
: expect.stringContaining(
'Error: This is an error\n' +
' at eval (eval at testFunction (inspected-page.html:29:11), <anonymous>:1:10)\n' +
' at ServerComponentError (file://~/(some)(really)(exotic-directory)/ReactFlight-test.js:1166:19)\n' +
' at file:///testing.js:42:3\n' +
' at file:///testing.js:42:3',
),
digest: 'a dev digest',
environmentName: 'Server',
},
],
findSourceMapURLCalls: gate(flags => flags.enableOwnerStacks)
? [
[__filename, 'Server'],
[__filename, 'Server'],
['inspected-page.html:29:11), <anonymous>', 'Server'],
[
'file://~/(some)(really)(exotic-directory)/ReactFlight-test.js',
'Server',
],
['file:///testing.js', 'Server'],
[__filename, 'Server'],
]
: [],
});
} else {
expect(errors.map(getErrorForJestMatcher)).toEqual([
{
message:
'An error occurred in the Server Components render. The specific message is omitted in production' +
' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
' may provide additional details about the nature of the error.',
stack:
'Error: An error occurred in the Server Components render. The specific message is omitted in production' +
' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
' may provide additional details about the nature of the error.',
digest: 'digest("This is an error")',
},
]);
}
});
it('should include server components in warning stacks', async () => {
function Component() {
return <div>{[<span />]}</div>;
}
const ClientComponent = clientReference(Component);
function Indirection({children}) {
return children;
}
function App() {
return ReactServer.createElement(
Indirection,
null,
ReactServer.createElement(ClientComponent),
);
}
const transport = ReactNoopFlightServer.render(<App />);
await expect(async () => {
await act(() => {
startTransition(() => {
ReactNoop.render(ReactNoopFlightClient.read(transport));
});
});
}).toErrorDev(
'Each child in a list should have a unique "key" prop.\n' +
'\n' +
'Check the render method of `Component`. See https://react.dev/link/warning-keys for more information.\n' +
' in span (at **)\n' +
' in Component (at **)\n' +
(gate(flags => flags.enableOwnerStacks)
? ''
: ' in Indirection (at **)\n') +
' in App (at **)',
);
});
it('should trigger the inner most error boundary inside a Client Component', async () => {
function ServerComponent() {
throw new Error('This was thrown in the Server Component.');
}
function ClientComponent({children}) {
return (
<ErrorBoundary expectedMessage="This was thrown in the Server Component.">
<div>{children}</div>
</ErrorBoundary>
);
}
const ClientComponentReference = clientReference(ClientComponent);
function Server() {
return (
<ClientComponentReference>
<ServerComponent />
</ClientComponentReference>
);
}
const data = ReactNoopFlightServer.render(<Server />, {
onError(x) {
},
});
function Client({promise}) {
return use(promise);
}
await act(() => {
startTransition(() => {
ReactNoop.render(
<NoErrorExpected>
<Client promise={ReactNoopFlightClient.read(data)} />
</NoErrorExpected>,
);
});
});
});
it('should warn in DEV if a toJSON instance is passed to a host component', () => {
const obj = {
toJSON() {
return 123;
},
};
expect(() => {
const transport = ReactNoopFlightServer.render(<input value={obj} />);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with toJSON methods are not supported. ' +
'Convert it manually to a simple value before passing it to props.\n' +
' <input value={{toJSON: ...}}>\n' +
' ^^^^^^^^^^^^^^^',
{withoutStack: true},
);
});
it('should warn in DEV if a toJSON instance is passed to a host component child', () => {
class MyError extends Error {
toJSON() {
return 123;
}
}
expect(() => {
const transport = ReactNoopFlightServer.render(
<div>Womp womp: {new MyError('spaghetti')}</div>,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
' <div>Womp womp: {Error}</div>\n' +
' ^^^^^^^',
{withoutStack: true},
);
});
it('should warn in DEV if a special object is passed to a host component', () => {
expect(() => {
const transport = ReactNoopFlightServer.render(<input value={Math} />);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Math objects are not supported.\n' +
' <input value={Math}>\n' +
' ^^^^^^',
{withoutStack: true},
);
});
it('should warn in DEV if an object with symbols is passed to a host component', () => {
expect(() => {
const transport = ReactNoopFlightServer.render(
<input value={{[Symbol.iterator]: {}}} />,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with symbol properties like Symbol.iterator are not supported.',
{withoutStack: true},
);
});
it('should warn in DEV if a toJSON instance is passed to a Client Component', () => {
const obj = {
toJSON() {
return 123;
},
};
function ClientImpl({value}) {
return <div>{value}</div>;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(<Client value={obj} />);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with toJSON methods are not supported.',
{withoutStack: true},
);
});
it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => {
const obj = {
toJSON() {
return 123;
},
};
function ClientImpl({children}) {
return <div>{children}</div>;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(
<Client>Current date: {obj}</Client>,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with toJSON methods are not supported. ' +
'Convert it manually to a simple value before passing it to props.\n' +
' <>Current date: {{toJSON: ...}}</>\n' +
' ^^^^^^^^^^^^^^^',
{withoutStack: true},
);
});
it('should warn in DEV if a special object is passed to a Client Component', () => {
function ClientImpl({value}) {
return <div>{value}</div>;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(<Client value={Math} />);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Math objects are not supported.\n' +
' <... value={Math}>\n' +
' ^^^^^^',
{withoutStack: true},
);
});
it('should warn in DEV if an object with symbols is passed to a Client Component', () => {
function ClientImpl({value}) {
return <div>{value}</div>;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(
<Client value={{[Symbol.iterator]: {}}} />,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with symbol properties like Symbol.iterator are not supported.',
{withoutStack: true},
);
});
it('should warn in DEV if a special object is passed to a nested object in Client Component', () => {
function ClientImpl({value}) {
return <div>{value}</div>;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(
<Client value={{hello: Math, title: <h1>hi</h1>}} />,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Math objects are not supported.\n' +
' {hello: Math, title: <h1/>}\n' +
' ^^^^',
{withoutStack: true},
);
});
it('should warn in DEV if a special object is passed to a nested array in Client Component', () => {
function ClientImpl({value}) {
return <div>{value}</div>;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(
<Client
value={['looooong string takes up noise', Math, <h1>hi</h1>]}
/>,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Math objects are not supported.\n' +
' [..., Math, <h1/>]\n' +
' ^^^^',
{withoutStack: true},
);
});
it('should NOT warn in DEV for key getters', () => {
const transport = ReactNoopFlightServer.render(<div key="a" />);
ReactNoopFlightClient.read(transport);
});
it('should warn in DEV a child is missing keys on server component', () => {
function NoKey({children}) {
return ReactServer.createElement('div', {
key: "this has a key but parent doesn't",
});
}
expect(() => {
jest.resetModules();
jest.mock('react', () => ReactServer);
const transport = ReactNoopFlightServer.render(
ReactServer.createElement(
'div',
null,
Array(6).fill(ReactServer.createElement(NoKey)),
),
);
jest.resetModules();
jest.mock('react', () => React);
ReactNoopFlightClient.read(transport);
}).toErrorDev('Each child in a list should have a unique "key" prop.');
});
it('should warn in DEV a child is missing keys on a fragment', () => {
expect(() => {
jest.resetModules();
jest.mock('react', () => ReactServer);
const transport = ReactNoopFlightServer.render(
ReactServer.createElement(
'div',
null,
Array(6).fill(ReactServer.createElement(ReactServer.Fragment)),
),
);
jest.resetModules();
jest.mock('react', () => React);
ReactNoopFlightClient.read(transport);
}).toErrorDev('Each child in a list should have a unique "key" prop.');
});
it('should warn in DEV a child is missing keys in client component', async () => {
function ParentClient({children}) {
return children;
}
const Parent = clientReference(ParentClient);
await expect(async () => {
const transport = ReactNoopFlightServer.render(
<Parent>{Array(6).fill(<div>no key</div>)}</Parent>,
);
ReactNoopFlightClient.read(transport);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
}).toErrorDev(
gate(flags => flags.enableOwnerStacks)
? 'Each child in a list should have a unique "key" prop.' +
'\n\nCheck the top-level render call using <ParentClient>. ' +
'See https://react.dev/link/warning-keys for more information.'
: 'Each child in a list should have a unique "key" prop. ' +
'See https://react.dev/link/warning-keys for more information.',
);
});
it('should error if a class instance is passed to a host component', () => {
class Foo {
method() {}
}
const errors = [];
ReactNoopFlightServer.render(<input value={new Foo()} />, {
onError(x) {
errors.push(x.message);
},
});
expect(errors).toEqual([
'Only plain objects, and a few built-ins, can be passed to Client Components ' +
'from Server Components. Classes or null prototypes are not supported.' +
(__DEV__
? '\n' + ' <input value={{}}>\n' + ' ^^^^'
: '\n' + ' {value: {}}\n' + ' ^^'),
]);
});
it('should error if useContext is called()', () => {
function ServerComponent() {
return ReactServer.useContext();
}
const errors = [];
ReactNoopFlightServer.render(<ServerComponent />, {
onError(x) {
errors.push(x.message);
},
});
expect(errors).toEqual(['ReactServer.useContext is not a function']);
});
it('should error if a context without a client reference is passed to use()', () => {
const Context = React.createContext();
function ServerComponent() {
return ReactServer.use(Context);
}
const errors = [];
ReactNoopFlightServer.render(<ServerComponent />, {
onError(x) {
errors.push(x.message);
},
});
expect(errors).toEqual([
'Cannot read a Client Context from a Server Component.',
]);
});
it('should error if a client reference is passed to use()', () => {
const Context = React.createContext();
const ClientContext = clientReference(Context);
function ServerComponent() {
return ReactServer.use(ClientContext);
}
const errors = [];
ReactNoopFlightServer.render(<ServerComponent />, {
onError(x) {
errors.push(x.message);
},
});
expect(errors).toEqual([
'Cannot read a Client Context from a Server Component.',
]);
});
describe('Hooks', () => {
function DivWithId({children}) {
const id = ReactServer.useId();
return <div prop={id}>{children}</div>;
}
it('should support useId', async () => {
function App() {
return (
<>
<DivWithId />
<DivWithId />
</>
);
}
const transport = ReactNoopFlightServer.render(<App />);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div prop=":S1:" />
<div prop=":S2:" />
</>,
);
});
it('accepts an identifier prefix that prefixes generated ids', async () => {
function App() {
return (
<>
<DivWithId />
<DivWithId />
</>
);
}
const transport = ReactNoopFlightServer.render(<App />, {
identifierPrefix: 'foo',
});
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div prop=":fooS1:" />
<div prop=":fooS2:" />
</>,
);
});
it('[TODO] it does not warn if you render a server element passed to a client module reference twice on the client when using useId', async () => {
function App() {
const id = ReactServer.useId();
const div = <div prop={id}>{id}</div>;
return <ClientDoublerModuleRef el={div} />;
}
function ClientDoubler({el}) {
Scheduler.log('ClientDoubler');
return (
<>
{el}
{el}
</>
);
}
const ClientDoublerModuleRef = clientReference(ClientDoubler);
const transport = ReactNoopFlightServer.render(<App />);
assertLog([]);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
assertLog(['ClientDoubler']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<div prop=":S1:">:S1:</div>
<div prop=":S1:">:S1:</div>
</>,
);
});
});
it('errors when a tainted object is serialized', async () => {
function UserClient({user}) {
return <span>{user.name}</span>;
}
const User = clientReference(UserClient);
const user = {
name: 'Seb',
age: 'rather not say',
};
ReactServer.experimental_taintObjectReference(
"Don't pass the raw user object to the client",
user,
);
const errors = [];
ReactNoopFlightServer.render(<User user={user} />, {
onError(x) {
errors.push(x.message);
},
});
expect(errors).toEqual(["Don't pass the raw user object to the client"]);
});
it('errors with a specific message when a tainted function is serialized', async () => {
function UserClient({user}) {
return <span>{user.name}</span>;
}
const User = clientReference(UserClient);
function change() {}
ReactServer.experimental_taintObjectReference(
'A change handler cannot be passed to a client component',
change,
);
const errors = [];
ReactNoopFlightServer.render(<User onChange={change} />, {
onError(x) {
errors.push(x.message);
},
});
expect(errors).toEqual([
'A change handler cannot be passed to a client component',
]);
});
it('errors when a tainted string is serialized', async () => {
function UserClient({user}) {
return <span>{user.name}</span>;
}
const User = clientReference(UserClient);
const process = {
env: {
SECRET: '3e971ecc1485fe78625598bf9b6f85db',
},
};
ReactServer.experimental_taintUniqueValue(
'Cannot pass a secret token to the client',
process,
process.env.SECRET,
);
const errors = [];
ReactNoopFlightServer.render(<User token={process.env.SECRET} />, {
onError(x) {
errors.push(x.message);
},
});
expect(errors).toEqual(['Cannot pass a secret token to the client']);
expect(process.env.SECRET).toBe('3e971ecc1485fe78625598bf9b6f85db');
});
it('errors when a tainted bigint is serialized', async () => {
function UserClient({user}) {
return <span>{user.name}</span>;
}
const User = clientReference(UserClient);
const currentUser = {
name: 'Seb',
token: BigInt('0x3e971ecc1485fe78625598bf9b6f85dc'),
};
ReactServer.experimental_taintUniqueValue(
'Cannot pass a secret token to the client',
currentUser,
currentUser.token,
);
function App({user}) {
return <User token={user.token} />;
}
const errors = [];
ReactNoopFlightServer.render(<App user={currentUser} />, {
onError(x) {
errors.push(x.message);
},
});
expect(errors).toEqual(['Cannot pass a secret token to the client']);
});
it('errors when a tainted binary value is serialized', async () => {
function UserClient({user}) {
return <span>{user.name}</span>;
}
const User = clientReference(UserClient);
const currentUser = {
name: 'Seb',
token: new Uint32Array([0x3e971ecc, 0x1485fe78, 0x625598bf, 0x9b6f85dd]),
};
ReactServer.experimental_taintUniqueValue(
'Cannot pass a secret token to the client',
currentUser,
currentUser.token,
);
function App({user}) {
const clone = user.token.slice();
return <User token={clone} />;
}
const errors = [];
ReactNoopFlightServer.render(<App user={currentUser} />, {
onError(x) {
errors.push(x.message);
},
});
expect(errors).toEqual(['Cannot pass a secret token to the client']);
});
it('keep a tainted value tainted until the end of any pending requests', async () => {
function UserClient({user}) {
return <span>{user.name}</span>;
}
const User = clientReference(UserClient);
function getUser() {
const user = {
name: 'Seb',
token: '3e971ecc1485fe78625598bf9b6f85db',
};
ReactServer.experimental_taintUniqueValue(
'Cannot pass a secret token to the client',
user,
user.token,
);
return user;
}
function App() {
const user = getUser();
const derivedValue = {...user};
gc();
return <User user={derivedValue} />;
}
let errors = [];
ReactNoopFlightServer.render(<App />, {
onError(x) {
errors.push(x.message);
},
});
expect(errors).toEqual(['Cannot pass a secret token to the client']);
errors = [];
ReactNoopFlightServer.render(
<User user={{token: '3e971ecc1485fe78625598bf9b6f85db'}} />,
{
onError(x) {
errors.push(x.message);
},
},
);
expect(errors).toEqual([]);
});
it('preserves state when keying a server component', async () => {
function StatefulClient({name}) {
const [state] = React.useState(name.toLowerCase());
return state;
}
const Stateful = clientReference(StatefulClient);
function Item({item}) {
return (
<div>
{item}
<Stateful name={item} />
</div>
);
}
function Items({items}) {
return items.map(item => {
return <Item key={item} item={item} />;
});
}
const transport = ReactNoopFlightServer.render(
<Items items={['A', 'B', 'C']} />,
);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div>Aa</div>
<div>Bb</div>
<div>Cc</div>
</>,
);
const transport2 = ReactNoopFlightServer.render(
<Items items={['B', 'A', 'D', 'C']} />,
);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport2));
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div>Bb</div>
<div>Aa</div>
<div>Dd</div>
<div>Cc</div>
</>,
);
});
it('does not inherit keys of children inside a server component', async () => {
function StatefulClient({name, initial}) {
const [state] = React.useState(initial);
return state;
}
const Stateful = clientReference(StatefulClient);
function Item({item, initial}) {
return (
<div key={item}>
{item}
<Stateful name={item} initial={initial} />
</div>
);
}
function IndirectItem({item, initial}) {
return <Item key="parent" item={item} initial={initial} />;
}
const transport = ReactNoopFlightServer.render(
<>
<Item item="A" initial={1} />
<Item item="B" initial={2} />
<IndirectItem item="C" initial={5} />
<IndirectItem item="C" initial={6} />
</>,
);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div>A1</div>
<div>B2</div>
<div>C5</div>
<div>C6</div>
</>,
);
const transport2 = ReactNoopFlightServer.render(
<>
<Item item="B" initial={3} />
<Item item="A" initial={4} />
<IndirectItem item="C" initial={7} />
<IndirectItem item="C" initial={8} />
</>,
);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport2));
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div>B3</div>
<div>A4</div>
<div>C5</div>
<div>C6</div>
</>,
);
});
it('shares state between single return and array return in a parent', async () => {
function StatefulClient({name, initial}) {
const [state] = React.useState(initial);
return state;
}
const Stateful = clientReference(StatefulClient);
function Item({item, initial}) {
return (
<span key={item}>
{item}
<Stateful name={item} initial={initial} />
</span>
);
}
function Condition({condition}) {
if (condition) {
return <Item item="A" initial={1} />;
}
return (
<>
<Item item="A" initial={2} />
<Item item="B" initial={3} />
</>
);
}
function ConditionPlain({condition}) {
if (condition) {
return (
<span>
C
<Stateful name="C" initial={1} />
</span>
);
}
return (
<>
<span>
C
<Stateful name="C" initial={2} />
</span>
<span>
D
<Stateful name="D" initial={3} />
</span>
</>
);
}
const transport = ReactNoopFlightServer.render(
<>
<div>
<Condition condition={true} />
</div>
<div>
<ConditionPlain condition={true} />
</div>
<div key="keyed">
<ConditionPlain condition={true} />
</div>
</>,
);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div>
<span>A1</span>
</div>
<div>
<span>C1</span>
</div>
<div>
<span>C1</span>
</div>
</>,
);
const transport2 = ReactNoopFlightServer.render(
<>
<div>
<Condition condition={false} />
</div>
<div>
<ConditionPlain condition={false} />
</div>
{null}
<div key="keyed">
<ConditionPlain condition={false} />
</div>
</>,
);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport2));
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div>
<span>A2{/* This should be A1 ideally */}</span>
<span>B3</span>
</div>
<div>
<span>C1</span>
<span>D3</span>
</div>
<div>
<span>C1</span>
<span>D3</span>
</div>
</>,
);
});
it('shares state between single return and array return in a set', async () => {
function StatefulClient({name, initial}) {
const [state] = React.useState(initial);
return state;
}
const Stateful = clientReference(StatefulClient);
function Item({item, initial}) {
return (
<span key={item}>
{item}
<Stateful name={item} initial={initial} />
</span>
);
}
function Condition({condition}) {
if (condition) {
return <Item item="A" initial={1} />;
}
return (
<>
<Item item="A" initial={2} />
<Item item="B" initial={3} />
</>
);
}
function ConditionPlain({condition}) {
if (condition) {
return (
<span>
C
<Stateful name="C" initial={1} />
</span>
);
}
return (
<>
<span>
C
<Stateful name="C" initial={2} />
</span>
<span>
D
<Stateful name="D" initial={3} />
</span>
</>
);
}
const transport = ReactNoopFlightServer.render(
<div>
<Condition condition={true} />
<ConditionPlain condition={true} />
<ConditionPlain key="keyed" condition={true} />
</div>,
);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>A1</span>
<span>C1</span>
<span>C1</span>
</div>,
);
const transport2 = ReactNoopFlightServer.render(
<div>
<Condition condition={false} />
<ConditionPlain condition={false} />
{null}
<ConditionPlain key="keyed" condition={false} />
</div>,
);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport2));
});
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>A2{/* Should be A1 */}</span>
<span>B3</span>
<span>C2{/* Should be C1 */}</span>
<span>D3</span>
<span>C2{/* Should be C1 */}</span>
<span>D3</span>
</div>,
);
});
it('preserves state with keys split across async work', async () => {
let resolve;
const promise = new Promise(r => (resolve = r));
function StatefulClient({name}) {
const [state] = React.useState(name.toLowerCase());
return state;
}
const Stateful = clientReference(StatefulClient);
function Item({name}) {
if (name === 'A') {
return promise.then(() => (
<div>
{name}
<Stateful name={name} />
</div>
));
}
return (
<div>
{name}
<Stateful name={name} />
</div>
);
}
const transport = ReactNoopFlightServer.render([
<Item key="a" name="A" />,
null,
]);
await resolve();
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(<div>Aa</div>);
const transport2 = ReactNoopFlightServer.render([
null,
<Item key="a" name="B" />,
]);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport2));
});
expect(ReactNoop).toMatchRenderedOutput(<div>Ba</div>);
});
it('shares state when moving keyed Server Components that render fragments', async () => {
function StatefulClient({name, initial}) {
const [state] = React.useState(initial);
return <span>{state}</span>;
}
const Stateful = clientReference(StatefulClient);
function ServerComponent({item, initial}) {
return [
<Stateful key="a" initial={'a' + initial} />,
<Stateful key="b" initial={'b' + initial} />,
];
}
const transport = ReactNoopFlightServer.render(
<div>
<ServerComponent key="A" initial={1} />
<ServerComponent key="B" initial={2} />
</div>,
);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>a1</span>
<span>b1</span>
<span>a2</span>
<span>b2</span>
</div>,
);
const transport2 = ReactNoopFlightServer.render(
<div>
<ServerComponent key="B" initial={4} />
<ServerComponent key="A" initial={3} />
</div>,
);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport2));
});
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>a2</span>
<span>b2</span>
<span>a1</span>
<span>b1</span>
</div>,
);
});
it('shares state when moving keyed Server Components that render async iterables', async () => {
function StatefulClient({name, initial}) {
const [state] = React.useState(initial);
return <span>{state}</span>;
}
const Stateful = clientReference(StatefulClient);
async function* ServerComponent({item, initial}) {
yield <Stateful key="a" initial={'a' + initial} />;
yield <Stateful key="b" initial={'b' + initial} />;
}
const transport = ReactNoopFlightServer.render(
<div>
<ServerComponent key="A" initial={1} />
<ServerComponent key="B" initial={2} />
</div>,
);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>a1</span>
<span>b1</span>
<span>a2</span>
<span>b2</span>
</div>,
);
const transport2 = ReactNoopFlightServer.render(
<div>
<ServerComponent key="B" initial={4} />
<ServerComponent key="A" initial={3} />
</div>,
);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport2));
});
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>a2</span>
<span>b2</span>
<span>a1</span>
<span>b1</span>
</div>,
);
});
it('preserves debug info for server-to-server pass through', async () => {
function ThirdPartyLazyComponent() {
return <span>!</span>;
}
const lazy = React.lazy(async function myLazy() {
return {
default: <ThirdPartyLazyComponent />,
};
});
function ThirdPartyComponent() {
return <span>stranger</span>;
}
function ThirdPartyFragmentComponent() {
return [<span key="1">Who</span>, ' ', <span key="2">dis?</span>];
}
function ServerComponent({transport}) {
const children = ReactNoopFlightClient.read(transport);
return <div>Hello, {children}</div>;
}
const promiseComponent = Promise.resolve(<ThirdPartyComponent />);
const thirdPartyTransport = ReactNoopFlightServer.render(
[promiseComponent, lazy, <ThirdPartyFragmentComponent key="3" />],
{
environmentName: 'third-party',
},
);
await 0;
const transport = ReactNoopFlightServer.render(
<ServerComponent transport={thirdPartyTransport} />,
);
await act(async () => {
const promise = ReactNoopFlightClient.read(transport);
expect(getDebugInfo(promise)).toEqual(
__DEV__
? [
{
name: 'ServerComponent',
env: 'Server',
key: null,
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
props: {
transport: expect.arrayContaining([]),
},
},
]
: undefined,
);
const result = await promise;
const thirdPartyChildren = await result.props.children[1];
expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
__DEV__
? [
{
name: 'ThirdPartyComponent',
env: 'third-party',
key: null,
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
props: {},
},
]
: undefined,
);
expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
__DEV__
? [
{
name: 'ThirdPartyLazyComponent',
env: 'third-party',
key: null,
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in myLazy (at **)\n in lazyInitializer (at **)'
: undefined,
props: {},
},
]
: undefined,
);
expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
__DEV__
? [
{
name: 'ThirdPartyFragmentComponent',
env: 'third-party',
key: '3',
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
props: {},
},
]
: undefined,
);
ReactNoop.render(result);
});
expect(ReactNoop).toMatchRenderedOutput(
<div>
Hello, <span>stranger</span>
<span>!</span>
<span>Who</span> <span>dis?</span>
</div>,
);
});
it('preserves debug info for server-to-server pass through of async iterables', async () => {
let resolve;
const iteratorPromise = new Promise(r => (resolve = r));
async function* ThirdPartyAsyncIterableComponent({item, initial}) {
yield <span key="1">Who</span>;
yield <span key="2">dis?</span>;
resolve();
}
function Keyed({children}) {
return children;
}
function ServerComponent({transport}) {
const children = ReactServer.use(
ReactNoopFlightClient.read(transport),
).root;
return (
<div>
<Keyed key="keyed">{children}</Keyed>
</div>
);
}
const thirdPartyTransport = ReactNoopFlightServer.render(
{root: <ThirdPartyAsyncIterableComponent />},
{
environmentName: 'third-party',
},
);
if (gate(flag => flag.enableFlightReadableStream)) {
await iteratorPromise;
}
await 0;
const transport = ReactNoopFlightServer.render(
<ServerComponent transport={thirdPartyTransport} />,
);
await act(async () => {
const promise = ReactNoopFlightClient.read(transport);
expect(getDebugInfo(promise)).toEqual(
__DEV__
? [
{
name: 'ServerComponent',
env: 'Server',
key: null,
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
props: {
transport: expect.arrayContaining([]),
},
},
]
: undefined,
);
const result = await promise;
const thirdPartyFragment = await result.props.children;
expect(getDebugInfo(thirdPartyFragment)).toEqual(
__DEV__
? [
{
name: 'Keyed',
env: 'Server',
key: 'keyed',
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in ServerComponent (at **)'
: undefined,
props: {
children: {},
},
},
]
: undefined,
);
expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual(
__DEV__
? [
{
name: 'ThirdPartyAsyncIterableComponent',
env: 'third-party',
key: null,
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
props: {},
},
]
: undefined,
);
ReactNoop.render(result);
});
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>Who</span>
<span>dis?</span>
</div>,
);
});
it('preserves error stacks passed through server-to-server with source maps', async () => {
async function ServerComponent({transport}) {
const thirdParty = ReactServer.use(
ReactNoopFlightClient.read(transport, {
findSourceMapURL(url) {
return 'source-map://' + url;
},
}),
);
await thirdParty.model;
return 'Should never render';
}
async function bar() {
throw new Error('third-party-error');
}
async function foo() {
await bar();
}
const rejectedPromise = foo();
const thirdPartyTransport = ReactNoopFlightServer.render(
{model: rejectedPromise},
{
environmentName: 'third-party',
onError(x) {
if (__DEV__) {
return 'a dev digest';
}
return `digest("${x.message}")`;
},
},
);
let originalError;
try {
await rejectedPromise;
} catch (x) {
originalError = x;
}
expect(originalError.message).toBe('third-party-error');
const transport = ReactNoopFlightServer.render(
<ServerComponent transport={thirdPartyTransport} />,
{
onError(x) {
if (__DEV__) {
return 'a dev digest';
}
return x.digest;
},
},
);
await 0;
await 0;
await 0;
const expectedErrorStack = originalError.stack
.split('\n')
.slice(0, 4)
.join('\n')
.replaceAll(
' (/',
gate(flags => flags.enableOwnerStacks) ? ' (file:///' : ' (/',
);
let sawReactPrefix = false;
const environments = [];
await act(async () => {
ReactNoop.render(
<ErrorBoundary
expectedMessage="third-party-error"
expectedEnviromentName="third-party"
expectedErrorStack={expectedErrorStack}>
{ReactNoopFlightClient.read(transport, {
findSourceMapURL(url, environmentName) {
if (url.startsWith('rsc://React/')) {
// We don't expect to see any React prefixed URLs here.
sawReactPrefix = true;
}
environments.push(environmentName);
// My not giving a source map, we should leave it intact.
return null;
},
})}
</ErrorBoundary>,
);
});
expect(sawReactPrefix).toBe(false);
if (__DEV__ && gate(flags => flags.enableOwnerStacks)) {
expect(environments.slice(0, 4)).toEqual([
'Server',
'third-party',
'third-party',
'third-party',
]);
} else {
expect(environments).toEqual([]);
}
});
it('can change the environment name inside a component', async () => {
let env = 'A';
function Component(props) {
env = 'B';
return <div>hi</div>;
}
const transport = ReactNoopFlightServer.render(
{
greeting: <Component />,
},
{
environmentName() {
return env;
},
},
);
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport);
const greeting = rootModel.greeting;
expect(getDebugInfo(greeting)).toEqual(
__DEV__
? [
{
name: 'Component',
env: 'A',
key: null,
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
props: {},
},
{
env: 'B',
},
]
: undefined,
);
ReactNoop.render(greeting);
});
expect(ReactNoop).toMatchRenderedOutput(<div>hi</div>);
});
it('replays logs, but not onError logs', async () => {
function foo() {
return 'hello';
}
function ServerComponent() {
console.log('hi', {
prop: 123,
fn: foo,
map: new Map([['foo', foo]]),
promise: new Promise(() => {}),
});
throw new Error('err');
}
function App() {
return ReactServer.createElement(ServerComponent);
}
let ownerStacks = [];
const mockConsoleLog = spyOnDevAndProd(console, 'log').mockImplementation(
() => {
ownerStacks.push(normalizeCodeLocInfo(ReactServer.captureOwnerStack()));
},
);
let transport;
expect(() => {
jest.resetModules();
jest.mock('react', () => require('react/react.react-server'));
ReactServer = require('react');
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
transport = ReactNoopFlightServer.render({
root: ReactServer.createElement(App),
});
}).toErrorDev('err');
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
expect(mockConsoleLog.mock.calls[0][1].prop).toBe(123);
expect(ownerStacks).toEqual(['\n in App (at **)']);
mockConsoleLog.mockClear();
mockConsoleLog.mockImplementation(() => {
ownerStacks.push(normalizeCodeLocInfo(React.captureOwnerStack()));
});
ownerStacks = [];
await ReactNoopFlightClient.read(transport);
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
expect(mockConsoleLog.mock.calls[0][1].prop).toBe(123);
const loggedFn = mockConsoleLog.mock.calls[0][1].fn;
expect(typeof loggedFn).toBe('function');
expect(loggedFn).not.toBe(foo);
expect(loggedFn.toString()).toBe(foo.toString());
const loggedMap = mockConsoleLog.mock.calls[0][1].map;
expect(loggedMap instanceof Map).toBe(true);
const loggedFn2 = loggedMap.get('foo');
expect(typeof loggedFn2).toBe('function');
expect(loggedFn2).not.toBe(foo);
expect(loggedFn2.toString()).toBe(foo.toString());
const promise = mockConsoleLog.mock.calls[0][1].promise;
expect(promise).toBeInstanceOf(Promise);
expect(ownerStacks).toEqual(['\n in App (at **)']);
});
it('uses the server component debug info as the element owner in DEV', async () => {
function Container({children}) {
return children;
}
function Greeting({firstName}) {
return ReactServer.createElement(
Container,
null,
ReactServer.createElement('span', null, 'Hello, ', firstName),
);
}
const model = {
greeting: ReactServer.createElement(Greeting, {firstName: 'Seb'}),
};
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport);
const greeting = rootModel.greeting;
expect(greeting.type).toBe('span');
if (__DEV__) {
const greetInfo = {
name: 'Greeting',
env: 'Server',
key: null,
owner: null,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Object.<anonymous> (at **)'
: undefined,
props: {
firstName: 'Seb',
},
};
expect(getDebugInfo(greeting)).toEqual([
greetInfo,
{
name: 'Container',
env: 'Server',
key: null,
owner: greetInfo,
stack: gate(flag => flag.enableOwnerStacks)
? ' in Greeting (at **)'
: undefined,
props: {
children: expect.objectContaining({
type: 'span',
props: {
children: ['Hello, ', 'Seb'],
},
}),
},
},
]);
expect(greeting._owner).toBe(greeting._debugInfo[0]);
} else {
expect(greeting._debugInfo).toBe(undefined);
expect(greeting._owner).toBe(
gate(flags => flags.disableStringRefs) ? undefined : null,
);
}
ReactNoop.render(greeting);
});
expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb</span>);
});
it('can get the component owner stacks during rendering in dev', () => {
let stack;
function Foo() {
return ReactServer.createElement(Bar, null);
}
function Bar() {
return ReactServer.createElement(
'div',
null,
ReactServer.createElement(Baz, null),
);
}
function Baz() {
stack = ReactServer.captureOwnerStack();
return ReactServer.createElement('span', null, 'hi');
}
ReactNoopFlightServer.render(
ReactServer.createElement(
'div',
null,
ReactServer.createElement(Foo, null),
),
);
expect(normalizeCodeLocInfo(stack)).toBe(
'\n in Bar (at **)' + '\n in Foo (at **)',
);
});
it('can track owner for a flight response created in another render', async () => {
jest.resetModules();
jest.mock('react', () => ReactServer);
const ReactNoopFlightClienOnTheServer = require('react-noop-renderer/flight-client');
jest.resetModules();
jest.mock('react', () => React);
let stack;
function Component() {
stack = ReactServer.captureOwnerStack();
return ReactServer.createElement('span', null, 'hi');
}
const ClientComponent = clientReference(Component);
function ThirdPartyComponent() {
return ReactServer.createElement(ClientComponent);
}
const thirdPartyTransport = ReactNoopFlightServer.render(
{children: ReactServer.createElement(ThirdPartyComponent)},
{
environmentName: 'third-party',
},
);
async function fetchThirdParty() {
return ReactNoopFlightClienOnTheServer.read(thirdPartyTransport);
}
async function FirstPartyComponent() {
const thirdParty = await fetchThirdParty();
return thirdParty.children;
}
function App() {
return ReactServer.createElement(FirstPartyComponent);
}
const transport = ReactNoopFlightServer.render(
ReactServer.createElement(App),
);
await act(async () => {
const root = await ReactNoopFlightClient.read(transport);
ReactNoop.render(root);
});
expect(normalizeCodeLocInfo(stack)).toBe(
'\n in ThirdPartyComponent (at **)' +
'\n in createResponse (at **)' +
'\n in read (at **)' +
'\n in fetchThirdParty (at **)' +
'\n in FirstPartyComponent (at **)' +
'\n in App (at **)',
);
});
it('can get the component owner stacks for onError in dev', async () => {
const thrownError = new Error('hi');
let caughtError;
let ownerStack;
function Foo() {
return ReactServer.createElement(Bar, null);
}
function Bar() {
return ReactServer.createElement(
'div',
null,
ReactServer.createElement(Baz, null),
);
}
function Baz() {
throw thrownError;
}
ReactNoopFlightServer.render(
ReactServer.createElement(
'div',
null,
ReactServer.createElement(Foo, null),
),
{
onError(error, errorInfo) {
caughtError = error;
ownerStack = ReactServer.captureOwnerStack
? ReactServer.captureOwnerStack()
: null;
},
},
);
expect(caughtError).toBe(thrownError);
expect(normalizeCodeLocInfo(ownerStack)).toBe(
'\n in Bar (at **)' + '\n in Foo (at **)',
);
});
it('should include only one component stack in replayed logs (if DevTools or polyfill adds them)', () => {
class MyError extends Error {
toJSON() {
return 123;
}
}
function Foo() {
return ReactServer.createElement('div', null, [
'Womp womp: ',
new MyError('spaghetti'),
]);
}
function Bar() {
const array = [];
array.push(ReactServer.createElement(Foo));
return ReactServer.createElement('div', null, array);
}
function App() {
return ReactServer.createElement(Bar);
}
jest.resetModules();
jest.mock('react', () => ReactServer);
const transport = ReactNoopFlightServer.render(
ReactServer.createElement(App),
);
assertConsoleErrorDev([
'Each child in a list should have a unique "key" prop.' +
' See https://react.dev/link/warning-keys for more information.\n' +
' in Bar (at **)\n' +
' in App (at **)',
'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
' <div>Womp womp: {Error}</div>\n' +
' ^^^^^^^\n' +
' in Foo (at **)\n' +
' in Bar (at **)\n' +
' in App (at **)',
]);
jest.resetModules();
jest.mock('react', () => React);
ReactNoopFlightClient.read(transport);
assertConsoleErrorDev([
'Each child in a list should have a unique "key" prop.' +
' See https://react.dev/link/warning-keys for more information.\n' +
' in Bar (at **)\n' +
' in App (at **)',
'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
' <div>Womp womp: {Error}</div>\n' +
' ^^^^^^^\n' +
' in Foo (at **)\n' +
' in Bar (at **)\n' +
' in App (at **)',
]);
});
it('can filter out stack frames of a serialized error in dev', async () => {
async function bar() {
throw new Error('my-error');
}
async function intermediate() {
await bar();
}
async function foo() {
await intermediate();
}
const rejectedPromise = foo();
const transport = ReactNoopFlightServer.render(
{model: rejectedPromise},
{
onError(x) {
return `digest("${x.message}")`;
},
filterStackFrame(url, functionName) {
return functionName !== 'intermediate';
},
},
);
let originalError;
try {
await rejectedPromise;
} catch (x) {
originalError = x;
}
const root = await ReactNoopFlightClient.read(transport);
let caughtError;
try {
await root.model;
} catch (x) {
caughtError = x;
}
if (__DEV__) {
expect(caughtError.message).toBe(originalError.message);
expect(normalizeCodeLocInfo(caughtError.stack)).toContain(
'\n in bar (at **)' + '\n in foo (at **)',
);
}
expect(normalizeCodeLocInfo(originalError.stack)).toContain(
'\n in bar (at **)' +
'\n in intermediate (at **)' +
'\n in foo (at **)',
);
expect(caughtError.digest).toBe('digest("my-error")');
});
});