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. global.IS_REACT_ACT_ENVIRONMENT = true;
    
  13. 
    
  14. // Our current version of JSDOM doesn't implement the event dispatching
    
  15. // so we polyfill it.
    
  16. const NativeFormData = global.FormData;
    
  17. const FormDataPolyfill = function FormData(form) {
    
  18.   const formData = new NativeFormData(form);
    
  19.   const formDataEvent = new Event('formdata', {
    
  20.     bubbles: true,
    
  21.     cancelable: false,
    
  22.   });
    
  23.   formDataEvent.formData = formData;
    
  24.   form.dispatchEvent(formDataEvent);
    
  25.   return formData;
    
  26. };
    
  27. NativeFormData.prototype.constructor = FormDataPolyfill;
    
  28. global.FormData = FormDataPolyfill;
    
  29. 
    
  30. describe('ReactDOMForm', () => {
    
  31.   let act;
    
  32.   let container;
    
  33.   let React;
    
  34.   let ReactDOM;
    
  35.   let ReactDOMClient;
    
  36.   let Scheduler;
    
  37.   let assertLog;
    
  38.   let waitForThrow;
    
  39.   let useState;
    
  40.   let Suspense;
    
  41.   let startTransition;
    
  42.   let textCache;
    
  43.   let useFormStatus;
    
  44.   let useFormState;
    
  45. 
    
  46.   beforeEach(() => {
    
  47.     jest.resetModules();
    
  48.     React = require('react');
    
  49.     ReactDOM = require('react-dom');
    
  50.     ReactDOMClient = require('react-dom/client');
    
  51.     Scheduler = require('scheduler');
    
  52.     act = require('internal-test-utils').act;
    
  53.     assertLog = require('internal-test-utils').assertLog;
    
  54.     waitForThrow = require('internal-test-utils').waitForThrow;
    
  55.     useState = React.useState;
    
  56.     Suspense = React.Suspense;
    
  57.     startTransition = React.startTransition;
    
  58.     useFormStatus = ReactDOM.useFormStatus;
    
  59.     useFormState = ReactDOM.useFormState;
    
  60.     container = document.createElement('div');
    
  61.     document.body.appendChild(container);
    
  62. 
    
  63.     textCache = new Map();
    
  64.   });
    
  65. 
    
  66.   function resolveText(text) {
    
  67.     const record = textCache.get(text);
    
  68.     if (record === undefined) {
    
  69.       const newRecord = {
    
  70.         status: 'resolved',
    
  71.         value: text,
    
  72.       };
    
  73.       textCache.set(text, newRecord);
    
  74.     } else if (record.status === 'pending') {
    
  75.       const thenable = record.value;
    
  76.       record.status = 'resolved';
    
  77.       record.value = text;
    
  78.       thenable.pings.forEach(t => t());
    
  79.     }
    
  80.   }
    
  81.   function resolveText(text) {
    
  82.     const record = textCache.get(text);
    
  83.     if (record === undefined) {
    
  84.       const newRecord = {
    
  85.         status: 'resolved',
    
  86.         value: text,
    
  87.       };
    
  88.       textCache.set(text, newRecord);
    
  89.     } else if (record.status === 'pending') {
    
  90.       const thenable = record.value;
    
  91.       record.status = 'resolved';
    
  92.       record.value = text;
    
  93.       thenable.pings.forEach(t => t(text));
    
  94.     }
    
  95.   }
    
  96. 
    
  97.   function readText(text) {
    
  98.     const record = textCache.get(text);
    
  99.     if (record !== undefined) {
    
  100.       switch (record.status) {
    
  101.         case 'pending':
    
  102.           Scheduler.log(`Suspend! [${text}]`);
    
  103.           throw record.value;
    
  104.         case 'rejected':
    
  105.           throw record.value;
    
  106.         case 'resolved':
    
  107.           return record.value;
    
  108.       }
    
  109.     } else {
    
  110.       Scheduler.log(`Suspend! [${text}]`);
    
  111.       const thenable = {
    
  112.         pings: [],
    
  113.         then(resolve) {
    
  114.           if (newRecord.status === 'pending') {
    
  115.             thenable.pings.push(resolve);
    
  116.           } else {
    
  117.             Promise.resolve().then(() => resolve(newRecord.value));
    
  118.           }
    
  119.         },
    
  120.       };
    
  121. 
    
  122.       const newRecord = {
    
  123.         status: 'pending',
    
  124.         value: thenable,
    
  125.       };
    
  126.       textCache.set(text, newRecord);
    
  127. 
    
  128.       throw thenable;
    
  129.     }
    
  130.   }
    
  131. 
    
  132.   function getText(text) {
    
  133.     const record = textCache.get(text);
    
  134.     if (record === undefined) {
    
  135.       const thenable = {
    
  136.         pings: [],
    
  137.         then(resolve) {
    
  138.           if (newRecord.status === 'pending') {
    
  139.             thenable.pings.push(resolve);
    
  140.           } else {
    
  141.             Promise.resolve().then(() => resolve(newRecord.value));
    
  142.           }
    
  143.         },
    
  144.       };
    
  145.       const newRecord = {
    
  146.         status: 'pending',
    
  147.         value: thenable,
    
  148.       };
    
  149.       textCache.set(text, newRecord);
    
  150.       return thenable;
    
  151.     } else {
    
  152.       switch (record.status) {
    
  153.         case 'pending':
    
  154.           return record.value;
    
  155.         case 'rejected':
    
  156.           return Promise.reject(record.value);
    
  157.         case 'resolved':
    
  158.           return Promise.resolve(record.value);
    
  159.       }
    
  160.     }
    
  161.   }
    
  162. 
    
  163.   function Text({text}) {
    
  164.     Scheduler.log(text);
    
  165.     return text;
    
  166.   }
    
  167. 
    
  168.   function AsyncText({text}) {
    
  169.     readText(text);
    
  170.     Scheduler.log(text);
    
  171.     return text;
    
  172.   }
    
  173. 
    
  174.   afterEach(() => {
    
  175.     document.body.removeChild(container);
    
  176.   });
    
  177. 
    
  178.   async function submit(submitter) {
    
  179.     await act(() => {
    
  180.       const form = submitter.form || submitter;
    
  181.       if (!submitter.form) {
    
  182.         submitter = undefined;
    
  183.       }
    
  184.       const submitEvent = new Event('submit', {
    
  185.         bubbles: true,
    
  186.         cancelable: true,
    
  187.       });
    
  188.       submitEvent.submitter = submitter;
    
  189.       const returnValue = form.dispatchEvent(submitEvent);
    
  190.       if (!returnValue) {
    
  191.         return;
    
  192.       }
    
  193.       const action =
    
  194.         (submitter && submitter.getAttribute('formaction')) || form.action;
    
  195.       if (!/\s*javascript:/i.test(action)) {
    
  196.         throw new Error('Navigate to: ' + action);
    
  197.       }
    
  198.     });
    
  199.   }
    
  200. 
    
  201.   // @gate enableFormActions
    
  202.   it('should allow passing a function to form action', async () => {
    
  203.     const ref = React.createRef();
    
  204.     let foo;
    
  205. 
    
  206.     function action(formData) {
    
  207.       foo = formData.get('foo');
    
  208.     }
    
  209. 
    
  210.     const root = ReactDOMClient.createRoot(container);
    
  211.     await act(async () => {
    
  212.       root.render(
    
  213.         <form action={action} ref={ref}>
    
  214.           <input type="text" name="foo" defaultValue="bar" />
    
  215.         </form>,
    
  216.       );
    
  217.     });
    
  218. 
    
  219.     await submit(ref.current);
    
  220. 
    
  221.     expect(foo).toBe('bar');
    
  222. 
    
  223.     // Try updating the action
    
  224. 
    
  225.     function action2(formData) {
    
  226.       foo = formData.get('foo') + '2';
    
  227.     }
    
  228. 
    
  229.     await act(async () => {
    
  230.       root.render(
    
  231.         <form action={action2} ref={ref}>
    
  232.           <input type="text" name="foo" defaultValue="bar" />
    
  233.         </form>,
    
  234.       );
    
  235.     });
    
  236. 
    
  237.     await submit(ref.current);
    
  238. 
    
  239.     expect(foo).toBe('bar2');
    
  240.   });
    
  241. 
    
  242.   // @gate enableFormActions
    
  243.   it('should allow passing a function to an input/button formAction', async () => {
    
  244.     const inputRef = React.createRef();
    
  245.     const buttonRef = React.createRef();
    
  246.     let rootActionCalled = false;
    
  247.     let savedTitle = null;
    
  248.     let deletedTitle = null;
    
  249. 
    
  250.     function action(formData) {
    
  251.       rootActionCalled = true;
    
  252.     }
    
  253. 
    
  254.     function saveItem(formData) {
    
  255.       savedTitle = formData.get('title');
    
  256.     }
    
  257. 
    
  258.     function deleteItem(formData) {
    
  259.       deletedTitle = formData.get('title');
    
  260.     }
    
  261. 
    
  262.     const root = ReactDOMClient.createRoot(container);
    
  263.     await act(async () => {
    
  264.       root.render(
    
  265.         <form action={action}>
    
  266.           <input type="text" name="title" defaultValue="Hello" />
    
  267.           <input
    
  268.             type="submit"
    
  269.             formAction={saveItem}
    
  270.             value="Save"
    
  271.             ref={inputRef}
    
  272.           />
    
  273.           <button formAction={deleteItem} ref={buttonRef}>
    
  274.             Delete
    
  275.           </button>
    
  276.         </form>,
    
  277.       );
    
  278.     });
    
  279. 
    
  280.     expect(savedTitle).toBe(null);
    
  281.     expect(deletedTitle).toBe(null);
    
  282. 
    
  283.     await submit(inputRef.current);
    
  284.     expect(savedTitle).toBe('Hello');
    
  285.     expect(deletedTitle).toBe(null);
    
  286.     savedTitle = null;
    
  287. 
    
  288.     await submit(buttonRef.current);
    
  289.     expect(savedTitle).toBe(null);
    
  290.     expect(deletedTitle).toBe('Hello');
    
  291.     deletedTitle = null;
    
  292. 
    
  293.     // Try updating the actions
    
  294. 
    
  295.     function saveItem2(formData) {
    
  296.       savedTitle = formData.get('title') + '2';
    
  297.     }
    
  298. 
    
  299.     function deleteItem2(formData) {
    
  300.       deletedTitle = formData.get('title') + '2';
    
  301.     }
    
  302. 
    
  303.     await act(async () => {
    
  304.       root.render(
    
  305.         <form action={action}>
    
  306.           <input type="text" name="title" defaultValue="Hello" />
    
  307.           <input
    
  308.             type="submit"
    
  309.             formAction={saveItem2}
    
  310.             value="Save"
    
  311.             ref={inputRef}
    
  312.           />
    
  313.           <button formAction={deleteItem2} ref={buttonRef}>
    
  314.             Delete
    
  315.           </button>
    
  316.         </form>,
    
  317.       );
    
  318.     });
    
  319. 
    
  320.     expect(savedTitle).toBe(null);
    
  321.     expect(deletedTitle).toBe(null);
    
  322. 
    
  323.     await submit(inputRef.current);
    
  324.     expect(savedTitle).toBe('Hello2');
    
  325.     expect(deletedTitle).toBe(null);
    
  326.     savedTitle = null;
    
  327. 
    
  328.     await submit(buttonRef.current);
    
  329.     expect(savedTitle).toBe(null);
    
  330.     expect(deletedTitle).toBe('Hello2');
    
  331. 
    
  332.     expect(rootActionCalled).toBe(false);
    
  333.   });
    
  334. 
    
  335.   // @gate enableFormActions || !__DEV__
    
  336.   it('should allow preventing default to block the action', async () => {
    
  337.     const ref = React.createRef();
    
  338.     let actionCalled = false;
    
  339. 
    
  340.     function action(formData) {
    
  341.       actionCalled = true;
    
  342.     }
    
  343. 
    
  344.     const root = ReactDOMClient.createRoot(container);
    
  345.     await act(async () => {
    
  346.       root.render(
    
  347.         <form action={action} ref={ref} onSubmit={e => e.preventDefault()}>
    
  348.           <input type="text" name="foo" defaultValue="bar" />
    
  349.         </form>,
    
  350.       );
    
  351.     });
    
  352. 
    
  353.     await submit(ref.current);
    
  354. 
    
  355.     expect(actionCalled).toBe(false);
    
  356.   });
    
  357. 
    
  358.   // @gate enableFormActions
    
  359.   it('should only submit the inner of nested forms', async () => {
    
  360.     const ref = React.createRef();
    
  361.     let data;
    
  362. 
    
  363.     function outerAction(formData) {
    
  364.       data = formData.get('data') + 'outer';
    
  365.     }
    
  366.     function innerAction(formData) {
    
  367.       data = formData.get('data') + 'inner';
    
  368.     }
    
  369. 
    
  370.     const root = ReactDOMClient.createRoot(container);
    
  371.     await expect(async () => {
    
  372.       await act(async () => {
    
  373.         // This isn't valid HTML but just in case.
    
  374.         root.render(
    
  375.           <form action={outerAction}>
    
  376.             <input type="text" name="data" defaultValue="outer" />
    
  377.             <form action={innerAction} ref={ref}>
    
  378.               <input type="text" name="data" defaultValue="inner" />
    
  379.             </form>
    
  380.           </form>,
    
  381.         );
    
  382.       });
    
  383.     }).toErrorDev([
    
  384.       'Warning: validateDOMNesting(...): <form> cannot appear as a descendant of <form>.' +
    
  385.         '\n    in form (at **)' +
    
  386.         '\n    in form (at **)',
    
  387.     ]);
    
  388. 
    
  389.     await submit(ref.current);
    
  390. 
    
  391.     expect(data).toBe('innerinner');
    
  392.   });
    
  393. 
    
  394.   // @gate enableFormActions
    
  395.   it('should only submit once if one root is nested inside the other', async () => {
    
  396.     const ref = React.createRef();
    
  397.     let outerCalled = 0;
    
  398.     let innerCalled = 0;
    
  399.     let bubbledSubmit = false;
    
  400. 
    
  401.     function outerAction(formData) {
    
  402.       outerCalled++;
    
  403.     }
    
  404. 
    
  405.     function innerAction(formData) {
    
  406.       innerCalled++;
    
  407.     }
    
  408. 
    
  409.     const innerContainerRef = React.createRef();
    
  410.     const outerRoot = ReactDOMClient.createRoot(container);
    
  411.     await act(async () => {
    
  412.       outerRoot.render(
    
  413.         // Nesting forms isn't valid HTML but just in case.
    
  414.         <div onSubmit={() => (bubbledSubmit = true)}>
    
  415.           <form action={outerAction}>
    
  416.             <div ref={innerContainerRef} />
    
  417.           </form>
    
  418.         </div>,
    
  419.       );
    
  420.     });
    
  421. 
    
  422.     const innerRoot = ReactDOMClient.createRoot(innerContainerRef.current);
    
  423.     await act(async () => {
    
  424.       innerRoot.render(
    
  425.         <form action={innerAction} ref={ref}>
    
  426.           <input type="text" name="data" defaultValue="inner" />
    
  427.         </form>,
    
  428.       );
    
  429.     });
    
  430. 
    
  431.     await submit(ref.current);
    
  432. 
    
  433.     expect(bubbledSubmit).toBe(true);
    
  434.     expect(outerCalled).toBe(0);
    
  435.     expect(innerCalled).toBe(1);
    
  436.   });
    
  437. 
    
  438.   // @gate enableFormActions
    
  439.   it('should only submit once if a portal is nested inside its own root', async () => {
    
  440.     const ref = React.createRef();
    
  441.     let outerCalled = 0;
    
  442.     let innerCalled = 0;
    
  443.     let bubbledSubmit = false;
    
  444. 
    
  445.     function outerAction(formData) {
    
  446.       outerCalled++;
    
  447.     }
    
  448. 
    
  449.     function innerAction(formData) {
    
  450.       innerCalled++;
    
  451.     }
    
  452. 
    
  453.     const innerContainer = document.createElement('div');
    
  454.     const innerContainerRef = React.createRef();
    
  455.     const outerRoot = ReactDOMClient.createRoot(container);
    
  456.     await act(async () => {
    
  457.       outerRoot.render(
    
  458.         // Nesting forms isn't valid HTML but just in case.
    
  459.         <div onSubmit={() => (bubbledSubmit = true)}>
    
  460.           <form action={outerAction}>
    
  461.             <div ref={innerContainerRef} />
    
  462.             {ReactDOM.createPortal(
    
  463.               <form action={innerAction} ref={ref}>
    
  464.                 <input type="text" name="data" defaultValue="inner" />
    
  465.               </form>,
    
  466.               innerContainer,
    
  467.             )}
    
  468.           </form>
    
  469.         </div>,
    
  470.       );
    
  471.     });
    
  472. 
    
  473.     innerContainerRef.current.appendChild(innerContainer);
    
  474. 
    
  475.     await submit(ref.current);
    
  476. 
    
  477.     expect(bubbledSubmit).toBe(true);
    
  478.     expect(outerCalled).toBe(0);
    
  479.     expect(innerCalled).toBe(1);
    
  480.   });
    
  481. 
    
  482.   // @gate enableFormActions
    
  483.   it('can read the clicked button in the formdata event', async () => {
    
  484.     const inputRef = React.createRef();
    
  485.     const buttonRef = React.createRef();
    
  486.     let button;
    
  487.     let title;
    
  488. 
    
  489.     function action(formData) {
    
  490.       button = formData.get('button');
    
  491.       title = formData.get('title');
    
  492.     }
    
  493. 
    
  494.     const root = ReactDOMClient.createRoot(container);
    
  495.     await act(async () => {
    
  496.       root.render(
    
  497.         <form action={action}>
    
  498.           <input type="text" name="title" defaultValue="hello" />
    
  499.           <input type="submit" name="button" value="save" />
    
  500.           <input type="submit" name="button" value="delete" ref={inputRef} />
    
  501.           <button name="button" value="edit" ref={buttonRef}>
    
  502.             Edit
    
  503.           </button>
    
  504.         </form>,
    
  505.       );
    
  506.     });
    
  507. 
    
  508.     container.addEventListener('formdata', e => {
    
  509.       // Process in the formdata event somehow
    
  510.       if (e.formData.get('button') === 'delete') {
    
  511.         e.formData.delete('title');
    
  512.       }
    
  513.     });
    
  514. 
    
  515.     await submit(inputRef.current);
    
  516. 
    
  517.     expect(button).toBe('delete');
    
  518.     expect(title).toBe(null);
    
  519. 
    
  520.     await submit(buttonRef.current);
    
  521. 
    
  522.     expect(button).toBe('edit');
    
  523.     expect(title).toBe('hello');
    
  524. 
    
  525.     // Ensure that the type field got correctly restored
    
  526.     expect(inputRef.current.getAttribute('type')).toBe('submit');
    
  527.     expect(buttonRef.current.getAttribute('type')).toBe(null);
    
  528.   });
    
  529. 
    
  530.   // @gate enableFormActions
    
  531.   it('excludes the submitter name when the submitter is a function action', async () => {
    
  532.     const inputRef = React.createRef();
    
  533.     const buttonRef = React.createRef();
    
  534.     let button;
    
  535. 
    
  536.     function action(formData) {
    
  537.       // A function action cannot control the name since it might be controlled by the server
    
  538.       // so we need to make sure it doesn't get into the FormData.
    
  539.       button = formData.get('button');
    
  540.     }
    
  541. 
    
  542.     const root = ReactDOMClient.createRoot(container);
    
  543.     await expect(async () => {
    
  544.       await act(async () => {
    
  545.         root.render(
    
  546.           <form>
    
  547.             <input
    
  548.               type="submit"
    
  549.               name="button"
    
  550.               value="delete"
    
  551.               ref={inputRef}
    
  552.               formAction={action}
    
  553.             />
    
  554.             <button
    
  555.               name="button"
    
  556.               value="edit"
    
  557.               ref={buttonRef}
    
  558.               formAction={action}>
    
  559.               Edit
    
  560.             </button>
    
  561.           </form>,
    
  562.         );
    
  563.       });
    
  564.     }).toErrorDev([
    
  565.       'Cannot specify a "name" prop for a button that specifies a function as a formAction.',
    
  566.     ]);
    
  567. 
    
  568.     await submit(inputRef.current);
    
  569. 
    
  570.     expect(button).toBe(null);
    
  571. 
    
  572.     await submit(buttonRef.current);
    
  573. 
    
  574.     expect(button).toBe(null);
    
  575. 
    
  576.     // Ensure that the type field got correctly restored
    
  577.     expect(inputRef.current.getAttribute('type')).toBe('submit');
    
  578.     expect(buttonRef.current.getAttribute('type')).toBe(null);
    
  579.   });
    
  580. 
    
  581.   // @gate enableFormActions || !__DEV__
    
  582.   it('allows a non-function formaction to override a function one', async () => {
    
  583.     const ref = React.createRef();
    
  584.     let actionCalled = false;
    
  585. 
    
  586.     function action(formData) {
    
  587.       actionCalled = true;
    
  588.     }
    
  589. 
    
  590.     const root = ReactDOMClient.createRoot(container);
    
  591.     await act(async () => {
    
  592.       root.render(
    
  593.         <form action={action}>
    
  594.           <input
    
  595.             type="submit"
    
  596.             formAction="http://example.com/submit"
    
  597.             ref={ref}
    
  598.           />
    
  599.         </form>,
    
  600.       );
    
  601.     });
    
  602. 
    
  603.     let nav;
    
  604.     try {
    
  605.       await submit(ref.current);
    
  606.     } catch (x) {
    
  607.       nav = x.message;
    
  608.     }
    
  609.     expect(nav).toBe('Navigate to: http://example.com/submit');
    
  610.     expect(actionCalled).toBe(false);
    
  611.   });
    
  612. 
    
  613.   // @gate enableFormActions || !__DEV__
    
  614.   it('allows a non-react html formaction to be invoked', async () => {
    
  615.     let actionCalled = false;
    
  616. 
    
  617.     function action(formData) {
    
  618.       actionCalled = true;
    
  619.     }
    
  620. 
    
  621.     const root = ReactDOMClient.createRoot(container);
    
  622.     await act(async () => {
    
  623.       root.render(
    
  624.         <form
    
  625.           action={action}
    
  626.           dangerouslySetInnerHTML={{
    
  627.             __html: `
    
  628.             <input
    
  629.               type="submit"
    
  630.               formAction="http://example.com/submit"
    
  631.             />
    
  632.           `,
    
  633.           }}
    
  634.         />,
    
  635.       );
    
  636.     });
    
  637. 
    
  638.     const node = container.getElementsByTagName('input')[0];
    
  639.     let nav;
    
  640.     try {
    
  641.       await submit(node);
    
  642.     } catch (x) {
    
  643.       nav = x.message;
    
  644.     }
    
  645.     expect(nav).toBe('Navigate to: http://example.com/submit');
    
  646.     expect(actionCalled).toBe(false);
    
  647.   });
    
  648. 
    
  649.   // @gate enableFormActions
    
  650.   // @gate enableAsyncActions
    
  651.   it('form actions are transitions', async () => {
    
  652.     const formRef = React.createRef();
    
  653. 
    
  654.     function Status() {
    
  655.       const {pending} = useFormStatus();
    
  656.       return pending ? <Text text="Pending..." /> : null;
    
  657.     }
    
  658. 
    
  659.     function App() {
    
  660.       const [state, setState] = useState('Initial');
    
  661.       return (
    
  662.         <form action={() => setState('Updated')} ref={formRef}>
    
  663.           <Status />
    
  664.           <Suspense fallback={<Text text="Loading..." />}>
    
  665.             <AsyncText text={state} />
    
  666.           </Suspense>
    
  667.         </form>
    
  668.       );
    
  669.     }
    
  670. 
    
  671.     const root = ReactDOMClient.createRoot(container);
    
  672.     await resolveText('Initial');
    
  673.     await act(() => root.render(<App />));
    
  674.     assertLog(['Initial']);
    
  675.     expect(container.textContent).toBe('Initial');
    
  676. 
    
  677.     // This should suspend because form actions are implicitly wrapped
    
  678.     // in startTransition.
    
  679.     await submit(formRef.current);
    
  680.     assertLog(['Pending...', 'Suspend! [Updated]', 'Loading...']);
    
  681.     expect(container.textContent).toBe('Pending...Initial');
    
  682. 
    
  683.     await act(() => resolveText('Updated'));
    
  684.     assertLog(['Updated']);
    
  685.     expect(container.textContent).toBe('Updated');
    
  686.   });
    
  687. 
    
  688.   // @gate enableFormActions
    
  689.   // @gate enableAsyncActions
    
  690.   it('multiple form actions', async () => {
    
  691.     const formRef = React.createRef();
    
  692. 
    
  693.     function Status() {
    
  694.       const {pending} = useFormStatus();
    
  695.       return pending ? <Text text="Pending..." /> : null;
    
  696.     }
    
  697. 
    
  698.     function App() {
    
  699.       const [state, setState] = useState(0);
    
  700.       return (
    
  701.         <form action={() => setState(n => n + 1)} ref={formRef}>
    
  702.           <Status />
    
  703.           <Suspense fallback={<Text text="Loading..." />}>
    
  704.             <AsyncText text={'Count: ' + state} />
    
  705.           </Suspense>
    
  706.         </form>
    
  707.       );
    
  708.     }
    
  709. 
    
  710.     const root = ReactDOMClient.createRoot(container);
    
  711.     await resolveText('Count: 0');
    
  712.     await act(() => root.render(<App />));
    
  713.     assertLog(['Count: 0']);
    
  714.     expect(container.textContent).toBe('Count: 0');
    
  715. 
    
  716.     // Update
    
  717.     await submit(formRef.current);
    
  718.     assertLog(['Pending...', 'Suspend! [Count: 1]', 'Loading...']);
    
  719.     expect(container.textContent).toBe('Pending...Count: 0');
    
  720. 
    
  721.     await act(() => resolveText('Count: 1'));
    
  722.     assertLog(['Count: 1']);
    
  723.     expect(container.textContent).toBe('Count: 1');
    
  724. 
    
  725.     // Update again
    
  726.     await submit(formRef.current);
    
  727.     assertLog(['Pending...', 'Suspend! [Count: 2]', 'Loading...']);
    
  728.     expect(container.textContent).toBe('Pending...Count: 1');
    
  729. 
    
  730.     await act(() => resolveText('Count: 2'));
    
  731.     assertLog(['Count: 2']);
    
  732.     expect(container.textContent).toBe('Count: 2');
    
  733.   });
    
  734. 
    
  735.   // @gate enableFormActions
    
  736.   it('form actions can be asynchronous', async () => {
    
  737.     const formRef = React.createRef();
    
  738. 
    
  739.     function Status() {
    
  740.       const {pending} = useFormStatus();
    
  741.       return pending ? <Text text="Pending..." /> : null;
    
  742.     }
    
  743. 
    
  744.     function App() {
    
  745.       const [state, setState] = useState('Initial');
    
  746.       return (
    
  747.         <form
    
  748.           action={async () => {
    
  749.             Scheduler.log('Async action started');
    
  750.             await getText('Wait');
    
  751.             startTransition(() => setState('Updated'));
    
  752.           }}
    
  753.           ref={formRef}>
    
  754.           <Status />
    
  755.           <Suspense fallback={<Text text="Loading..." />}>
    
  756.             <AsyncText text={state} />
    
  757.           </Suspense>
    
  758.         </form>
    
  759.       );
    
  760.     }
    
  761. 
    
  762.     const root = ReactDOMClient.createRoot(container);
    
  763.     await resolveText('Initial');
    
  764.     await act(() => root.render(<App />));
    
  765.     assertLog(['Initial']);
    
  766.     expect(container.textContent).toBe('Initial');
    
  767. 
    
  768.     await submit(formRef.current);
    
  769.     assertLog(['Async action started', 'Pending...']);
    
  770. 
    
  771.     await act(() => resolveText('Wait'));
    
  772.     assertLog(['Suspend! [Updated]', 'Loading...']);
    
  773.     expect(container.textContent).toBe('Pending...Initial');
    
  774. 
    
  775.     await act(() => resolveText('Updated'));
    
  776.     assertLog(['Updated']);
    
  777.     expect(container.textContent).toBe('Updated');
    
  778.   });
    
  779. 
    
  780.   it('sync errors in form actions can be captured by an error boundary', async () => {
    
  781.     if (gate(flags => !(flags.enableFormActions && flags.enableAsyncActions))) {
    
  782.       // TODO: Uncaught JSDOM errors fail the test after the scope has finished
    
  783.       // so don't work with the `gate` mechanism.
    
  784.       return;
    
  785.     }
    
  786. 
    
  787.     class ErrorBoundary extends React.Component {
    
  788.       state = {error: null};
    
  789.       static getDerivedStateFromError(error) {
    
  790.         return {error};
    
  791.       }
    
  792.       render() {
    
  793.         if (this.state.error !== null) {
    
  794.           return <Text text={this.state.error.message} />;
    
  795.         }
    
  796.         return this.props.children;
    
  797.       }
    
  798.     }
    
  799. 
    
  800.     const formRef = React.createRef();
    
  801. 
    
  802.     function App() {
    
  803.       return (
    
  804.         <ErrorBoundary>
    
  805.           <form
    
  806.             action={() => {
    
  807.               throw new Error('Oh no!');
    
  808.             }}
    
  809.             ref={formRef}>
    
  810.             <Text text="Everything is fine" />
    
  811.           </form>
    
  812.         </ErrorBoundary>
    
  813.       );
    
  814.     }
    
  815. 
    
  816.     const root = ReactDOMClient.createRoot(container);
    
  817.     await act(() => root.render(<App />));
    
  818.     assertLog(['Everything is fine']);
    
  819.     expect(container.textContent).toBe('Everything is fine');
    
  820. 
    
  821.     await submit(formRef.current);
    
  822.     assertLog(['Oh no!', 'Oh no!']);
    
  823.     expect(container.textContent).toBe('Oh no!');
    
  824.   });
    
  825. 
    
  826.   it('async errors in form actions can be captured by an error boundary', async () => {
    
  827.     if (gate(flags => !(flags.enableFormActions && flags.enableAsyncActions))) {
    
  828.       // TODO: Uncaught JSDOM errors fail the test after the scope has finished
    
  829.       // so don't work with the `gate` mechanism.
    
  830.       return;
    
  831.     }
    
  832. 
    
  833.     class ErrorBoundary extends React.Component {
    
  834.       state = {error: null};
    
  835.       static getDerivedStateFromError(error) {
    
  836.         return {error};
    
  837.       }
    
  838.       render() {
    
  839.         if (this.state.error !== null) {
    
  840.           return <Text text={this.state.error.message} />;
    
  841.         }
    
  842.         return this.props.children;
    
  843.       }
    
  844.     }
    
  845. 
    
  846.     const formRef = React.createRef();
    
  847. 
    
  848.     function App() {
    
  849.       return (
    
  850.         <ErrorBoundary>
    
  851.           <form
    
  852.             action={async () => {
    
  853.               Scheduler.log('Async action started');
    
  854.               await getText('Wait');
    
  855.               throw new Error('Oh no!');
    
  856.             }}
    
  857.             ref={formRef}>
    
  858.             <Text text="Everything is fine" />
    
  859.           </form>
    
  860.         </ErrorBoundary>
    
  861.       );
    
  862.     }
    
  863. 
    
  864.     const root = ReactDOMClient.createRoot(container);
    
  865.     await act(() => root.render(<App />));
    
  866.     assertLog(['Everything is fine']);
    
  867.     expect(container.textContent).toBe('Everything is fine');
    
  868. 
    
  869.     await submit(formRef.current);
    
  870.     assertLog(['Async action started']);
    
  871.     expect(container.textContent).toBe('Everything is fine');
    
  872. 
    
  873.     await act(() => resolveText('Wait'));
    
  874.     assertLog(['Oh no!', 'Oh no!']);
    
  875.     expect(container.textContent).toBe('Oh no!');
    
  876.   });
    
  877. 
    
  878.   // @gate enableFormActions
    
  879.   // @gate enableAsyncActions
    
  880.   it('useFormStatus reads the status of a pending form action', async () => {
    
  881.     const formRef = React.createRef();
    
  882. 
    
  883.     function Status() {
    
  884.       const {pending, data, action, method} = useFormStatus();
    
  885.       if (!pending) {
    
  886.         return <Text text="No pending action" />;
    
  887.       } else {
    
  888.         const foo = data.get('foo');
    
  889.         return (
    
  890.           <Text
    
  891.             text={`Pending action ${action.name}: foo is ${foo}, method is ${method}`}
    
  892.           />
    
  893.         );
    
  894.       }
    
  895.     }
    
  896. 
    
  897.     async function myAction() {
    
  898.       Scheduler.log('Async action started');
    
  899.       await getText('Wait');
    
  900.       Scheduler.log('Async action finished');
    
  901.     }
    
  902. 
    
  903.     function App() {
    
  904.       return (
    
  905.         <form action={myAction} ref={formRef}>
    
  906.           <input type="text" name="foo" defaultValue="bar" />
    
  907.           <Status />
    
  908.         </form>
    
  909.       );
    
  910.     }
    
  911. 
    
  912.     const root = ReactDOMClient.createRoot(container);
    
  913.     await act(() => root.render(<App />));
    
  914.     assertLog(['No pending action']);
    
  915.     expect(container.textContent).toBe('No pending action');
    
  916. 
    
  917.     await submit(formRef.current);
    
  918.     assertLog([
    
  919.       'Async action started',
    
  920.       'Pending action myAction: foo is bar, method is get',
    
  921.     ]);
    
  922.     expect(container.textContent).toBe(
    
  923.       'Pending action myAction: foo is bar, method is get',
    
  924.     );
    
  925. 
    
  926.     await act(() => resolveText('Wait'));
    
  927.     assertLog(['Async action finished', 'No pending action']);
    
  928.   });
    
  929. 
    
  930.   // @gate enableFormActions
    
  931.   it('should error if submitting a form manually', async () => {
    
  932.     const ref = React.createRef();
    
  933. 
    
  934.     let error = null;
    
  935.     let result = null;
    
  936. 
    
  937.     function emulateForceSubmit(submitter) {
    
  938.       const form = submitter.form || submitter;
    
  939.       const action =
    
  940.         (submitter && submitter.getAttribute('formaction')) || form.action;
    
  941.       try {
    
  942.         if (!/\s*javascript:/i.test(action)) {
    
  943.           throw new Error('Navigate to: ' + action);
    
  944.         } else {
    
  945.           // eslint-disable-next-line no-new-func
    
  946.           result = Function(action.slice(11))();
    
  947.         }
    
  948.       } catch (x) {
    
  949.         error = x;
    
  950.       }
    
  951.     }
    
  952. 
    
  953.     const root = ReactDOMClient.createRoot(container);
    
  954.     await act(async () => {
    
  955.       root.render(
    
  956.         <form
    
  957.           action={() => {}}
    
  958.           ref={ref}
    
  959.           onSubmit={e => {
    
  960.             e.preventDefault();
    
  961.             emulateForceSubmit(e.target);
    
  962.           }}>
    
  963.           <input type="text" name="foo" defaultValue="bar" />
    
  964.         </form>,
    
  965.       );
    
  966.     });
    
  967. 
    
  968.     // This submits the form, which gets blocked and then resubmitted. It's a somewhat
    
  969.     // common idiom but we don't support this pattern unless it uses requestSubmit().
    
  970.     await submit(ref.current);
    
  971.     expect(result).toBe(null);
    
  972.     expect(error.message).toContain(
    
  973.       'A React form was unexpectedly submitted. If you called form.submit()',
    
  974.     );
    
  975.   });
    
  976. 
    
  977.   // @gate enableFormActions
    
  978.   // @gate enableAsyncActions
    
  979.   test('useFormState updates state asynchronously and queues multiple actions', async () => {
    
  980.     let actionCounter = 0;
    
  981.     async function action(state, type) {
    
  982.       actionCounter++;
    
  983. 
    
  984.       Scheduler.log(`Async action started [${actionCounter}]`);
    
  985.       await getText(`Wait [${actionCounter}]`);
    
  986. 
    
  987.       switch (type) {
    
  988.         case 'increment':
    
  989.           return state + 1;
    
  990.         case 'decrement':
    
  991.           return state - 1;
    
  992.         default:
    
  993.           return state;
    
  994.       }
    
  995.     }
    
  996. 
    
  997.     let dispatch;
    
  998.     function App() {
    
  999.       const [state, _dispatch] = useFormState(action, 0);
    
  1000.       dispatch = _dispatch;
    
  1001.       return <Text text={state} />;
    
  1002.     }
    
  1003. 
    
  1004.     const root = ReactDOMClient.createRoot(container);
    
  1005.     await act(() => root.render(<App />));
    
  1006.     assertLog([0]);
    
  1007.     expect(container.textContent).toBe('0');
    
  1008. 
    
  1009.     await act(() => dispatch('increment'));
    
  1010.     assertLog(['Async action started [1]']);
    
  1011.     expect(container.textContent).toBe('0');
    
  1012. 
    
  1013.     // Dispatch a few more actions. None of these will start until the previous
    
  1014.     // one finishes.
    
  1015.     await act(() => dispatch('increment'));
    
  1016.     await act(() => dispatch('decrement'));
    
  1017.     await act(() => dispatch('increment'));
    
  1018.     assertLog([]);
    
  1019. 
    
  1020.     // Each action starts as soon as the previous one finishes.
    
  1021.     // NOTE: React does not render in between these actions because they all
    
  1022.     // update the same queue, which means they get entangled together. This is
    
  1023.     // intentional behavior.
    
  1024.     await act(() => resolveText('Wait [1]'));
    
  1025.     assertLog(['Async action started [2]']);
    
  1026.     await act(() => resolveText('Wait [2]'));
    
  1027.     assertLog(['Async action started [3]']);
    
  1028.     await act(() => resolveText('Wait [3]'));
    
  1029.     assertLog(['Async action started [4]']);
    
  1030.     await act(() => resolveText('Wait [4]'));
    
  1031. 
    
  1032.     // Finally the last action finishes and we can render the result.
    
  1033.     assertLog([2]);
    
  1034.     expect(container.textContent).toBe('2');
    
  1035.   });
    
  1036. 
    
  1037.   // @gate enableFormActions
    
  1038.   // @gate enableAsyncActions
    
  1039.   test('useFormState supports inline actions', async () => {
    
  1040.     let increment;
    
  1041.     function App({stepSize}) {
    
  1042.       const [state, dispatch] = useFormState(async prevState => {
    
  1043.         return prevState + stepSize;
    
  1044.       }, 0);
    
  1045.       increment = dispatch;
    
  1046.       return <Text text={state} />;
    
  1047.     }
    
  1048. 
    
  1049.     // Initial render
    
  1050.     const root = ReactDOMClient.createRoot(container);
    
  1051.     await act(() => root.render(<App stepSize={1} />));
    
  1052.     assertLog([0]);
    
  1053. 
    
  1054.     // Perform an action. This will increase the state by 1, as defined by the
    
  1055.     // stepSize prop.
    
  1056.     await act(() => increment());
    
  1057.     assertLog([1]);
    
  1058. 
    
  1059.     // Now increase the stepSize prop to 10. Subsequent steps will increase
    
  1060.     // by this amount.
    
  1061.     await act(() => root.render(<App stepSize={10} />));
    
  1062.     assertLog([1]);
    
  1063. 
    
  1064.     // Increment again. The state should increase by 10.
    
  1065.     await act(() => increment());
    
  1066.     assertLog([11]);
    
  1067.   });
    
  1068. 
    
  1069.   // @gate enableFormActions
    
  1070.   // @gate enableAsyncActions
    
  1071.   test('useFormState: dispatch throws if called during render', async () => {
    
  1072.     function App() {
    
  1073.       const [state, dispatch] = useFormState(async () => {}, 0);
    
  1074.       dispatch();
    
  1075.       return <Text text={state} />;
    
  1076.     }
    
  1077. 
    
  1078.     const root = ReactDOMClient.createRoot(container);
    
  1079.     await act(async () => {
    
  1080.       root.render(<App />);
    
  1081.       await waitForThrow('Cannot update form state while rendering.');
    
  1082.     });
    
  1083.   });
    
  1084. 
    
  1085.   // @gate enableFormActions
    
  1086.   // @gate enableAsyncActions
    
  1087.   test('queues multiple actions and runs them in order', async () => {
    
  1088.     let action;
    
  1089.     function App() {
    
  1090.       const [state, dispatch] = useFormState(
    
  1091.         async (s, a) => await getText(a),
    
  1092.         'A',
    
  1093.       );
    
  1094.       action = dispatch;
    
  1095.       return <Text text={state} />;
    
  1096.     }
    
  1097. 
    
  1098.     const root = ReactDOMClient.createRoot(container);
    
  1099.     await act(() => root.render(<App />));
    
  1100.     assertLog(['A']);
    
  1101. 
    
  1102.     await act(() => action('B'));
    
  1103.     await act(() => action('C'));
    
  1104.     await act(() => action('D'));
    
  1105. 
    
  1106.     await act(() => resolveText('B'));
    
  1107.     await act(() => resolveText('C'));
    
  1108.     await act(() => resolveText('D'));
    
  1109. 
    
  1110.     assertLog(['D']);
    
  1111.     expect(container.textContent).toBe('D');
    
  1112.   });
    
  1113. 
    
  1114.   // @gate enableFormActions
    
  1115.   // @gate enableAsyncActions
    
  1116.   test('useFormState: works if action is sync', async () => {
    
  1117.     let increment;
    
  1118.     function App({stepSize}) {
    
  1119.       const [state, dispatch] = useFormState(prevState => {
    
  1120.         return prevState + stepSize;
    
  1121.       }, 0);
    
  1122.       increment = dispatch;
    
  1123.       return <Text text={state} />;
    
  1124.     }
    
  1125. 
    
  1126.     // Initial render
    
  1127.     const root = ReactDOMClient.createRoot(container);
    
  1128.     await act(() => root.render(<App stepSize={1} />));
    
  1129.     assertLog([0]);
    
  1130. 
    
  1131.     // Perform an action. This will increase the state by 1, as defined by the
    
  1132.     // stepSize prop.
    
  1133.     await act(() => increment());
    
  1134.     assertLog([1]);
    
  1135. 
    
  1136.     // Now increase the stepSize prop to 10. Subsequent steps will increase
    
  1137.     // by this amount.
    
  1138.     await act(() => root.render(<App stepSize={10} />));
    
  1139.     assertLog([1]);
    
  1140. 
    
  1141.     // Increment again. The state should increase by 10.
    
  1142.     await act(() => increment());
    
  1143.     assertLog([11]);
    
  1144.   });
    
  1145. 
    
  1146.   // @gate enableFormActions
    
  1147.   // @gate enableAsyncActions
    
  1148.   test('useFormState: can mix sync and async actions', async () => {
    
  1149.     let action;
    
  1150.     function App() {
    
  1151.       const [state, dispatch] = useFormState((s, a) => a, 'A');
    
  1152.       action = dispatch;
    
  1153.       return <Text text={state} />;
    
  1154.     }
    
  1155. 
    
  1156.     const root = ReactDOMClient.createRoot(container);
    
  1157.     await act(() => root.render(<App />));
    
  1158.     assertLog(['A']);
    
  1159. 
    
  1160.     await act(() => action(getText('B')));
    
  1161.     await act(() => action('C'));
    
  1162.     await act(() => action(getText('D')));
    
  1163.     await act(() => action('E'));
    
  1164. 
    
  1165.     await act(() => resolveText('B'));
    
  1166.     await act(() => resolveText('D'));
    
  1167.     assertLog(['E']);
    
  1168.     expect(container.textContent).toBe('E');
    
  1169.   });
    
  1170. 
    
  1171.   // @gate enableFormActions
    
  1172.   // @gate enableAsyncActions
    
  1173.   test('useFormState: error handling (sync action)', async () => {
    
  1174.     let resetErrorBoundary;
    
  1175.     class ErrorBoundary extends React.Component {
    
  1176.       state = {error: null};
    
  1177.       static getDerivedStateFromError(error) {
    
  1178.         return {error};
    
  1179.       }
    
  1180.       render() {
    
  1181.         resetErrorBoundary = () => this.setState({error: null});
    
  1182.         if (this.state.error !== null) {
    
  1183.           return <Text text={'Caught an error: ' + this.state.error.message} />;
    
  1184.         }
    
  1185.         return this.props.children;
    
  1186.       }
    
  1187.     }
    
  1188. 
    
  1189.     let action;
    
  1190.     function App() {
    
  1191.       const [state, dispatch] = useFormState((s, a) => {
    
  1192.         if (a.endsWith('!')) {
    
  1193.           throw new Error(a);
    
  1194.         }
    
  1195.         return a;
    
  1196.       }, 'A');
    
  1197.       action = dispatch;
    
  1198.       return <Text text={state} />;
    
  1199.     }
    
  1200. 
    
  1201.     const root = ReactDOMClient.createRoot(container);
    
  1202.     await act(() =>
    
  1203.       root.render(
    
  1204.         <ErrorBoundary>
    
  1205.           <App />
    
  1206.         </ErrorBoundary>,
    
  1207.       ),
    
  1208.     );
    
  1209.     assertLog(['A']);
    
  1210. 
    
  1211.     await act(() => action('Oops!'));
    
  1212.     assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
    
  1213.     expect(container.textContent).toBe('Caught an error: Oops!');
    
  1214. 
    
  1215.     // Reset the error boundary
    
  1216.     await act(() => resetErrorBoundary());
    
  1217.     assertLog(['A']);
    
  1218. 
    
  1219.     // Trigger an error again, but this time, perform another action that
    
  1220.     // overrides the first one and fixes the error
    
  1221.     await act(() => {
    
  1222.       action('Oops!');
    
  1223.       action('B');
    
  1224.     });
    
  1225.     assertLog(['B']);
    
  1226.     expect(container.textContent).toBe('B');
    
  1227.   });
    
  1228. 
    
  1229.   // @gate enableFormActions
    
  1230.   // @gate enableAsyncActions
    
  1231.   test('useFormState: error handling (async action)', async () => {
    
  1232.     let resetErrorBoundary;
    
  1233.     class ErrorBoundary extends React.Component {
    
  1234.       state = {error: null};
    
  1235.       static getDerivedStateFromError(error) {
    
  1236.         return {error};
    
  1237.       }
    
  1238.       render() {
    
  1239.         resetErrorBoundary = () => this.setState({error: null});
    
  1240.         if (this.state.error !== null) {
    
  1241.           return <Text text={'Caught an error: ' + this.state.error.message} />;
    
  1242.         }
    
  1243.         return this.props.children;
    
  1244.       }
    
  1245.     }
    
  1246. 
    
  1247.     let action;
    
  1248.     function App() {
    
  1249.       const [state, dispatch] = useFormState(async (s, a) => {
    
  1250.         const text = await getText(a);
    
  1251.         if (text.endsWith('!')) {
    
  1252.           throw new Error(text);
    
  1253.         }
    
  1254.         return text;
    
  1255.       }, 'A');
    
  1256.       action = dispatch;
    
  1257.       return <Text text={state} />;
    
  1258.     }
    
  1259. 
    
  1260.     const root = ReactDOMClient.createRoot(container);
    
  1261.     await act(() =>
    
  1262.       root.render(
    
  1263.         <ErrorBoundary>
    
  1264.           <App />
    
  1265.         </ErrorBoundary>,
    
  1266.       ),
    
  1267.     );
    
  1268.     assertLog(['A']);
    
  1269. 
    
  1270.     await act(() => action('Oops!'));
    
  1271.     assertLog([]);
    
  1272.     await act(() => resolveText('Oops!'));
    
  1273.     assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
    
  1274.     expect(container.textContent).toBe('Caught an error: Oops!');
    
  1275. 
    
  1276.     // Reset the error boundary
    
  1277.     await act(() => resetErrorBoundary());
    
  1278.     assertLog(['A']);
    
  1279. 
    
  1280.     // Trigger an error again, but this time, perform another action that
    
  1281.     // overrides the first one and fixes the error
    
  1282.     await act(() => {
    
  1283.       action('Oops!');
    
  1284.       action('B');
    
  1285.     });
    
  1286.     assertLog([]);
    
  1287.     await act(() => resolveText('B'));
    
  1288.     assertLog(['B']);
    
  1289.     expect(container.textContent).toBe('B');
    
  1290.   });
    
  1291. });