/*** 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* @jest-environment node*/'use strict';
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;
describe('ReactFlight', () => {
beforeEach(() => {
jest.resetModules();
jest.mock('react', () => require('react/react.shared-subset'));
ReactServer = require('react');
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
// This stores the state so we need to preserve it
const flightModules = require('react-noop-renderer/flight-modules');
__unmockReact();
jest.resetModules();
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;
ErrorBoundary = class extends React.Component {
state = {hasError: false, error: null};
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};}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');
} 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.',
);expect(this.state.error.digest).toContain(this.props.expectedMessage);
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 createServerContext(globalName, defaultValue, withStack) {
let ctx;
expect(() => {
ctx = React.createServerContext(globalName, defaultValue);
}).toErrorDev(
'Server Context is deprecated and will soon be removed. ' +
'It was never documented and we have found it not to be useful ' +
'enough to warrant the downside it imposes on all apps.',
{withoutStack: !withStack},
);return ctx;
}function createServerServerContext(globalName, defaultValue, withStack) {
let ctx;
expect(() => {
ctx = ReactServer.createServerContext(globalName, defaultValue);
}).toErrorDev(
'Server Context is deprecated and will soon be removed. ' +
'It was never documented and we have found it not to be useful ' +
'enough to warrant the downside it imposes on all apps.',
{withoutStack: !withStack},
);return ctx;
}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;
ReactNoop.render(greeting);
});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 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);
});// @gate FIXME
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(
// already checked -0 with expects above'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: truesize: 2greet: worldcontent: [["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: truesize: 2hi: truecontent: ["hi",{"obj":"key"}]
selected: true`);});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 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 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>{() => {}}</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 FunctionPropClient() {return <Client>{() => {}}</Client>;}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 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><ErrorBoundaryexpectedMessage={'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><ErrorBoundaryexpectedMessage={'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 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}) {// This should catch the error thrown by the Server Component, even though it has already happened.
// We currently need to wrap it in a div because as it's set up right now, a lazy reference will// throw during reconciliation which will trigger the parent of the error boundary.
// This is similar to how these will suspend the parent if it's a direct child of a Suspense boundary.
// That's a bug.
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) {// ignore},});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: function}}>\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: function}}</>\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(
<Clientvalue={['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 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.',]);
});it('should warn in DEV if a a client reference is passed to useContext()', () => {const Context = React.createContext();
const ClientContext = clientReference(Context);function ServerComponent() {return ReactServer.useContext(ClientContext);
}expect(() => {const transport = ReactNoopFlightServer.render(<ServerComponent />);
ReactNoopFlightClient.read(transport);
}).toErrorDev('Cannot read a Client Context from a Server Component.', {
withoutStack: true,});});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 () => {
// @TODO Today if you render a Server Component with useId and pass it to a Client Component and that Client Component renders the element in two or more// places the id used on the server will be duplicated in the client. This is a deviation from the guarantees useId makes for Fizz/Client and is a consequence
// of the fact that the Server Component is actually rendered on the server and is reduced to a set of host elements before being passed to the Client component// so the output passed to the Client has no knowledge of the useId use. In the future we would like to add a DEV warning when this happens. For now
// we just accept that it is a nuance of useId in Flightfunction 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></>,);});});describe('ServerContext', () => {// @gate enableServerContextit('supports basic createServerContext usage', async () => {const ServerContext = createServerServerContext('ServerContext','hello from server',);function Foo() {const context = ReactServer.useContext(ServerContext);
return <div>{context}</div>;}const transport = ReactNoopFlightServer.render(<Foo />);
await act(async () => {ReactNoop.render(await ReactNoopFlightClient.read(transport));
});expect(ReactNoop).toMatchRenderedOutput(<div>hello from server</div>);
});// @gate enableServerContextit('propagates ServerContext providers in flight', async () => {const ServerContext = createServerServerContext('ServerContext','default',);function Foo() {return (<div><ServerContext.Provider value="hi this is server">
<Bar /></ServerContext.Provider>
</div>);}function Bar() {const context = ReactServer.useContext(ServerContext);
return context;}const transport = ReactNoopFlightServer.render(<Foo />);
await act(async () => {ReactNoop.render(await ReactNoopFlightClient.read(transport));
});expect(ReactNoop).toMatchRenderedOutput(<div>hi this is server</div>);
});// @gate enableServerContextit('errors if you try passing JSX through ServerContext value', () => {const ServerContext = createServerServerContext('ServerContext', {foo: {bar: <span>hi this is default</span>,},});function Foo() {return (<div><ServerContext.Provider
value={{foo: {bar: <span>hi this is server</span>,},}}><Bar /></ServerContext.Provider>
</div>);}function Bar() {const context = ReactServer.useContext(ServerContext);
return context.foo.bar;
}expect(() => {ReactNoopFlightServer.render(<Foo />);
}).toErrorDev('React elements are not allowed in ServerContext', {
withoutStack: true,});});// @gate enableServerContextit('propagates ServerContext and cleans up the providers in flight', async () => {const ServerContext = createServerServerContext('ServerContext','default',);function Foo() {return (<><ServerContext.Provider value="hi this is server outer">
<ServerContext.Provider value="hi this is server">
<Bar /></ServerContext.Provider>
<ServerContext.Provider value="hi this is server2">
<Bar /></ServerContext.Provider>
<Bar /></ServerContext.Provider>
<ServerContext.Provider value="hi this is server outer2">
<Bar /></ServerContext.Provider>
<Bar /></>);}function Bar() {const context = ReactServer.useContext(ServerContext);
return <span>{context}</span>;}const transport = ReactNoopFlightServer.render(<Foo />);
await act(async () => {ReactNoop.render(await ReactNoopFlightClient.read(transport));
});expect(ReactNoop).toMatchRenderedOutput(
<><span>hi this is server</span><span>hi this is server2</span><span>hi this is server outer</span><span>hi this is server outer2</span><span>default</span></>,);});// @gate enableServerContextit('propagates ServerContext providers in flight after suspending', async () => {const ServerContext = createServerServerContext('ServerContext','default',);function Foo() {return (<div><ServerContext.Provider value="hi this is server">
<React.Suspense fallback={'Loading'}>
<Bar /></React.Suspense>
</ServerContext.Provider>
</div>);}let resolve;const promise = new Promise(res => {resolve = () => {promise.unsuspend = true;
res();};});function Bar() {if (!promise.unsuspend) {
Scheduler.log('suspended');
throw promise;}Scheduler.log('rendered');
const context = ReactServer.useContext(ServerContext);
return context;}const transport = ReactNoopFlightServer.render(<Foo />);
assertLog(['suspended']);
await act(async () => {resolve();await promise;jest.runAllImmediates();
});assertLog(['rendered']);
await act(async () => {ReactNoop.render(await ReactNoopFlightClient.read(transport));
});expect(ReactNoop).toMatchRenderedOutput(<div>hi this is server</div>);
});// @gate enableServerContextit('serializes ServerContext to client', async () => {const ServerContext = createServerServerContext('ServerContext','default',);const ClientContext = createServerContext('ServerContext', 'default');function ClientBar() {Scheduler.log('ClientBar');
const context = React.useContext(ClientContext);
return <span>{context}</span>;}const Bar = clientReference(ClientBar);function Foo() {return (<ServerContext.Provider value="hi this is server">
<Bar /></ServerContext.Provider>
);}const model = {foo: <Foo />,};const transport = ReactNoopFlightServer.render(model);
assertLog([]);
await act(async () => {const flightModel = await ReactNoopFlightClient.read(transport);
ReactNoop.render(flightModel.foo);
});assertLog(['ClientBar']);
expect(ReactNoop).toMatchRenderedOutput(<span>hi this is server</span>);
expect(() => {createServerContext('ServerContext', 'default');}).toThrow('ServerContext: ServerContext already defined');
});// @gate enableServerContextit('takes ServerContext from the client for refetching use cases', async () => {const ServerContext = createServerServerContext('ServerContext','default',);function Bar() {return <span>{ReactServer.useContext(ServerContext)}</span>;
}const transport = ReactNoopFlightServer.render(<Bar />, {
context: [['ServerContext', 'Override']],
});await act(async () => {const flightModel = await ReactNoopFlightClient.read(transport);
ReactNoop.render(flightModel);
});expect(ReactNoop).toMatchRenderedOutput(<span>Override</span>);
});// @gate enableServerContextit('sets default initial value when defined lazily on server or client', async () => {let ServerContext;function inlineLazyServerContextInitialization() {if (!ServerContext) {ServerContext = createServerServerContext('ServerContext', 'default');}return ServerContext;}let ClientContext;function inlineContextInitialization() {if (!ClientContext) {ClientContext = createServerContext('ServerContext', 'default', true);}return ClientContext;}function ClientBaz() {const context = inlineContextInitialization();const value = React.useContext(context);
return <div>{value}</div>;}const Baz = clientReference(ClientBaz);function Bar() {return (<article><div>{ReactServer.useContext(inlineLazyServerContextInitialization())}
</div><Baz /></article>);}function ServerApp() {const Context = inlineLazyServerContextInitialization();return (<><Context.Provider value="test">
<Bar /></Context.Provider>
<Bar /></>);}function ClientApp({serverModel}) {return (<>{serverModel}<ClientBaz /></>);}const transport = ReactNoopFlightServer.render(<ServerApp />);
expect(ClientContext).toBe(undefined);
// Reset all modules, except flight-modules which keeps the registry of Client Componentsconst flightModules = require('react-noop-renderer/flight-modules');jest.resetModules();
jest.mock('react', () => require('react/react.shared-subset'));
jest.mock('react-noop-renderer/flight-modules', () => flightModules);
ReactServer = require('react');ReactNoopFlightServer = require('react-noop-renderer/flight-server');__unmockReact();jest.resetModules();
jest.mock('react-noop-renderer/flight-modules', () => flightModules);
React = require('react');ReactNoop = require('react-noop-renderer');ReactNoopFlightClient = require('react-noop-renderer/flight-client');act = require('internal-test-utils').act;
Scheduler = require('scheduler');await act(async () => {const serverModel = await ReactNoopFlightClient.read(transport);
ReactNoop.render(<ClientApp serverModel={serverModel} />);
});expect(ClientContext).not.toBe(ServerContext);
expect(ReactNoop).toMatchRenderedOutput(
<><article><div>test</div><div>test</div></article><article><div>default</div><div>default</div></article><div>default</div></>,);});});// @gate enableTaint
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"]);
});// @gate enableTaint
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',]);
});// @gate enableTaint
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']);
// This just ensures the process object is kept alive for the life time of
// the test since we're simulating a global as an example.
expect(process.env.SECRET).toBe('3e971ecc1485fe78625598bf9b6f85db');
});
// @gate enableTaint
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']);
});// @gate enableTaint && enableBinaryFlight
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']);
});// @gate enableTaint
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};
// A garbage collection can happen at any time. Even before the end of
// this request. This would clean up the user object.
gc();
// We should still block the tainted value.
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']);
// After the previous requests finishes, the token can be rendered again.
errors = [];
ReactNoopFlightServer.render(
<User user={{token: '3e971ecc1485fe78625598bf9b6f85db'}} />,
{onError(x) {
errors.push(x.message);
},},);expect(errors).toEqual([]);
});});