1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @emails react-core
    
  8.  */
    
  9. 
    
  10. 'use strict';
    
  11. 
    
  12. import {insertNodesAndExecuteScripts} from 'react-dom/src/test-utils/FizzTestUtils';
    
  13. 
    
  14. // Polyfills for test environment
    
  15. global.ReadableStream =
    
  16.   require('web-streams-polyfill/ponyfill/es6').ReadableStream;
    
  17. global.TextEncoder = require('util').TextEncoder;
    
  18. global.TextDecoder = require('util').TextDecoder;
    
  19. 
    
  20. // Don't wait before processing work on the server.
    
  21. // TODO: we can replace this with FlightServer.act().
    
  22. global.setTimeout = cb => cb();
    
  23. 
    
  24. let container;
    
  25. let clientExports;
    
  26. let serverExports;
    
  27. let webpackMap;
    
  28. let webpackServerMap;
    
  29. let React;
    
  30. let ReactDOMServer;
    
  31. let ReactServerDOMServer;
    
  32. let ReactServerDOMClient;
    
  33. let ReactDOMClient;
    
  34. let useFormState;
    
  35. let act;
    
  36. 
    
  37. describe('ReactFlightDOMForm', () => {
    
  38.   beforeEach(() => {
    
  39.     jest.resetModules();
    
  40.     // Simulate the condition resolution
    
  41.     jest.mock('react', () => require('react/react.shared-subset'));
    
  42.     jest.mock('react-server-dom-webpack/server', () =>
    
  43.       require('react-server-dom-webpack/server.edge'),
    
  44.     );
    
  45.     ReactServerDOMServer = require('react-server-dom-webpack/server.edge');
    
  46.     const WebpackMock = require('./utils/WebpackMock');
    
  47.     clientExports = WebpackMock.clientExports;
    
  48.     serverExports = WebpackMock.serverExports;
    
  49.     webpackMap = WebpackMock.webpackMap;
    
  50.     webpackServerMap = WebpackMock.webpackServerMap;
    
  51.     __unmockReact();
    
  52.     jest.resetModules();
    
  53.     React = require('react');
    
  54.     ReactServerDOMClient = require('react-server-dom-webpack/client.edge');
    
  55.     ReactDOMServer = require('react-dom/server.edge');
    
  56.     ReactDOMClient = require('react-dom/client');
    
  57.     act = require('react-dom/test-utils').act;
    
  58.     useFormState = require('react-dom').useFormState;
    
  59.     container = document.createElement('div');
    
  60.     document.body.appendChild(container);
    
  61.   });
    
  62. 
    
  63.   afterEach(() => {
    
  64.     document.body.removeChild(container);
    
  65.   });
    
  66. 
    
  67.   async function POST(formData) {
    
  68.     const boundAction = await ReactServerDOMServer.decodeAction(
    
  69.       formData,
    
  70.       webpackServerMap,
    
  71.     );
    
  72.     const returnValue = boundAction();
    
  73.     const formState = await ReactServerDOMServer.decodeFormState(
    
  74.       await returnValue,
    
  75.       formData,
    
  76.       webpackServerMap,
    
  77.     );
    
  78.     return {returnValue, formState};
    
  79.   }
    
  80. 
    
  81.   function submit(submitter) {
    
  82.     const form = submitter.form || submitter;
    
  83.     if (!submitter.form) {
    
  84.       submitter = undefined;
    
  85.     }
    
  86.     const submitEvent = new Event('submit', {bubbles: true, cancelable: true});
    
  87.     submitEvent.submitter = submitter;
    
  88.     const returnValue = form.dispatchEvent(submitEvent);
    
  89.     if (!returnValue) {
    
  90.       return;
    
  91.     }
    
  92.     const action =
    
  93.       (submitter && submitter.getAttribute('formaction')) || form.action;
    
  94.     if (!/\s*javascript:/i.test(action)) {
    
  95.       const method = (submitter && submitter.formMethod) || form.method;
    
  96.       const encType = (submitter && submitter.formEnctype) || form.enctype;
    
  97.       if (method === 'post' && encType === 'multipart/form-data') {
    
  98.         let formData;
    
  99.         if (submitter) {
    
  100.           const temp = document.createElement('input');
    
  101.           temp.name = submitter.name;
    
  102.           temp.value = submitter.value;
    
  103.           submitter.parentNode.insertBefore(temp, submitter);
    
  104.           formData = new FormData(form);
    
  105.           temp.parentNode.removeChild(temp);
    
  106.         } else {
    
  107.           formData = new FormData(form);
    
  108.         }
    
  109.         return POST(formData);
    
  110.       }
    
  111.       throw new Error('Navigate to: ' + action);
    
  112.     }
    
  113.   }
    
  114. 
    
  115.   async function readIntoContainer(stream) {
    
  116.     const reader = stream.getReader();
    
  117.     let result = '';
    
  118.     while (true) {
    
  119.       const {done, value} = await reader.read();
    
  120.       if (done) {
    
  121.         break;
    
  122.       }
    
  123.       result += Buffer.from(value).toString('utf8');
    
  124.     }
    
  125.     const temp = document.createElement('div');
    
  126.     temp.innerHTML = result;
    
  127.     insertNodesAndExecuteScripts(temp, container, null);
    
  128.   }
    
  129. 
    
  130.   // @gate enableFormActions
    
  131.   it('can submit a passed server action without hydrating it', async () => {
    
  132.     let foo = null;
    
  133. 
    
  134.     const serverAction = serverExports(function action(formData) {
    
  135.       foo = formData.get('foo');
    
  136.       return 'hello';
    
  137.     });
    
  138.     function App() {
    
  139.       return (
    
  140.         <form action={serverAction}>
    
  141.           <input type="text" name="foo" defaultValue="bar" />
    
  142.         </form>
    
  143.       );
    
  144.     }
    
  145.     const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
    
  146.     const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
    
  147.       ssrManifest: {
    
  148.         moduleMap: null,
    
  149.         moduleLoading: null,
    
  150.       },
    
  151.     });
    
  152.     const ssrStream = await ReactDOMServer.renderToReadableStream(response);
    
  153.     await readIntoContainer(ssrStream);
    
  154. 
    
  155.     const form = container.firstChild;
    
  156. 
    
  157.     expect(foo).toBe(null);
    
  158. 
    
  159.     const {returnValue} = await submit(form);
    
  160. 
    
  161.     expect(returnValue).toBe('hello');
    
  162.     expect(foo).toBe('bar');
    
  163.   });
    
  164. 
    
  165.   // @gate enableFormActions
    
  166.   it('can submit an imported server action without hydrating it', async () => {
    
  167.     let foo = null;
    
  168. 
    
  169.     const ServerModule = serverExports(function action(formData) {
    
  170.       foo = formData.get('foo');
    
  171.       return 'hi';
    
  172.     });
    
  173.     const serverAction = ReactServerDOMClient.createServerReference(
    
  174.       ServerModule.$$id,
    
  175.     );
    
  176.     function App() {
    
  177.       return (
    
  178.         <form action={serverAction}>
    
  179.           <input type="text" name="foo" defaultValue="bar" />
    
  180.         </form>
    
  181.       );
    
  182.     }
    
  183. 
    
  184.     const ssrStream = await ReactDOMServer.renderToReadableStream(<App />);
    
  185.     await readIntoContainer(ssrStream);
    
  186. 
    
  187.     const form = container.firstChild;
    
  188. 
    
  189.     expect(foo).toBe(null);
    
  190. 
    
  191.     const {returnValue} = await submit(form);
    
  192. 
    
  193.     expect(returnValue).toBe('hi');
    
  194. 
    
  195.     expect(foo).toBe('bar');
    
  196.   });
    
  197. 
    
  198.   // @gate enableFormActions
    
  199.   it('can submit a complex closure server action without hydrating it', async () => {
    
  200.     let foo = null;
    
  201. 
    
  202.     const serverAction = serverExports(function action(bound, formData) {
    
  203.       foo = formData.get('foo') + bound.complex;
    
  204.       return 'hello';
    
  205.     });
    
  206.     function App() {
    
  207.       return (
    
  208.         <form action={serverAction.bind(null, {complex: 'object'})}>
    
  209.           <input type="text" name="foo" defaultValue="bar" />
    
  210.         </form>
    
  211.       );
    
  212.     }
    
  213.     const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
    
  214.     const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
    
  215.       ssrManifest: {
    
  216.         moduleMap: null,
    
  217.         moduleLoading: null,
    
  218.       },
    
  219.     });
    
  220.     const ssrStream = await ReactDOMServer.renderToReadableStream(response);
    
  221.     await readIntoContainer(ssrStream);
    
  222. 
    
  223.     const form = container.firstChild;
    
  224. 
    
  225.     expect(foo).toBe(null);
    
  226. 
    
  227.     const {returnValue} = await submit(form);
    
  228. 
    
  229.     expect(returnValue).toBe('hello');
    
  230.     expect(foo).toBe('barobject');
    
  231.   });
    
  232. 
    
  233.   // @gate enableFormActions
    
  234.   it('can submit a multiple complex closure server action without hydrating it', async () => {
    
  235.     let foo = null;
    
  236. 
    
  237.     const serverAction = serverExports(function action(bound, formData) {
    
  238.       foo = formData.get('foo') + bound.complex;
    
  239.       return 'hello' + bound.complex;
    
  240.     });
    
  241.     function App() {
    
  242.       return (
    
  243.         <form action={serverAction.bind(null, {complex: 'a'})}>
    
  244.           <input type="text" name="foo" defaultValue="bar" />
    
  245.           <button formAction={serverAction.bind(null, {complex: 'b'})} />
    
  246.           <button formAction={serverAction.bind(null, {complex: 'c'})} />
    
  247.           <input
    
  248.             type="submit"
    
  249.             formAction={serverAction.bind(null, {complex: 'd'})}
    
  250.           />
    
  251.         </form>
    
  252.       );
    
  253.     }
    
  254.     const rscStream = ReactServerDOMServer.renderToReadableStream(<App />);
    
  255.     const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
    
  256.       ssrManifest: {
    
  257.         moduleMap: null,
    
  258.         moduleLoading: null,
    
  259.       },
    
  260.     });
    
  261.     const ssrStream = await ReactDOMServer.renderToReadableStream(response);
    
  262.     await readIntoContainer(ssrStream);
    
  263. 
    
  264.     const form = container.firstChild;
    
  265. 
    
  266.     expect(foo).toBe(null);
    
  267. 
    
  268.     const {returnValue} = await submit(form.getElementsByTagName('button')[1]);
    
  269. 
    
  270.     expect(returnValue).toBe('helloc');
    
  271.     expect(foo).toBe('barc');
    
  272.   });
    
  273. 
    
  274.   // @gate enableFormActions
    
  275.   it('can bind an imported server action on the client without hydrating it', async () => {
    
  276.     let foo = null;
    
  277. 
    
  278.     const ServerModule = serverExports(function action(bound, formData) {
    
  279.       foo = formData.get('foo') + bound.complex;
    
  280.       return 'hello';
    
  281.     });
    
  282.     const serverAction = ReactServerDOMClient.createServerReference(
    
  283.       ServerModule.$$id,
    
  284.     );
    
  285.     function Client() {
    
  286.       return (
    
  287.         <form action={serverAction.bind(null, {complex: 'object'})}>
    
  288.           <input type="text" name="foo" defaultValue="bar" />
    
  289.         </form>
    
  290.       );
    
  291.     }
    
  292. 
    
  293.     const ssrStream = await ReactDOMServer.renderToReadableStream(<Client />);
    
  294.     await readIntoContainer(ssrStream);
    
  295. 
    
  296.     const form = container.firstChild;
    
  297. 
    
  298.     expect(foo).toBe(null);
    
  299. 
    
  300.     const {returnValue} = await submit(form);
    
  301. 
    
  302.     expect(returnValue).toBe('hello');
    
  303.     expect(foo).toBe('barobject');
    
  304.   });
    
  305. 
    
  306.   // @gate enableFormActions
    
  307.   it('can bind a server action on the client without hydrating it', async () => {
    
  308.     let foo = null;
    
  309. 
    
  310.     const serverAction = serverExports(function action(bound, formData) {
    
  311.       foo = formData.get('foo') + bound.complex;
    
  312.       return 'hello';
    
  313.     });
    
  314. 
    
  315.     function Client({action}) {
    
  316.       return (
    
  317.         <form action={action.bind(null, {complex: 'object'})}>
    
  318.           <input type="text" name="foo" defaultValue="bar" />
    
  319.         </form>
    
  320.       );
    
  321.     }
    
  322.     const ClientRef = await clientExports(Client);
    
  323. 
    
  324.     const rscStream = ReactServerDOMServer.renderToReadableStream(
    
  325.       <ClientRef action={serverAction} />,
    
  326.       webpackMap,
    
  327.     );
    
  328.     const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
    
  329.       ssrManifest: {
    
  330.         moduleMap: null,
    
  331.         moduleLoading: null,
    
  332.       },
    
  333.     });
    
  334.     const ssrStream = await ReactDOMServer.renderToReadableStream(response);
    
  335.     await readIntoContainer(ssrStream);
    
  336. 
    
  337.     const form = container.firstChild;
    
  338. 
    
  339.     expect(foo).toBe(null);
    
  340. 
    
  341.     const {returnValue} = await submit(form);
    
  342. 
    
  343.     expect(returnValue).toBe('hello');
    
  344.     expect(foo).toBe('barobject');
    
  345.   });
    
  346. 
    
  347.   // @gate enableFormActions
    
  348.   // @gate enableAsyncActions
    
  349.   it("useFormState's dispatch binds the initial state to the provided action", async () => {
    
  350.     const serverAction = serverExports(
    
  351.       async function action(prevState, formData) {
    
  352.         return {
    
  353.           count:
    
  354.             prevState.count + parseInt(formData.get('incrementAmount'), 10),
    
  355.         };
    
  356.       },
    
  357.     );
    
  358. 
    
  359.     const initialState = {count: 1};
    
  360.     function Client({action}) {
    
  361.       const [state, dispatch] = useFormState(action, initialState);
    
  362.       return (
    
  363.         <form action={dispatch}>
    
  364.           <span>Count: {state.count}</span>
    
  365.           <input type="text" name="incrementAmount" defaultValue="5" />
    
  366.         </form>
    
  367.       );
    
  368.     }
    
  369.     const ClientRef = await clientExports(Client);
    
  370. 
    
  371.     const rscStream = ReactServerDOMServer.renderToReadableStream(
    
  372.       <ClientRef action={serverAction} />,
    
  373.       webpackMap,
    
  374.     );
    
  375.     const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
    
  376.       ssrManifest: {
    
  377.         moduleMap: null,
    
  378.         moduleLoading: null,
    
  379.       },
    
  380.     });
    
  381.     const ssrStream = await ReactDOMServer.renderToReadableStream(response);
    
  382.     await readIntoContainer(ssrStream);
    
  383. 
    
  384.     const form = container.getElementsByTagName('form')[0];
    
  385.     const span = container.getElementsByTagName('span')[0];
    
  386.     expect(span.textContent).toBe('Count: 1');
    
  387. 
    
  388.     const {returnValue} = await submit(form);
    
  389.     expect(await returnValue).toEqual({count: 6});
    
  390.   });
    
  391. 
    
  392.   // @gate enableFormActions
    
  393.   // @gate enableAsyncActions
    
  394.   it('useFormState can reuse state during MPA form submission', async () => {
    
  395.     const serverAction = serverExports(
    
  396.       async function action(prevState, formData) {
    
  397.         return prevState + 1;
    
  398.       },
    
  399.     );
    
  400. 
    
  401.     function Form({action}) {
    
  402.       const [count, dispatch] = useFormState(action, 1);
    
  403.       return <form action={dispatch}>{count}</form>;
    
  404.     }
    
  405. 
    
  406.     function Client({action}) {
    
  407.       return (
    
  408.         <div>
    
  409.           <Form action={action} />
    
  410.           <Form action={action} />
    
  411.           <Form action={action} />
    
  412.         </div>
    
  413.       );
    
  414.     }
    
  415. 
    
  416.     const ClientRef = await clientExports(Client);
    
  417. 
    
  418.     const rscStream = ReactServerDOMServer.renderToReadableStream(
    
  419.       <ClientRef action={serverAction} />,
    
  420.       webpackMap,
    
  421.     );
    
  422.     const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
    
  423.       ssrManifest: {
    
  424.         moduleMap: null,
    
  425.         moduleLoading: null,
    
  426.       },
    
  427.     });
    
  428.     const ssrStream = await ReactDOMServer.renderToReadableStream(response);
    
  429.     await readIntoContainer(ssrStream);
    
  430. 
    
  431.     expect(container.textContent).toBe('111');
    
  432. 
    
  433.     // There are three identical forms. We're going to submit the second one.
    
  434.     const form = container.getElementsByTagName('form')[1];
    
  435.     const {formState} = await submit(form);
    
  436. 
    
  437.     // Simulate an MPA form submission by resetting the container and
    
  438.     // rendering again.
    
  439.     container.innerHTML = '';
    
  440. 
    
  441.     const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
    
  442.       <ClientRef action={serverAction} />,
    
  443.       webpackMap,
    
  444.     );
    
  445.     const postbackResponse = ReactServerDOMClient.createFromReadableStream(
    
  446.       postbackRscStream,
    
  447.       {
    
  448.         ssrManifest: {
    
  449.           moduleMap: null,
    
  450.           moduleLoading: null,
    
  451.         },
    
  452.       },
    
  453.     );
    
  454.     const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
    
  455.       postbackResponse,
    
  456.       {formState: formState},
    
  457.     );
    
  458.     await readIntoContainer(postbackSsrStream);
    
  459. 
    
  460.     // Only the second form's state should have been updated.
    
  461.     expect(container.textContent).toBe('121');
    
  462. 
    
  463.     // Test that it hydrates correctly
    
  464.     if (__DEV__) {
    
  465.       // TODO: Can't use our internal act() util that works in production
    
  466.       // because it works by overriding the timer APIs, which this test module
    
  467.       // also does. Remove dev condition once FlightServer.act() is available.
    
  468.       await act(() => {
    
  469.         ReactDOMClient.hydrateRoot(container, postbackResponse, {
    
  470.           formState: formState,
    
  471.         });
    
  472.       });
    
  473.       expect(container.textContent).toBe('121');
    
  474.     }
    
  475.   });
    
  476. 
    
  477.   // @gate enableFormActions
    
  478.   // @gate enableAsyncActions
    
  479.   it(
    
  480.     'useFormState preserves state if arity is the same, but different ' +
    
  481.       'arguments are bound (i.e. inline closure)',
    
  482.     async () => {
    
  483.       const serverAction = serverExports(
    
  484.         async function action(stepSize, prevState, formData) {
    
  485.           return prevState + stepSize;
    
  486.         },
    
  487.       );
    
  488. 
    
  489.       function Form({action}) {
    
  490.         const [count, dispatch] = useFormState(action, 1);
    
  491.         return <form action={dispatch}>{count}</form>;
    
  492.       }
    
  493. 
    
  494.       function Client({action}) {
    
  495.         return (
    
  496.           <div>
    
  497.             <Form action={action} />
    
  498.             <Form action={action} />
    
  499.             <Form action={action} />
    
  500.           </div>
    
  501.         );
    
  502.       }
    
  503. 
    
  504.       const ClientRef = await clientExports(Client);
    
  505. 
    
  506.       const rscStream = ReactServerDOMServer.renderToReadableStream(
    
  507.         // Note: `.bind` is the same as an inline closure with 'use server'
    
  508.         <ClientRef action={serverAction.bind(null, 1)} />,
    
  509.         webpackMap,
    
  510.       );
    
  511.       const response = ReactServerDOMClient.createFromReadableStream(
    
  512.         rscStream,
    
  513.         {
    
  514.           ssrManifest: {
    
  515.             moduleMap: null,
    
  516.             moduleLoading: null,
    
  517.           },
    
  518.         },
    
  519.       );
    
  520.       const ssrStream = await ReactDOMServer.renderToReadableStream(response);
    
  521.       await readIntoContainer(ssrStream);
    
  522. 
    
  523.       expect(container.textContent).toBe('111');
    
  524. 
    
  525.       // There are three identical forms. We're going to submit the second one.
    
  526.       const form = container.getElementsByTagName('form')[1];
    
  527.       const {formState} = await submit(form);
    
  528. 
    
  529.       // Simulate an MPA form submission by resetting the container and
    
  530.       // rendering again.
    
  531.       container.innerHTML = '';
    
  532. 
    
  533.       // On the next page, the same server action is rendered again, but with
    
  534.       // a different bound stepSize argument. We should treat this as the same
    
  535.       // action signature.
    
  536.       const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
    
  537.         // Note: `.bind` is the same as an inline closure with 'use server'
    
  538.         <ClientRef action={serverAction.bind(null, 5)} />,
    
  539.         webpackMap,
    
  540.       );
    
  541.       const postbackResponse = ReactServerDOMClient.createFromReadableStream(
    
  542.         postbackRscStream,
    
  543.         {
    
  544.           ssrManifest: {
    
  545.             moduleMap: null,
    
  546.             moduleLoading: null,
    
  547.           },
    
  548.         },
    
  549.       );
    
  550.       const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
    
  551.         postbackResponse,
    
  552.         {formState: formState},
    
  553.       );
    
  554.       await readIntoContainer(postbackSsrStream);
    
  555. 
    
  556.       // The state should have been preserved because the action signatures are
    
  557.       // the same. (Note that the amount increased by 1, because that was the
    
  558.       // value of stepSize at the time the form was submitted)
    
  559.       expect(container.textContent).toBe('121');
    
  560. 
    
  561.       // Now submit the form again. This time, the state should increase by 5
    
  562.       // because the stepSize argument has changed.
    
  563.       const form2 = container.getElementsByTagName('form')[1];
    
  564.       const {formState: formState2} = await submit(form2);
    
  565. 
    
  566.       container.innerHTML = '';
    
  567. 
    
  568.       const postbackRscStream2 = ReactServerDOMServer.renderToReadableStream(
    
  569.         // Note: `.bind` is the same as an inline closure with 'use server'
    
  570.         <ClientRef action={serverAction.bind(null, 5)} />,
    
  571.         webpackMap,
    
  572.       );
    
  573.       const postbackResponse2 = ReactServerDOMClient.createFromReadableStream(
    
  574.         postbackRscStream2,
    
  575.         {
    
  576.           ssrManifest: {
    
  577.             moduleMap: null,
    
  578.             moduleLoading: null,
    
  579.           },
    
  580.         },
    
  581.       );
    
  582.       const postbackSsrStream2 = await ReactDOMServer.renderToReadableStream(
    
  583.         postbackResponse2,
    
  584.         {formState: formState2},
    
  585.       );
    
  586.       await readIntoContainer(postbackSsrStream2);
    
  587. 
    
  588.       expect(container.textContent).toBe('171');
    
  589.     },
    
  590.   );
    
  591. 
    
  592.   // @gate enableFormActions
    
  593.   // @gate enableAsyncActions
    
  594.   it('useFormState does not reuse state if action signatures are different', async () => {
    
  595.     // This is the same as the previous test, except instead of using bind to
    
  596.     // configure the server action (i.e. a closure), it swaps the action.
    
  597.     const increaseBy1 = serverExports(
    
  598.       async function action(prevState, formData) {
    
  599.         return prevState + 1;
    
  600.       },
    
  601.     );
    
  602. 
    
  603.     const increaseBy5 = serverExports(
    
  604.       async function action(prevState, formData) {
    
  605.         return prevState + 5;
    
  606.       },
    
  607.     );
    
  608. 
    
  609.     function Form({action}) {
    
  610.       const [count, dispatch] = useFormState(action, 1);
    
  611.       return <form action={dispatch}>{count}</form>;
    
  612.     }
    
  613. 
    
  614.     function Client({action}) {
    
  615.       return (
    
  616.         <div>
    
  617.           <Form action={action} />
    
  618.           <Form action={action} />
    
  619.           <Form action={action} />
    
  620.         </div>
    
  621.       );
    
  622.     }
    
  623. 
    
  624.     const ClientRef = await clientExports(Client);
    
  625. 
    
  626.     const rscStream = ReactServerDOMServer.renderToReadableStream(
    
  627.       <ClientRef action={increaseBy1} />,
    
  628.       webpackMap,
    
  629.     );
    
  630.     const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
    
  631.       ssrManifest: {
    
  632.         moduleMap: null,
    
  633.         moduleLoading: null,
    
  634.       },
    
  635.     });
    
  636.     const ssrStream = await ReactDOMServer.renderToReadableStream(response);
    
  637.     await readIntoContainer(ssrStream);
    
  638. 
    
  639.     expect(container.textContent).toBe('111');
    
  640. 
    
  641.     // There are three identical forms. We're going to submit the second one.
    
  642.     const form = container.getElementsByTagName('form')[1];
    
  643.     const {formState} = await submit(form);
    
  644. 
    
  645.     // Simulate an MPA form submission by resetting the container and
    
  646.     // rendering again.
    
  647.     container.innerHTML = '';
    
  648. 
    
  649.     // On the next page, a different server action is rendered. It should not
    
  650.     // reuse the state from the previous page.
    
  651.     const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
    
  652.       <ClientRef action={increaseBy5} />,
    
  653.       webpackMap,
    
  654.     );
    
  655.     const postbackResponse = ReactServerDOMClient.createFromReadableStream(
    
  656.       postbackRscStream,
    
  657.       {
    
  658.         ssrManifest: {
    
  659.           moduleMap: null,
    
  660.           moduleLoading: null,
    
  661.         },
    
  662.       },
    
  663.     );
    
  664.     const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
    
  665.       postbackResponse,
    
  666.       {formState: formState},
    
  667.     );
    
  668.     await readIntoContainer(postbackSsrStream);
    
  669. 
    
  670.     // The state should not have been preserved because the action signatures
    
  671.     // are not the same.
    
  672.     expect(container.textContent).toBe('111');
    
  673.   });
    
  674. 
    
  675.   // @gate enableFormActions
    
  676.   // @gate enableAsyncActions
    
  677.   it('when permalink is provided, useFormState compares that instead of the keypath', async () => {
    
  678.     const serverAction = serverExports(
    
  679.       async function action(prevState, formData) {
    
  680.         return prevState + 1;
    
  681.       },
    
  682.     );
    
  683. 
    
  684.     function Form({action, permalink}) {
    
  685.       const [count, dispatch] = useFormState(action, 1, permalink);
    
  686.       return <form action={dispatch}>{count}</form>;
    
  687.     }
    
  688. 
    
  689.     function Page1({action, permalink}) {
    
  690.       return <Form action={action} permalink={permalink} />;
    
  691.     }
    
  692. 
    
  693.     function Page2({action, permalink}) {
    
  694.       return <Form action={action} permalink={permalink} />;
    
  695.     }
    
  696. 
    
  697.     const Page1Ref = await clientExports(Page1);
    
  698.     const Page2Ref = await clientExports(Page2);
    
  699. 
    
  700.     const rscStream = ReactServerDOMServer.renderToReadableStream(
    
  701.       <Page1Ref action={serverAction} permalink="/permalink" />,
    
  702.       webpackMap,
    
  703.     );
    
  704.     const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
    
  705.       ssrManifest: {
    
  706.         moduleMap: null,
    
  707.         moduleLoading: null,
    
  708.       },
    
  709.     });
    
  710.     const ssrStream = await ReactDOMServer.renderToReadableStream(response);
    
  711.     await readIntoContainer(ssrStream);
    
  712. 
    
  713.     expect(container.textContent).toBe('1');
    
  714. 
    
  715.     // Submit the form
    
  716.     const form = container.getElementsByTagName('form')[0];
    
  717.     const {formState} = await submit(form);
    
  718. 
    
  719.     // Simulate an MPA form submission by resetting the container and
    
  720.     // rendering again.
    
  721.     container.innerHTML = '';
    
  722. 
    
  723.     // On the next page, the same server action is rendered again, but in
    
  724.     // a different component tree. However, because a permalink option was
    
  725.     // passed, the state should be preserved.
    
  726.     const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
    
  727.       <Page2Ref action={serverAction} permalink="/permalink" />,
    
  728.       webpackMap,
    
  729.     );
    
  730.     const postbackResponse = ReactServerDOMClient.createFromReadableStream(
    
  731.       postbackRscStream,
    
  732.       {
    
  733.         ssrManifest: {
    
  734.           moduleMap: null,
    
  735.           moduleLoading: null,
    
  736.         },
    
  737.       },
    
  738.     );
    
  739.     const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
    
  740.       postbackResponse,
    
  741.       {formState: formState},
    
  742.     );
    
  743.     await readIntoContainer(postbackSsrStream);
    
  744. 
    
  745.     expect(container.textContent).toBe('2');
    
  746. 
    
  747.     // Now submit the form again. This time, the permalink will be different, so
    
  748.     // the state is not preserved.
    
  749.     const form2 = container.getElementsByTagName('form')[0];
    
  750.     const {formState: formState2} = await submit(form2);
    
  751. 
    
  752.     container.innerHTML = '';
    
  753. 
    
  754.     const postbackRscStream2 = ReactServerDOMServer.renderToReadableStream(
    
  755.       <Page1Ref action={serverAction} permalink="/some-other-permalink" />,
    
  756.       webpackMap,
    
  757.     );
    
  758.     const postbackResponse2 = ReactServerDOMClient.createFromReadableStream(
    
  759.       postbackRscStream2,
    
  760.       {
    
  761.         ssrManifest: {
    
  762.           moduleMap: null,
    
  763.           moduleLoading: null,
    
  764.         },
    
  765.       },
    
  766.     );
    
  767.     const postbackSsrStream2 = await ReactDOMServer.renderToReadableStream(
    
  768.       postbackResponse2,
    
  769.       {formState: formState2},
    
  770.     );
    
  771.     await readIntoContainer(postbackSsrStream2);
    
  772. 
    
  773.     // The state was reset because the permalink didn't match
    
  774.     expect(container.textContent).toBe('1');
    
  775.   });
    
  776. 
    
  777.   // @gate enableFormActions
    
  778.   // @gate enableAsyncActions
    
  779.   it('useFormState can change the action URL with the `permalink` argument', async () => {
    
  780.     const serverAction = serverExports(function action(prevState) {
    
  781.       return {state: prevState.count + 1};
    
  782.     });
    
  783. 
    
  784.     const initialState = {count: 1};
    
  785.     function Client({action}) {
    
  786.       const [state, dispatch] = useFormState(
    
  787.         action,
    
  788.         initialState,
    
  789.         '/permalink',
    
  790.       );
    
  791.       return (
    
  792.         <form action={dispatch}>
    
  793.           <span>Count: {state.count}</span>
    
  794.         </form>
    
  795.       );
    
  796.     }
    
  797.     const ClientRef = await clientExports(Client);
    
  798. 
    
  799.     const rscStream = ReactServerDOMServer.renderToReadableStream(
    
  800.       <ClientRef action={serverAction} />,
    
  801.       webpackMap,
    
  802.     );
    
  803.     const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
    
  804.       ssrManifest: {
    
  805.         moduleMap: null,
    
  806.         moduleLoading: null,
    
  807.       },
    
  808.     });
    
  809.     const ssrStream = await ReactDOMServer.renderToReadableStream(response);
    
  810.     await readIntoContainer(ssrStream);
    
  811. 
    
  812.     const form = container.getElementsByTagName('form')[0];
    
  813.     const span = container.getElementsByTagName('span')[0];
    
  814.     expect(span.textContent).toBe('Count: 1');
    
  815. 
    
  816.     expect(form.action).toBe('http://localhost/permalink');
    
  817.   });
    
  818. 
    
  819.   // @gate enableFormActions
    
  820.   // @gate enableAsyncActions
    
  821.   it('useFormState `permalink` is coerced to string', async () => {
    
  822.     const serverAction = serverExports(function action(prevState) {
    
  823.       return {state: prevState.count + 1};
    
  824.     });
    
  825. 
    
  826.     class Permalink {
    
  827.       toString() {
    
  828.         return '/permalink';
    
  829.       }
    
  830.     }
    
  831. 
    
  832.     const permalink = new Permalink();
    
  833. 
    
  834.     const initialState = {count: 1};
    
  835.     function Client({action}) {
    
  836.       const [state, dispatch] = useFormState(action, initialState, permalink);
    
  837.       return (
    
  838.         <form action={dispatch}>
    
  839.           <span>Count: {state.count}</span>
    
  840.         </form>
    
  841.       );
    
  842.     }
    
  843.     const ClientRef = await clientExports(Client);
    
  844. 
    
  845.     const rscStream = ReactServerDOMServer.renderToReadableStream(
    
  846.       <ClientRef action={serverAction} />,
    
  847.       webpackMap,
    
  848.     );
    
  849.     const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
    
  850.       ssrManifest: {
    
  851.         moduleMap: null,
    
  852.         moduleLoading: null,
    
  853.       },
    
  854.     });
    
  855.     const ssrStream = await ReactDOMServer.renderToReadableStream(response);
    
  856.     await readIntoContainer(ssrStream);
    
  857. 
    
  858.     const form = container.getElementsByTagName('form')[0];
    
  859.     const span = container.getElementsByTagName('span')[0];
    
  860.     expect(span.textContent).toBe('Count: 1');
    
  861. 
    
  862.     expect(form.action).toBe('http://localhost/permalink');
    
  863.   });
    
  864. });