'use strict';
import {insertNodesAndExecuteScripts} from 'react-dom/src/test-utils/FizzTestUtils';
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
global.Blob.prototype.stream = function () {
const impl = Object.getOwnPropertySymbols(this)[0];
const buffer = this[impl]._buffer;
return new ReadableStream({
start(c) {
c.enqueue(new Uint8Array(buffer));
c.close();
},
});
};
global.Blob.prototype.text = async function () {
const impl = Object.getOwnPropertySymbols(this)[0];
return this[impl]._buffer.toString('utf8');
};
global.setTimeout = cb => cb();
let container;
let clientExports;
let serverExports;
let webpackMap;
let webpackServerMap;
let React;
let ReactDOMServer;
let ReactServerDOMServer;
let ReactServerDOMClient;
let ReactDOMClient;
let useActionState;
let act;
describe('ReactFlightDOMForm', () => {
beforeEach(() => {
jest.resetModules();
jest.mock('react', () => require('react/react.react-server'));
jest.mock('react-server-dom-webpack/server', () =>
require('react-server-dom-webpack/server.edge'),
);
ReactServerDOMServer = require('react-server-dom-webpack/server.edge');
const WebpackMock = require('./utils/WebpackMock');
clientExports = WebpackMock.clientExports;
serverExports = WebpackMock.serverExports;
webpackMap = WebpackMock.webpackMap;
webpackServerMap = WebpackMock.webpackServerMap;
__unmockReact();
jest.resetModules();
React = require('react');
ReactServerDOMClient = require('react-server-dom-webpack/client.edge');
ReactDOMServer = require('react-dom/server.edge');
ReactDOMClient = require('react-dom/client');
act = React.act;
useActionState = require('react').useActionState;
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
async function POST(formData) {
const boundAction = await ReactServerDOMServer.decodeAction(
formData,
webpackServerMap,
);
const returnValue = boundAction();
const formState = await ReactServerDOMServer.decodeFormState(
await returnValue,
formData,
webpackServerMap,
);
return {returnValue, formState};
}
function submit(submitter) {
const form = submitter.form || submitter;
if (!submitter.form) {
submitter = undefined;
}
const submitEvent = new Event('submit', {bubbles: true, cancelable: true});
submitEvent.submitter = submitter;
const returnValue = form.dispatchEvent(submitEvent);
if (!returnValue) {
return;
}
const action =
(submitter && submitter.getAttribute('formaction')) || form.action;
if (!/\s*javascript:/i.test(action)) {
const method = (submitter && submitter.formMethod) || form.method;
const encType = (submitter && submitter.formEnctype) || form.enctype;
if (method === 'post' && encType === 'multipart/form-data') {
let formData;
if (submitter) {
const temp = document.createElement('input');
temp.name = submitter.name;
temp.value = submitter.value;
submitter.parentNode.insertBefore(temp, submitter);
formData = new FormData(form);
temp.parentNode.removeChild(temp);
} else {
formData = new FormData(form);
}
return POST(formData);
}
throw new Error('Navigate to: ' + action);
}
}
async function readIntoContainer(stream) {
const reader = stream.getReader();
let result = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
result += Buffer.from(value).toString('utf8');
}
const temp = document.createElement('div');
temp.innerHTML = result;
insertNodesAndExecuteScripts(temp, container, null);
}
it('can submit a passed server action without hydrating it', async () => {
let foo = null;
const serverAction = serverExports(function action(formData) {
foo = formData.get('foo');
return 'hello';
});
function App() {
return (
<form action={serverAction}>
<input type="text" name="foo" defaultValue="bar" />
</form>
);
}
const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
const form = container.firstChild;
expect(foo).toBe(null);
const {returnValue} = await submit(form);
expect(returnValue).toBe('hello');
expect(foo).toBe('bar');
});
it('can submit an imported server action without hydrating it', async () => {
let foo = null;
const ServerModule = serverExports(function action(formData) {
foo = formData.get('foo');
return 'hi';
});
const serverAction = ReactServerDOMClient.createServerReference(
ServerModule.$$id,
);
function App() {
return (
<form action={serverAction}>
<input type="text" name="foo" defaultValue="bar" />
</form>
);
}
const ssrStream = await ReactDOMServer.renderToReadableStream(<App />);
await readIntoContainer(ssrStream);
const form = container.firstChild;
expect(foo).toBe(null);
const {returnValue} = await submit(form);
expect(returnValue).toBe('hi');
expect(foo).toBe('bar');
});
it('can submit a complex closure server action without hydrating it', async () => {
let foo = null;
const serverAction = serverExports(function action(bound, formData) {
foo = formData.get('foo') + bound.complex;
return 'hello';
});
function App() {
return (
<form action={serverAction.bind(null, {complex: 'object'})}>
<input type="text" name="foo" defaultValue="bar" />
</form>
);
}
const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
const form = container.firstChild;
expect(foo).toBe(null);
const {returnValue} = await submit(form);
expect(returnValue).toBe('hello');
expect(foo).toBe('barobject');
});
it('can submit a multiple complex closure server action without hydrating it', async () => {
let foo = null;
const serverAction = serverExports(function action(bound, formData) {
foo = formData.get('foo') + bound.complex;
return 'hello' + bound.complex;
});
function App() {
return (
<form action={serverAction.bind(null, {complex: 'a'})}>
<input type="text" name="foo" defaultValue="bar" />
<button formAction={serverAction.bind(null, {complex: 'b'})} />
<button formAction={serverAction.bind(null, {complex: 'c'})} />
<input
type="submit"
formAction={serverAction.bind(null, {complex: 'd'})}
/>
</form>
);
}
const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
const form = container.firstChild;
expect(foo).toBe(null);
const {returnValue} = await submit(form.getElementsByTagName('button')[1]);
expect(returnValue).toBe('helloc');
expect(foo).toBe('barc');
});
it('can bind an imported server action on the client without hydrating it', async () => {
let foo = null;
const ServerModule = serverExports(function action(bound, formData) {
foo = formData.get('foo') + bound.complex;
return 'hello';
});
const serverAction = ReactServerDOMClient.createServerReference(
ServerModule.$$id,
);
function Client() {
return (
<form action={serverAction.bind(null, {complex: 'object'})}>
<input type="text" name="foo" defaultValue="bar" />
</form>
);
}
const ssrStream = await ReactDOMServer.renderToReadableStream(<Client />);
await readIntoContainer(ssrStream);
const form = container.firstChild;
expect(foo).toBe(null);
const {returnValue} = await submit(form);
expect(returnValue).toBe('hello');
expect(foo).toBe('barobject');
});
it('can bind a server action on the client without hydrating it', async () => {
let foo = null;
const serverAction = serverExports(function action(bound, formData) {
foo = formData.get('foo') + bound.complex;
return 'hello';
});
function Client({action}) {
return (
<form action={action.bind(null, {complex: 'object'})}>
<input type="text" name="foo" defaultValue="bar" />
</form>
);
}
const ClientRef = await clientExports(Client);
const rscStream = ReactServerDOMServer.renderToReadableStream(
<ClientRef action={serverAction} />,
webpackMap,
);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
const form = container.firstChild;
expect(foo).toBe(null);
const {returnValue} = await submit(form);
expect(returnValue).toBe('hello');
expect(foo).toBe('barobject');
});
it("useActionState's dispatch binds the initial state to the provided action", async () => {
const serverAction = serverExports(
async function action(prevState, formData) {
return {
count:
prevState.count + parseInt(formData.get('incrementAmount'), 10),
};
},
);
const initialState = {count: 1};
function Client({action}) {
const [state, dispatch, isPending] = useActionState(action, initialState);
return (
<form action={dispatch}>
<span>{isPending ? 'Pending...' : ''}</span>
<span>Count: {state.count}</span>
<input type="text" name="incrementAmount" defaultValue="5" />
</form>
);
}
const ClientRef = await clientExports(Client);
const rscStream = ReactServerDOMServer.renderToReadableStream(
<ClientRef action={serverAction} />,
webpackMap,
);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
const form = container.getElementsByTagName('form')[0];
const pendingSpan = container.getElementsByTagName('span')[0];
const stateSpan = container.getElementsByTagName('span')[1];
expect(pendingSpan.textContent).toBe('');
expect(stateSpan.textContent).toBe('Count: 1');
const {returnValue} = await submit(form);
expect(await returnValue).toEqual({count: 6});
});
it('useActionState can reuse state during MPA form submission', async () => {
const serverAction = serverExports(
async function action(prevState, formData) {
return prevState + 1;
},
);
function Form({action}) {
const [count, dispatch, isPending] = useActionState(action, 1);
return (
<form action={dispatch}>
{isPending ? 'Pending...' : ''}
{count}
</form>
);
}
function Client({action}) {
return (
<div>
<Form action={action} />
<Form action={action} />
<Form action={action} />
</div>
);
}
const ClientRef = await clientExports(Client);
const rscStream = ReactServerDOMServer.renderToReadableStream(
<ClientRef action={serverAction} />,
webpackMap,
);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
expect(container.textContent).toBe('111');
const form = container.getElementsByTagName('form')[1];
const {formState} = await submit(form);
container.innerHTML = '';
const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
<ClientRef action={serverAction} />,
webpackMap,
);
const postbackResponse = ReactServerDOMClient.createFromReadableStream(
postbackRscStream,
{
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);
const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
postbackResponse,
{formState: formState},
);
await readIntoContainer(postbackSsrStream);
expect(container.textContent).toBe('121');
if (__DEV__) {
await act(() => {
ReactDOMClient.hydrateRoot(container, postbackResponse, {
formState: formState,
});
});
expect(container.textContent).toBe('121');
}
});
it(
'useActionState preserves state if arity is the same, but different ' +
'arguments are bound (i.e. inline closure)',
async () => {
const serverAction = serverExports(
async function action(stepSize, prevState, formData) {
return prevState + stepSize;
},
);
function Form({action}) {
const [count, dispatch, isPending] = useActionState(action, 1);
return (
<form action={dispatch}>
{isPending ? 'Pending...' : ''}
{count}
</form>
);
}
function Client({action}) {
return (
<div>
<Form action={action} />
<Form action={action} />
<Form action={action} />
</div>
);
}
const ClientRef = await clientExports(Client);
const rscStream = ReactServerDOMServer.renderToReadableStream(
<ClientRef action={serverAction.bind(null, 1)} />,
webpackMap,
);
const response = ReactServerDOMClient.createFromReadableStream(
rscStream,
{
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
expect(container.textContent).toBe('111');
const form = container.getElementsByTagName('form')[1];
const {formState} = await submit(form);
container.innerHTML = '';
const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
<ClientRef action={serverAction.bind(null, 5)} />,
webpackMap,
);
const postbackResponse = ReactServerDOMClient.createFromReadableStream(
postbackRscStream,
{
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);
const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
postbackResponse,
{formState: formState},
);
await readIntoContainer(postbackSsrStream);
expect(container.textContent).toBe('121');
const form2 = container.getElementsByTagName('form')[1];
const {formState: formState2} = await submit(form2);
container.innerHTML = '';
const postbackRscStream2 = ReactServerDOMServer.renderToReadableStream(
<ClientRef action={serverAction.bind(null, 5)} />,
webpackMap,
);
const postbackResponse2 = ReactServerDOMClient.createFromReadableStream(
postbackRscStream2,
{
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);
const postbackSsrStream2 = await ReactDOMServer.renderToReadableStream(
postbackResponse2,
{formState: formState2},
);
await readIntoContainer(postbackSsrStream2);
expect(container.textContent).toBe('171');
},
);
it('useActionState does not reuse state if action signatures are different', async () => {
const increaseBy1 = serverExports(
async function action(prevState, formData) {
return prevState + 1;
},
);
const increaseBy5 = serverExports(
async function action(prevState, formData) {
return prevState + 5;
},
);
function Form({action}) {
const [count, dispatch, isPending] = useActionState(action, 1);
return (
<form action={dispatch}>
{isPending ? 'Pending...' : ''}
{count}
</form>
);
}
function Client({action}) {
return (
<div>
<Form action={action} />
<Form action={action} />
<Form action={action} />
</div>
);
}
const ClientRef = await clientExports(Client);
const rscStream = ReactServerDOMServer.renderToReadableStream(
<ClientRef action={increaseBy1} />,
webpackMap,
);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
expect(container.textContent).toBe('111');
const form = container.getElementsByTagName('form')[1];
const {formState} = await submit(form);
container.innerHTML = '';
const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
<ClientRef action={increaseBy5} />,
webpackMap,
);
const postbackResponse = ReactServerDOMClient.createFromReadableStream(
postbackRscStream,
{
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);
const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
postbackResponse,
{formState: formState},
);
await readIntoContainer(postbackSsrStream);
expect(container.textContent).toBe('111');
});
it('when permalink is provided, useActionState compares that instead of the keypath', async () => {
const serverAction = serverExports(
async function action(prevState, formData) {
return prevState + 1;
},
);
function Form({action, permalink}) {
const [count, dispatch, isPending] = useActionState(action, 1, permalink);
return (
<form action={dispatch}>
{isPending ? 'Pending...' : ''}
{count}
</form>
);
}
function Page1({action, permalink}) {
return <Form action={action} permalink={permalink} />;
}
function Page2({action, permalink}) {
return <Form action={action} permalink={permalink} />;
}
const Page1Ref = await clientExports(Page1);
const Page2Ref = await clientExports(Page2);
const rscStream = ReactServerDOMServer.renderToReadableStream(
<Page1Ref action={serverAction} permalink="/permalink" />,
webpackMap,
);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
expect(container.textContent).toBe('1');
const form = container.getElementsByTagName('form')[0];
const {formState} = await submit(form);
container.innerHTML = '';
const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
<Page2Ref action={serverAction} permalink="/permalink" />,
webpackMap,
);
const postbackResponse = ReactServerDOMClient.createFromReadableStream(
postbackRscStream,
{
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);
const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
postbackResponse,
{formState: formState},
);
await readIntoContainer(postbackSsrStream);
expect(container.textContent).toBe('2');
const form2 = container.getElementsByTagName('form')[0];
const {formState: formState2} = await submit(form2);
container.innerHTML = '';
const postbackRscStream2 = ReactServerDOMServer.renderToReadableStream(
<Page1Ref action={serverAction} permalink="/some-other-permalink" />,
webpackMap,
);
const postbackResponse2 = ReactServerDOMClient.createFromReadableStream(
postbackRscStream2,
{
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);
const postbackSsrStream2 = await ReactDOMServer.renderToReadableStream(
postbackResponse2,
{formState: formState2},
);
await readIntoContainer(postbackSsrStream2);
expect(container.textContent).toBe('1');
});
it('useActionState can change the action URL with the `permalink` argument', async () => {
const serverAction = serverExports(function action(prevState) {
return {state: prevState.count + 1};
});
const initialState = {count: 1};
function Client({action}) {
const [state, dispatch, isPending] = useActionState(
action,
initialState,
'/permalink',
);
return (
<form action={dispatch}>
<span>{isPending ? 'Pending...' : ''}</span>
<span>Count: {state.count}</span>
</form>
);
}
const ClientRef = await clientExports(Client);
const rscStream = ReactServerDOMServer.renderToReadableStream(
<ClientRef action={serverAction} />,
webpackMap,
);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
const form = container.getElementsByTagName('form')[0];
const pendingSpan = container.getElementsByTagName('span')[0];
const stateSpan = container.getElementsByTagName('span')[1];
expect(pendingSpan.textContent).toBe('');
expect(stateSpan.textContent).toBe('Count: 1');
expect(form.action).toBe('http://localhost/permalink');
});
it('useActionState `permalink` is coerced to string', async () => {
const serverAction = serverExports(function action(prevState) {
return {state: prevState.count + 1};
});
class Permalink {
toString() {
return '/permalink';
}
}
const permalink = new Permalink();
const initialState = {count: 1};
function Client({action}) {
const [state, dispatch, isPending] = useActionState(
action,
initialState,
permalink,
);
return (
<form action={dispatch}>
<span>{isPending ? 'Pending...' : ''}</span>
<span>Count: {state.count}</span>
</form>
);
}
const ClientRef = await clientExports(Client);
const rscStream = ReactServerDOMServer.renderToReadableStream(
<ClientRef action={serverAction} />,
webpackMap,
);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
const form = container.getElementsByTagName('form')[0];
const pendingSpan = container.getElementsByTagName('span')[0];
const stateSpan = container.getElementsByTagName('span')[1];
expect(pendingSpan.textContent).toBe('');
expect(stateSpan.textContent).toBe('Count: 1');
expect(form.action).toBe('http://localhost/permalink');
});
it('useActionState can return JSX state during MPA form submission', async () => {
const serverAction = serverExports(
async function action(prevState, formData) {
return <div>error message</div>;
},
);
function Form({action}) {
const [errorMsg, dispatch] = useActionState(action, null);
return <form action={dispatch}>{errorMsg}</form>;
}
const FormRef = await clientExports(Form);
const rscStream = ReactServerDOMServer.renderToReadableStream(
<FormRef action={serverAction} />,
webpackMap,
);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
const form1 = container.getElementsByTagName('form')[0];
expect(form1.textContent).toBe('');
async function submitTheForm() {
const form = container.getElementsByTagName('form')[0];
const {formState} = await submit(form);
container.innerHTML = '';
const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
<FormRef action={serverAction} />,
webpackMap,
);
const postbackResponse = ReactServerDOMClient.createFromReadableStream(
postbackRscStream,
{
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);
const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
postbackResponse,
{formState: formState},
);
await readIntoContainer(postbackSsrStream);
}
await expect(submitTheForm).toErrorDev(
'Failed to serialize an action for progressive enhancement:\n' +
'Error: React Element cannot be passed to Server Functions from the Client without a temporary reference set. Pass a TemporaryReferenceSet to the options.\n' +
' [<div/>]\n' +
' ^^^^^^',
);
const form2 = container.getElementsByTagName('form')[0];
expect(form2.textContent).toBe('error message');
expect(form2.firstChild.tagName).toBe('DIV');
});
it('useActionState can return binary state during MPA form submission', async () => {
const serverAction = serverExports(
async function action(prevState, formData) {
return new Blob([new Uint8Array([104, 105])]);
},
);
let blob;
function Form({action}) {
const [errorMsg, dispatch] = useActionState(action, null);
let text;
if (errorMsg) {
blob = errorMsg;
text = React.use(blob.text());
}
return <form action={dispatch}>{text}</form>;
}
const FormRef = await clientExports(Form);
const rscStream = ReactServerDOMServer.renderToReadableStream(
<FormRef action={serverAction} />,
webpackMap,
);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);
const form1 = container.getElementsByTagName('form')[0];
expect(form1.textContent).toBe('');
async function submitTheForm() {
const form = container.getElementsByTagName('form')[0];
const {formState} = await submit(form);
container.innerHTML = '';
const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
{formState, root: <FormRef action={serverAction} />},
webpackMap,
);
const postbackResponse =
await ReactServerDOMClient.createFromReadableStream(postbackRscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
postbackResponse.root,
{formState: postbackResponse.formState},
);
await readIntoContainer(postbackSsrStream);
}
await expect(submitTheForm).toErrorDev(
'Failed to serialize an action for progressive enhancement:\n' +
'Error: File/Blob fields are not yet supported in progressive forms. Will fallback to client hydration.',
);
expect(blob instanceof Blob).toBe(true);
expect(blob.size).toBe(2);
const form2 = container.getElementsByTagName('form')[0];
expect(form2.textContent).toBe('hi');
});
});