35
let ReactDOMClient;
36
let Scheduler;
37
let assertLog;
38
+ let assertConsoleErrorDev;
39
let waitForThrow;
40
let useState;
41
let Suspense;
42
let startTransition;
43
+ let useTransition;
44
let use;
45
let textCache;
46
let useFormStatus;
56
act = require('internal-test-utils').act;
57
assertLog = require('internal-test-utils').assertLog;
58
waitForThrow = require('internal-test-utils').waitForThrow;
59
+ assertConsoleErrorDev =
60
+ require('internal-test-utils').assertConsoleErrorDev;
61
useState = React.useState;
62
Suspense = React.Suspense;
63
startTransition = React.startTransition;
64
+ useTransition = React.useTransition;
65
use = React.use;
66
useFormStatus = ReactDOM.useFormStatus;
67
requestFormReset = ReactDOM.requestFormReset;
1787
// The form was reset even though the action didn't finish.
1788
expect(inputRef.current.value).toBe('Initial');
1789
});
1790
+
1791
+ test("regression: submitter's formAction prop is coerced correctly before checking if it exists", async () => {
1792
+ function App({submitterAction}) {
1793
+ return (
1794
+ <form action={() => Scheduler.log('Form action')}>
1795
+ <button ref={buttonRef} type="submit" formAction={submitterAction} />
1796
+ </form>
1797
+ );
1798
+ }
1799
+
1800
+ const buttonRef = React.createRef();
1801
+ const root = ReactDOMClient.createRoot(container);
1802
+
1803
+ await act(() =>
1804
+ root.render(
1805
+ <App submitterAction={() => Scheduler.log('Button action')} />,
1806
+ ),
1807
+ );
1808
+ await submit(buttonRef.current);
1809
+ assertLog(['Button action']);
1810
+
1811
+ // When there's no button action, the form action should fire
1812
+ await act(() => root.render(<App submitterAction={null} />));
1813
+ await submit(buttonRef.current);
1814
+ assertLog(['Form action']);
1815
+
1816
+ // Symbols are coerced to null, so this should fire the form action
1817
+ await act(() => root.render(<App submitterAction={Symbol()} />));
1818
+ assertConsoleErrorDev(['Invalid value for prop `formAction`']);
1819
+ await submit(buttonRef.current);
1820
+ assertLog(['Form action']);
1821
+
1822
+ // Booleans are coerced to null, so this should fire the form action
1823
+ await act(() => root.render(<App submitterAction={true} />));
1824
+ await submit(buttonRef.current);
1825
+ assertLog(['Form action']);
1826
+
1827
+ // A string on the submitter should prevent the form action from firing
1828
+ // and trigger the native behavior
1829
+ await act(() => root.render(<App submitterAction="https://react.dev/" />));
1830
+ await expect(submit(buttonRef.current)).rejects.toThrow(
1831
+ 'Navigate to: https://react.dev/',
1832
+ );
1833
+ });
1834
+
1835
+ test(
1836
+ 'useFormStatus is activated if startTransition is called ' +
1837
+ 'inside preventDefault-ed submit event',
1838
+ async () => {
1839
+ function Output({value}) {
1840
+ const {pending} = useFormStatus();
1841
+ return <Text text={pending ? `${value} (pending...)` : value} />;
1842
+ }
1843
+
1844
+ function App({value}) {
1845
+ const [, startFormTransition] = useTransition();
1846
+
1847
+ function onSubmit(event) {
1848
+ event.preventDefault();
1849
+ startFormTransition(async () => {
1850
+ const updatedValue = event.target.elements.search.value;
1851
+ Scheduler.log('Action started');
1852
+ await getText('Wait');
1853
+ Scheduler.log('Action finished');
1854
+ startTransition(() => root.render(<App value={updatedValue} />));
1855
+ });
1856
+ }
1857
+ return (
1858
+ <form ref={formRef} onSubmit={onSubmit}>
1859
+ <input
1860
+ ref={inputRef}
1861
+ type="text"
1862
+ name="search"
1863
+ defaultValue={value}
1864
+ />
1865
+ <div ref={outputRef}>
1866
+ <Output value={value} />
1867
+ </div>
1868
+ </form>
1869
+ );
1870
+ }
1871
+
1872
+ const formRef = React.createRef();
1873
+ const inputRef = React.createRef();
1874
+ const outputRef = React.createRef();
1875
+ const root = ReactDOMClient.createRoot(container);
1876
+ await act(() => root.render(<App value="Initial" />));
1877
+ assertLog(['Initial']);
1878
+
1879
+ // Update the input to something different
1880
+ inputRef.current.value = 'Updated';
1881
+
1882
+ // Submit the form.
1883
+ await submit(formRef.current);
1884
+ // The form switches into a pending state.
1885
+ assertLog(['Action started', 'Initial (pending...)']);
1886
+ expect(outputRef.current.textContent).toBe('Initial (pending...)');
1887
+
1888
+ // While the submission is still pending, update the input again so we
1889
+ // can check whether the form is reset after the action finishes.
1890
+ inputRef.current.value = 'Updated again after submission';
1891
+
1892
+ // Resolve the async action
1893
+ await act(() => resolveText('Wait'));
1894
+ assertLog(['Action finished', 'Updated']);
1895
+ expect(outputRef.current.textContent).toBe('Updated');
1896
+
1897
+ // Confirm that the form was not automatically reset (should call
1898
+ // requestFormReset(formRef.current) to opt into this behavior)
1899
+ expect(inputRef.current.value).toBe('Updated again after submission');
1900
+ },
1901
+ );
1902
+
1903
+ test('useFormStatus is not activated if startTransition is not called', async () => {
1904
+ function Output({value}) {
1905
+ const {pending} = useFormStatus();
1906
+
1907
+ return (
1908
+ <Text
1909
+ text={
1910
+ pending
1911
+ ? 'Should be unreachable! This test should never activate the pending state.'
1912
+ : value
1913
+ }
1914
+ />
1915
+ );
1916
+ }
1917
+
1918
+ function App({value}) {
1919
+ async function onSubmit(event) {
1920
+ event.preventDefault();
1921
+ const updatedValue = event.target.elements.search.value;
1922
+ Scheduler.log('Async event handler started');
1923
+ await getText('Wait');
1924
+ Scheduler.log('Async event handler finished');
1925
+ startTransition(() => root.render(<App value={updatedValue} />));
1926
+ }
1927
+ return (
1928
+ <form ref={formRef} onSubmit={onSubmit}>
1929
+ <input
1930
+ ref={inputRef}
1931
+ type="text"
1932
+ name="search"
1933
+ defaultValue={value}
1934
+ />
1935
+ <div ref={outputRef}>
1936
+ <Output value={value} />
1937
+ </div>
1938
+ </form>
1939
+ );
1940
+ }
1941
+
1942
+ const formRef = React.createRef();
1943
+ const inputRef = React.createRef();
1944
+ const outputRef = React.createRef();
1945
+ const root = ReactDOMClient.createRoot(container);
1946
+ await act(() => root.render(<App value="Initial" />));
1947
+ assertLog(['Initial']);
1948
+
1949
+ // Update the input to something different
1950
+ inputRef.current.value = 'Updated';
1951
+
1952
+ // Submit the form.
1953
+ await submit(formRef.current);
1954
+ // Unlike the previous test, which uses startTransition to manually dispatch
1955
+ // an action, this test uses a regular event handler, so useFormStatus is
1956
+ // not activated.
1957
+ assertLog(['Async event handler started']);
1958
+ expect(outputRef.current.textContent).toBe('Initial');
1959
+
1960
+ // While the submission is still pending, update the input again so we
1961
+ // can check whether the form is reset after the action finishes.
1962
+ inputRef.current.value = 'Updated again after submission';
1963
+
1964
+ // Resolve the async action
1965
+ await act(() => resolveText('Wait'));
1966
+ assertLog(['Async event handler finished', 'Updated']);
1967
+ expect(outputRef.current.textContent).toBe('Updated');
1968
+
1969
+ // Confirm that the form was not automatically reset (should call
1970
+ // requestFormReset(formRef.current) to opt into this behavior)
1971
+ expect(inputRef.current.value).toBe('Updated again after submission');
1972
+ });
1973
+
1974
+ test('useFormStatus is not activated if event is not preventDefault-ed ', async () => {
1975
+ function Output({value}) {
1976
+ const {pending} = useFormStatus();
1977
+ return <Text text={pending ? `${value} (pending...)` : value} />;
1978
+ }
1979
+
1980
+ function App({value}) {
1981
+ const [, startFormTransition] = useTransition();
1982
+
1983
+ function onSubmit(event) {
1984
+ // This event is not preventDefault-ed, so the default form submission
1985
+ // happens, and useFormStatus is not activated.
1986
+ startFormTransition(async () => {
1987
+ const updatedValue = event.target.elements.search.value;
1988
+ Scheduler.log('Action started');
1989
+ await getText('Wait');
1990
+ Scheduler.log('Action finished');
1991
+ startTransition(() => root.render(<App value={updatedValue} />));
1992
+ });
1993
+ }
1994
+ return (
1995
+ <form ref={formRef} onSubmit={onSubmit}>
1996
+ <input
1997
+ ref={inputRef}
1998
+ type="text"
1999
+ name="search"
2000
+ defaultValue={value}
2001
+ />
2002
+ <div ref={outputRef}>
2003
+ <Output value={value} />
2004
+ </div>
2005
+ </form>
2006
+ );
2007
+ }
2008
+
2009
+ const formRef = React.createRef();
2010
+ const inputRef = React.createRef();
2011
+ const outputRef = React.createRef();
2012
+ const root = ReactDOMClient.createRoot(container);
2013
+ await act(() => root.render(<App value="Initial" />));
2014
+ assertLog(['Initial']);
2015
+
2016
+ // Update the input to something different
2017
+ inputRef.current.value = 'Updated';
2018
+
2019
+ // Submitting the form should trigger the default navigation behavior
2020
+ await expect(submit(formRef.current)).rejects.toThrow(
2021
+ 'Navigate to: http://localhost/',
2022
+ );
2023
+
2024
+ // The useFormStatus hook was not activated
2025
+ assertLog(['Action started', 'Initial']);
2026
+ expect(outputRef.current.textContent).toBe('Initial');
2027
+ });
2028
+
2029
+ test('useFormStatus coerces the value of the "action" prop', async () => {
2030
+ function Status() {
2031
+ const {pending, action} = useFormStatus();
2032
+
2033
+ if (pending) {
2034
+ Scheduler.log(action);
2035
+ return 'Pending';
2036
+ } else {
2037
+ return 'Not pending';
2038
+ }
2039
+ }
2040
+
2041
+ function Form({action}) {
2042
+ const [, startFormTransition] = useTransition();
2043
+
2044
+ function onSubmit(event) {
2045
+ event.preventDefault();
2046
+ // Schedule an empty action for no other purpose than to trigger the
2047
+ // pending state.
2048
+ startFormTransition(async () => {});
2049
+ }
2050
+ return (
2051
+ <form ref={formRef} action={action} onSubmit={onSubmit}>
2052
+ <Status />
2053
+ </form>
2054
+ );
2055
+ }
2056
+
2057
+ const formRef = React.createRef();
2058
+ const root = ReactDOMClient.createRoot(container);
2059
+
2060
+ // Symbols are coerced to null
2061
+ await act(() => root.render(<Form action={Symbol()} />));
2062
+ assertConsoleErrorDev(['Invalid value for prop `action`']);
2063
+ await submit(formRef.current);
2064
+ assertLog([null]);
2065
+
2066
+ // Booleans are coerced to null
2067
+ await act(() => root.render(<Form action={true} />));
2068
+ await submit(formRef.current);
2069
+ assertLog([null]);
2070
+
2071
+ // Strings are passed through
2072
+ await act(() => root.render(<Form action="https://react.dev" />));
2073
+ await submit(formRef.current);
2074
+ assertLog(['https://react.dev']);
2075
+
2076
+ // Functions are passed through
2077
+ const actionFn = () => {};
2078
+ await act(() => root.render(<Form action={actionFn} />));
2079
+ await submit(formRef.current);
2080
+ assertLog([actionFn]);
2081
+
2082
+ // Everything else is toString-ed
2083
+ class MyAction {
2084
+ toString() {
2085
+ return 'stringified action';
2086
+ }
2087
+ }
2088
+ await act(() => root.render(<Form action={new MyAction()} />));
2089
+ await submit(formRef.current);
2090
+ assertLog(['stringified action']);
2091
+ });
2092
});