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 '../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. 
    
  19. let act;
    
  20. let container;
    
  21. let React;
    
  22. let ReactDOMServer;
    
  23. let ReactDOMClient;
    
  24. let useFormStatus;
    
  25. let useOptimistic;
    
  26. let useFormState;
    
  27. 
    
  28. describe('ReactDOMFizzForm', () => {
    
  29.   beforeEach(() => {
    
  30.     jest.resetModules();
    
  31.     React = require('react');
    
  32.     ReactDOMServer = require('react-dom/server.browser');
    
  33.     ReactDOMClient = require('react-dom/client');
    
  34.     useFormStatus = require('react-dom').useFormStatus;
    
  35.     useFormState = require('react-dom').useFormState;
    
  36.     useOptimistic = require('react').useOptimistic;
    
  37.     act = require('internal-test-utils').act;
    
  38.     container = document.createElement('div');
    
  39.     document.body.appendChild(container);
    
  40.   });
    
  41. 
    
  42.   afterEach(() => {
    
  43.     document.body.removeChild(container);
    
  44.   });
    
  45. 
    
  46.   function submit(submitter) {
    
  47.     const form = submitter.form || submitter;
    
  48.     if (!submitter.form) {
    
  49.       submitter = undefined;
    
  50.     }
    
  51.     const submitEvent = new Event('submit', {bubbles: true, cancelable: true});
    
  52.     submitEvent.submitter = submitter;
    
  53.     const returnValue = form.dispatchEvent(submitEvent);
    
  54.     if (!returnValue) {
    
  55.       return;
    
  56.     }
    
  57.     const action =
    
  58.       (submitter && submitter.getAttribute('formaction')) || form.action;
    
  59.     if (!/\s*javascript:/i.test(action)) {
    
  60.       throw new Error('Navigate to: ' + action);
    
  61.     }
    
  62.   }
    
  63. 
    
  64.   async function readIntoContainer(stream) {
    
  65.     const reader = stream.getReader();
    
  66.     let result = '';
    
  67.     while (true) {
    
  68.       const {done, value} = await reader.read();
    
  69.       if (done) {
    
  70.         break;
    
  71.       }
    
  72.       result += Buffer.from(value).toString('utf8');
    
  73.     }
    
  74.     const temp = document.createElement('div');
    
  75.     temp.innerHTML = result;
    
  76.     insertNodesAndExecuteScripts(temp, container, null);
    
  77.   }
    
  78. 
    
  79.   // @gate enableFormActions
    
  80.   it('should allow passing a function to form action during SSR', async () => {
    
  81.     const ref = React.createRef();
    
  82.     let foo;
    
  83. 
    
  84.     function action(formData) {
    
  85.       foo = formData.get('foo');
    
  86.     }
    
  87.     function App() {
    
  88.       return (
    
  89.         <form action={action} ref={ref}>
    
  90.           <input type="text" name="foo" defaultValue="bar" />
    
  91.         </form>
    
  92.       );
    
  93.     }
    
  94. 
    
  95.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  96.     await readIntoContainer(stream);
    
  97.     await act(async () => {
    
  98.       ReactDOMClient.hydrateRoot(container, <App />);
    
  99.     });
    
  100. 
    
  101.     submit(ref.current);
    
  102. 
    
  103.     expect(foo).toBe('bar');
    
  104.   });
    
  105. 
    
  106.   // @gate enableFormActions
    
  107.   it('should allow passing a function to an input/button formAction', async () => {
    
  108.     const inputRef = React.createRef();
    
  109.     const buttonRef = React.createRef();
    
  110.     let rootActionCalled = false;
    
  111.     let savedTitle = null;
    
  112.     let deletedTitle = null;
    
  113. 
    
  114.     function action(formData) {
    
  115.       rootActionCalled = true;
    
  116.     }
    
  117. 
    
  118.     function saveItem(formData) {
    
  119.       savedTitle = formData.get('title');
    
  120.     }
    
  121. 
    
  122.     function deleteItem(formData) {
    
  123.       deletedTitle = formData.get('title');
    
  124.     }
    
  125. 
    
  126.     function App() {
    
  127.       return (
    
  128.         <form action={action}>
    
  129.           <input type="text" name="title" defaultValue="Hello" />
    
  130.           <input
    
  131.             type="submit"
    
  132.             formAction={saveItem}
    
  133.             value="Save"
    
  134.             ref={inputRef}
    
  135.           />
    
  136.           <button formAction={deleteItem} ref={buttonRef}>
    
  137.             Delete
    
  138.           </button>
    
  139.         </form>
    
  140.       );
    
  141.     }
    
  142. 
    
  143.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  144.     await readIntoContainer(stream);
    
  145.     await act(async () => {
    
  146.       ReactDOMClient.hydrateRoot(container, <App />);
    
  147.     });
    
  148. 
    
  149.     expect(savedTitle).toBe(null);
    
  150.     expect(deletedTitle).toBe(null);
    
  151. 
    
  152.     submit(inputRef.current);
    
  153.     expect(savedTitle).toBe('Hello');
    
  154.     expect(deletedTitle).toBe(null);
    
  155.     savedTitle = null;
    
  156. 
    
  157.     submit(buttonRef.current);
    
  158.     expect(savedTitle).toBe(null);
    
  159.     expect(deletedTitle).toBe('Hello');
    
  160.     deletedTitle = null;
    
  161. 
    
  162.     expect(rootActionCalled).toBe(false);
    
  163.   });
    
  164. 
    
  165.   // @gate enableFormActions || !__DEV__
    
  166.   it('should warn when passing a function action during SSR and string during hydration', async () => {
    
  167.     function action(formData) {}
    
  168.     function App({isClient}) {
    
  169.       return (
    
  170.         <form action={isClient ? 'action' : action}>
    
  171.           <input type="text" name="foo" defaultValue="bar" />
    
  172.         </form>
    
  173.       );
    
  174.     }
    
  175. 
    
  176.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  177.     await readIntoContainer(stream);
    
  178.     await expect(async () => {
    
  179.       await act(async () => {
    
  180.         ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
    
  181.       });
    
  182.     }).toErrorDev(
    
  183.       'Prop `action` did not match. Server: "function" Client: "action"',
    
  184.     );
    
  185.   });
    
  186. 
    
  187.   // @gate enableFormActions || !__DEV__
    
  188.   it('should ideally warn when passing a string during SSR and function during hydration', async () => {
    
  189.     function action(formData) {}
    
  190.     function App({isClient}) {
    
  191.       return (
    
  192.         <form action={isClient ? action : 'action'}>
    
  193.           <input type="text" name="foo" defaultValue="bar" />
    
  194.         </form>
    
  195.       );
    
  196.     }
    
  197. 
    
  198.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  199.     await readIntoContainer(stream);
    
  200.     // This should ideally warn because only the client provides a function that doesn't line up.
    
  201.     await act(async () => {
    
  202.       ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
    
  203.     });
    
  204.   });
    
  205. 
    
  206.   // @gate enableFormActions || !__DEV__
    
  207.   it('should reset form fields after you update away from hydrated function', async () => {
    
  208.     const formRef = React.createRef();
    
  209.     const inputRef = React.createRef();
    
  210.     const buttonRef = React.createRef();
    
  211.     function action(formData) {}
    
  212.     function App({isUpdate}) {
    
  213.       return (
    
  214.         <form
    
  215.           action={isUpdate ? 'action' : action}
    
  216.           ref={formRef}
    
  217.           method={isUpdate ? 'POST' : null}>
    
  218.           <input
    
  219.             type="submit"
    
  220.             formAction={isUpdate ? 'action' : action}
    
  221.             ref={inputRef}
    
  222.             formTarget={isUpdate ? 'elsewhere' : null}
    
  223.           />
    
  224.           <button
    
  225.             formAction={isUpdate ? 'action' : action}
    
  226.             ref={buttonRef}
    
  227.             formEncType={isUpdate ? 'multipart/form-data' : null}
    
  228.           />
    
  229.         </form>
    
  230.       );
    
  231.     }
    
  232. 
    
  233.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  234.     await readIntoContainer(stream);
    
  235.     let root;
    
  236.     await act(async () => {
    
  237.       root = ReactDOMClient.hydrateRoot(container, <App />);
    
  238.     });
    
  239.     await act(async () => {
    
  240.       root.render(<App isUpdate={true} />);
    
  241.     });
    
  242.     expect(formRef.current.getAttribute('action')).toBe('action');
    
  243.     expect(formRef.current.hasAttribute('encType')).toBe(false);
    
  244.     expect(formRef.current.getAttribute('method')).toBe('POST');
    
  245.     expect(formRef.current.hasAttribute('target')).toBe(false);
    
  246. 
    
  247.     expect(inputRef.current.getAttribute('formAction')).toBe('action');
    
  248.     expect(inputRef.current.hasAttribute('name')).toBe(false);
    
  249.     expect(inputRef.current.hasAttribute('formEncType')).toBe(false);
    
  250.     expect(inputRef.current.hasAttribute('formMethod')).toBe(false);
    
  251.     expect(inputRef.current.getAttribute('formTarget')).toBe('elsewhere');
    
  252. 
    
  253.     expect(buttonRef.current.getAttribute('formAction')).toBe('action');
    
  254.     expect(buttonRef.current.hasAttribute('name')).toBe(false);
    
  255.     expect(buttonRef.current.getAttribute('formEncType')).toBe(
    
  256.       'multipart/form-data',
    
  257.     );
    
  258.     expect(buttonRef.current.hasAttribute('formMethod')).toBe(false);
    
  259.     expect(buttonRef.current.hasAttribute('formTarget')).toBe(false);
    
  260.   });
    
  261. 
    
  262.   // @gate enableFormActions || !__DEV__
    
  263.   it('should reset form fields after you remove a hydrated function', async () => {
    
  264.     const formRef = React.createRef();
    
  265.     const inputRef = React.createRef();
    
  266.     const buttonRef = React.createRef();
    
  267.     function action(formData) {}
    
  268.     function App({isUpdate}) {
    
  269.       return (
    
  270.         <form action={isUpdate ? undefined : action} ref={formRef}>
    
  271.           <input
    
  272.             type="submit"
    
  273.             formAction={isUpdate ? undefined : action}
    
  274.             ref={inputRef}
    
  275.           />
    
  276.           <button formAction={isUpdate ? undefined : action} ref={buttonRef} />
    
  277.         </form>
    
  278.       );
    
  279.     }
    
  280. 
    
  281.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  282.     await readIntoContainer(stream);
    
  283.     let root;
    
  284.     await act(async () => {
    
  285.       root = ReactDOMClient.hydrateRoot(container, <App />);
    
  286.     });
    
  287.     await act(async () => {
    
  288.       root.render(<App isUpdate={true} />);
    
  289.     });
    
  290.     expect(formRef.current.hasAttribute('action')).toBe(false);
    
  291.     expect(formRef.current.hasAttribute('encType')).toBe(false);
    
  292.     expect(formRef.current.hasAttribute('method')).toBe(false);
    
  293.     expect(formRef.current.hasAttribute('target')).toBe(false);
    
  294. 
    
  295.     expect(inputRef.current.hasAttribute('formAction')).toBe(false);
    
  296.     expect(inputRef.current.hasAttribute('name')).toBe(false);
    
  297.     expect(inputRef.current.hasAttribute('formEncType')).toBe(false);
    
  298.     expect(inputRef.current.hasAttribute('formMethod')).toBe(false);
    
  299.     expect(inputRef.current.hasAttribute('formTarget')).toBe(false);
    
  300. 
    
  301.     expect(buttonRef.current.hasAttribute('formAction')).toBe(false);
    
  302.     expect(buttonRef.current.hasAttribute('name')).toBe(false);
    
  303.     expect(buttonRef.current.hasAttribute('formEncType')).toBe(false);
    
  304.     expect(buttonRef.current.hasAttribute('formMethod')).toBe(false);
    
  305.     expect(buttonRef.current.hasAttribute('formTarget')).toBe(false);
    
  306.   });
    
  307. 
    
  308.   // @gate enableFormActions || !__DEV__
    
  309.   it('should restore the form fields even if they were incorrectly set', async () => {
    
  310.     const formRef = React.createRef();
    
  311.     const inputRef = React.createRef();
    
  312.     const buttonRef = React.createRef();
    
  313.     function action(formData) {}
    
  314.     function App({isUpdate}) {
    
  315.       return (
    
  316.         <form
    
  317.           action={isUpdate ? 'action' : action}
    
  318.           ref={formRef}
    
  319.           method="DELETE">
    
  320.           <input
    
  321.             type="submit"
    
  322.             formAction={isUpdate ? 'action' : action}
    
  323.             ref={inputRef}
    
  324.             formTarget="elsewhere"
    
  325.           />
    
  326.           <button
    
  327.             formAction={isUpdate ? 'action' : action}
    
  328.             ref={buttonRef}
    
  329.             formEncType="text/plain"
    
  330.           />
    
  331.         </form>
    
  332.       );
    
  333.     }
    
  334. 
    
  335.     // Specifying the extra form fields are a DEV error, but we expect it
    
  336.     // to eventually still be patched up after an update.
    
  337.     await expect(async () => {
    
  338.       const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  339.       await readIntoContainer(stream);
    
  340.     }).toErrorDev([
    
  341.       'Cannot specify a encType or method for a form that specifies a function as the action.',
    
  342.       'Cannot specify a formTarget for a button that specifies a function as a formAction.',
    
  343.     ]);
    
  344.     let root;
    
  345.     await expect(async () => {
    
  346.       await act(async () => {
    
  347.         root = ReactDOMClient.hydrateRoot(container, <App />);
    
  348.       });
    
  349.     }).toErrorDev(['Prop `formTarget` did not match.']);
    
  350.     await act(async () => {
    
  351.       root.render(<App isUpdate={true} />);
    
  352.     });
    
  353.     expect(formRef.current.getAttribute('action')).toBe('action');
    
  354.     expect(formRef.current.hasAttribute('encType')).toBe(false);
    
  355.     expect(formRef.current.getAttribute('method')).toBe('DELETE');
    
  356.     expect(formRef.current.hasAttribute('target')).toBe(false);
    
  357. 
    
  358.     expect(inputRef.current.getAttribute('formAction')).toBe('action');
    
  359.     expect(inputRef.current.hasAttribute('name')).toBe(false);
    
  360.     expect(inputRef.current.hasAttribute('formEncType')).toBe(false);
    
  361.     expect(inputRef.current.hasAttribute('formMethod')).toBe(false);
    
  362.     expect(inputRef.current.getAttribute('formTarget')).toBe('elsewhere');
    
  363. 
    
  364.     expect(buttonRef.current.getAttribute('formAction')).toBe('action');
    
  365.     expect(buttonRef.current.hasAttribute('name')).toBe(false);
    
  366.     expect(buttonRef.current.getAttribute('formEncType')).toBe('text/plain');
    
  367.     expect(buttonRef.current.hasAttribute('formMethod')).toBe(false);
    
  368.     expect(buttonRef.current.hasAttribute('formTarget')).toBe(false);
    
  369.   });
    
  370. 
    
  371.   // @gate enableFormActions
    
  372.   // @gate enableAsyncActions
    
  373.   it('useFormStatus is not pending during server render', async () => {
    
  374.     function App() {
    
  375.       const {pending} = useFormStatus();
    
  376.       return 'Pending: ' + pending;
    
  377.     }
    
  378. 
    
  379.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  380.     await readIntoContainer(stream);
    
  381.     expect(container.textContent).toBe('Pending: false');
    
  382. 
    
  383.     await act(() => ReactDOMClient.hydrateRoot(container, <App />));
    
  384.     expect(container.textContent).toBe('Pending: false');
    
  385.   });
    
  386. 
    
  387.   // @gate enableFormActions
    
  388.   it('should replay a form action after hydration', async () => {
    
  389.     let foo;
    
  390.     function action(formData) {
    
  391.       foo = formData.get('foo');
    
  392.     }
    
  393.     function App() {
    
  394.       return (
    
  395.         <form action={action}>
    
  396.           <input type="text" name="foo" defaultValue="bar" />
    
  397.         </form>
    
  398.       );
    
  399.     }
    
  400. 
    
  401.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  402.     await readIntoContainer(stream);
    
  403. 
    
  404.     // Dispatch an event before hydration
    
  405.     submit(container.getElementsByTagName('form')[0]);
    
  406. 
    
  407.     await act(async () => {
    
  408.       ReactDOMClient.hydrateRoot(container, <App />);
    
  409.     });
    
  410. 
    
  411.     // It should've now been replayed
    
  412.     expect(foo).toBe('bar');
    
  413.   });
    
  414. 
    
  415.   // @gate enableFormActions
    
  416.   it('should replay input/button formAction', async () => {
    
  417.     let rootActionCalled = false;
    
  418.     let savedTitle = null;
    
  419.     let deletedTitle = null;
    
  420. 
    
  421.     function action(formData) {
    
  422.       rootActionCalled = true;
    
  423.     }
    
  424. 
    
  425.     function saveItem(formData) {
    
  426.       savedTitle = formData.get('title');
    
  427.     }
    
  428. 
    
  429.     function deleteItem(formData) {
    
  430.       deletedTitle = formData.get('title');
    
  431.     }
    
  432. 
    
  433.     function App() {
    
  434.       return (
    
  435.         <form action={action}>
    
  436.           <input type="text" name="title" defaultValue="Hello" />
    
  437.           <input type="submit" formAction={saveItem} value="Save" />
    
  438.           <button formAction={deleteItem}>Delete</button>
    
  439.         </form>
    
  440.       );
    
  441.     }
    
  442. 
    
  443.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  444.     await readIntoContainer(stream);
    
  445. 
    
  446.     submit(container.getElementsByTagName('input')[1]);
    
  447.     submit(container.getElementsByTagName('button')[0]);
    
  448. 
    
  449.     await act(async () => {
    
  450.       ReactDOMClient.hydrateRoot(container, <App />);
    
  451.     });
    
  452. 
    
  453.     expect(savedTitle).toBe('Hello');
    
  454.     expect(deletedTitle).toBe('Hello');
    
  455.     expect(rootActionCalled).toBe(false);
    
  456.   });
    
  457. 
    
  458.   // @gate enableAsyncActions
    
  459.   it('useOptimistic returns passthrough value', async () => {
    
  460.     function App() {
    
  461.       const [optimisticState] = useOptimistic('hi');
    
  462.       return optimisticState;
    
  463.     }
    
  464. 
    
  465.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  466.     await readIntoContainer(stream);
    
  467.     expect(container.textContent).toBe('hi');
    
  468. 
    
  469.     await act(async () => {
    
  470.       ReactDOMClient.hydrateRoot(container, <App />);
    
  471.     });
    
  472.     expect(container.textContent).toBe('hi');
    
  473.   });
    
  474. 
    
  475.   // @gate enableFormActions
    
  476.   // @gate enableAsyncActions
    
  477.   it('useFormState returns initial state', async () => {
    
  478.     async function action(state) {
    
  479.       return state;
    
  480.     }
    
  481. 
    
  482.     function App() {
    
  483.       const [state] = useFormState(action, 0);
    
  484.       return state;
    
  485.     }
    
  486. 
    
  487.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  488.     await readIntoContainer(stream);
    
  489.     expect(container.textContent).toBe('0');
    
  490. 
    
  491.     await act(async () => {
    
  492.       ReactDOMClient.hydrateRoot(container, <App />);
    
  493.     });
    
  494.     expect(container.textContent).toBe('0');
    
  495.   });
    
  496. 
    
  497.   // @gate enableFormActions
    
  498.   it('can provide a custom action on the server for actions', async () => {
    
  499.     const ref = React.createRef();
    
  500.     let foo;
    
  501. 
    
  502.     function action(formData) {
    
  503.       foo = formData.get('foo');
    
  504.     }
    
  505.     action.$$FORM_ACTION = function (identifierPrefix) {
    
  506.       const extraFields = new FormData();
    
  507.       extraFields.append(identifierPrefix + 'hello', 'world');
    
  508.       return {
    
  509.         action: this.name,
    
  510.         name: identifierPrefix,
    
  511.         method: 'POST',
    
  512.         encType: 'multipart/form-data',
    
  513.         target: 'self',
    
  514.         data: extraFields,
    
  515.       };
    
  516.     };
    
  517.     function App() {
    
  518.       return (
    
  519.         <form action={action} ref={ref} method={null}>
    
  520.           <input type="text" name="foo" defaultValue="bar" />
    
  521.         </form>
    
  522.       );
    
  523.     }
    
  524. 
    
  525.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  526.     await readIntoContainer(stream);
    
  527. 
    
  528.     const form = container.firstChild;
    
  529.     expect(form.getAttribute('action')).toBe('action');
    
  530.     expect(form.getAttribute('method')).toBe('POST');
    
  531.     expect(form.getAttribute('enctype')).toBe('multipart/form-data');
    
  532.     expect(form.getAttribute('target')).toBe('self');
    
  533.     const formActionName = form.firstChild.getAttribute('name');
    
  534.     expect(
    
  535.       container
    
  536.         .querySelector('input[name="' + formActionName + 'hello"]')
    
  537.         .getAttribute('value'),
    
  538.     ).toBe('world');
    
  539. 
    
  540.     await act(async () => {
    
  541.       ReactDOMClient.hydrateRoot(container, <App />);
    
  542.     });
    
  543. 
    
  544.     submit(ref.current);
    
  545. 
    
  546.     expect(foo).toBe('bar');
    
  547.   });
    
  548. 
    
  549.   // @gate enableFormActions
    
  550.   it('can provide a custom action on buttons the server for actions', async () => {
    
  551.     const hiddenRef = React.createRef();
    
  552.     const inputRef = React.createRef();
    
  553.     const buttonRef = React.createRef();
    
  554.     let foo;
    
  555. 
    
  556.     function action(formData) {
    
  557.       foo = formData.get('foo');
    
  558.     }
    
  559.     action.$$FORM_ACTION = function (identifierPrefix) {
    
  560.       const extraFields = new FormData();
    
  561.       extraFields.append(identifierPrefix + 'hello', 'world');
    
  562.       return {
    
  563.         action: this.name,
    
  564.         name: identifierPrefix,
    
  565.         method: 'POST',
    
  566.         encType: 'multipart/form-data',
    
  567.         target: 'self',
    
  568.         data: extraFields,
    
  569.       };
    
  570.     };
    
  571.     function App() {
    
  572.       return (
    
  573.         <form>
    
  574.           <input type="hidden" name="foo" value="bar" ref={hiddenRef} />
    
  575.           <input
    
  576.             type="submit"
    
  577.             formAction={action}
    
  578.             method={null}
    
  579.             ref={inputRef}
    
  580.           />
    
  581.           <button formAction={action} ref={buttonRef} target={null} />
    
  582.         </form>
    
  583.       );
    
  584.     }
    
  585. 
    
  586.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  587.     await readIntoContainer(stream);
    
  588. 
    
  589.     const input = container.getElementsByTagName('input')[1];
    
  590.     const button = container.getElementsByTagName('button')[0];
    
  591.     expect(input.getAttribute('formaction')).toBe('action');
    
  592.     expect(input.getAttribute('formmethod')).toBe('POST');
    
  593.     expect(input.getAttribute('formenctype')).toBe('multipart/form-data');
    
  594.     expect(input.getAttribute('formtarget')).toBe('self');
    
  595.     expect(button.getAttribute('formaction')).toBe('action');
    
  596.     expect(button.getAttribute('formmethod')).toBe('POST');
    
  597.     expect(button.getAttribute('formenctype')).toBe('multipart/form-data');
    
  598.     expect(button.getAttribute('formtarget')).toBe('self');
    
  599.     const inputName = input.getAttribute('name');
    
  600.     const buttonName = button.getAttribute('name');
    
  601.     expect(
    
  602.       container
    
  603.         .querySelector('input[name="' + inputName + 'hello"]')
    
  604.         .getAttribute('value'),
    
  605.     ).toBe('world');
    
  606.     expect(
    
  607.       container
    
  608.         .querySelector('input[name="' + buttonName + 'hello"]')
    
  609.         .getAttribute('value'),
    
  610.     ).toBe('world');
    
  611. 
    
  612.     await act(async () => {
    
  613.       ReactDOMClient.hydrateRoot(container, <App />);
    
  614.     });
    
  615. 
    
  616.     expect(hiddenRef.current.name).toBe('foo');
    
  617. 
    
  618.     submit(inputRef.current);
    
  619. 
    
  620.     expect(foo).toBe('bar');
    
  621. 
    
  622.     foo = null;
    
  623. 
    
  624.     submit(buttonRef.current);
    
  625. 
    
  626.     expect(foo).toBe('bar');
    
  627.   });
    
  628. 
    
  629.   // @gate enableFormActions
    
  630.   it('can hydrate hidden fields in the beginning of a form', async () => {
    
  631.     const hiddenRef = React.createRef();
    
  632. 
    
  633.     let invoked = false;
    
  634.     function action(formData) {
    
  635.       invoked = true;
    
  636.     }
    
  637.     action.$$FORM_ACTION = function (identifierPrefix) {
    
  638.       const extraFields = new FormData();
    
  639.       extraFields.append(identifierPrefix + 'hello', 'world');
    
  640.       return {
    
  641.         action: '',
    
  642.         name: identifierPrefix,
    
  643.         method: 'POST',
    
  644.         encType: 'multipart/form-data',
    
  645.         data: extraFields,
    
  646.       };
    
  647.     };
    
  648.     function App() {
    
  649.       return (
    
  650.         <form action={action}>
    
  651.           <input type="hidden" name="bar" defaultValue="baz" ref={hiddenRef} />
    
  652.           <input type="text" name="foo" defaultValue="bar" />
    
  653.         </form>
    
  654.       );
    
  655.     }
    
  656. 
    
  657.     const stream = await ReactDOMServer.renderToReadableStream(<App />);
    
  658.     await readIntoContainer(stream);
    
  659. 
    
  660.     const barField = container.querySelector('[name=bar]');
    
  661. 
    
  662.     await act(async () => {
    
  663.       ReactDOMClient.hydrateRoot(container, <App />);
    
  664.     });
    
  665. 
    
  666.     expect(hiddenRef.current).toBe(barField);
    
  667. 
    
  668.     expect(hiddenRef.current.name).toBe('bar');
    
  669.     expect(hiddenRef.current.value).toBe('baz');
    
  670. 
    
  671.     expect(container.querySelectorAll('[name=bar]').length).toBe(1);
    
  672. 
    
  673.     submit(hiddenRef.current.form);
    
  674. 
    
  675.     expect(invoked).toBe(true);
    
  676.   });
    
  677. });