43
let textCache;
44
let useFormStatus;
45
let useActionState;
46
+ let requestFormReset;
47
48
beforeEach(() => {
49
jest.resetModules();
59
startTransition = React.startTransition;
60
use = React.use;
61
useFormStatus = ReactDOM.useFormStatus;
62
+ requestFormReset = ReactDOM.requestFormReset;
63
container = document.createElement('div');
64
document.body.appendChild(container);
65
1416
expect(inputRef.current.value).toBe('acdlite');
1417
expect(divRef.current.textContent).toEqual('Current username: acdlite');
1418
});
1419
+
1420
+ test('requestFormReset schedules a form reset after transition completes', async () => {
1421
+ // This is the same as the previous test, except the form is updated with
1422
+ // a userspace action instead of a built-in form action.
1423
+
1424
+ const formRef = React.createRef();
1425
+ const inputRef = React.createRef();
1426
+ const divRef = React.createRef();
1427
+
1428
+ function App({promiseForUsername}) {
1429
+ // Make this suspensey to simulate RSC streaming.
1430
+ const username = use(promiseForUsername);
1431
+
1432
+ return (
1433
+ <form ref={formRef}>
1434
+ <input
1435
+ ref={inputRef}
1436
+ text="text"
1437
+ name="username"
1438
+ defaultValue={username}
1439
+ />
1440
+ <div ref={divRef}>
1441
+ <Text text={'Current username: ' + username} />
1442
+ </div>
1443
+ </form>
1444
+ );
1445
+ }
1446
+
1447
+ // Initial render
1448
+ const root = ReactDOMClient.createRoot(container);
1449
+ const promiseForInitialUsername = getText('(empty)');
1450
+ await resolveText('(empty)');
1451
+ await act(() =>
1452
+ root.render(<App promiseForUsername={promiseForInitialUsername} />),
1453
+ );
1454
+ assertLog(['Current username: (empty)']);
1455
+ expect(divRef.current.textContent).toEqual('Current username: (empty)');
1456
+
1457
+ // Dirty the uncontrolled input
1458
+ inputRef.current.value = ' AcdLite ';
1459
+
1460
+ // This is a userspace action. It does not trigger a real form submission.
1461
+ // The practical use case is implementing a custom action prop using
1462
+ // onSubmit without losing the built-in form resetting behavior.
1463
+ await act(() => {
1464
+ startTransition(async () => {
1465
+ const form = formRef.current;
1466
+ const formData = new FormData(form);
1467
+ requestFormReset(form);
1468
+
1469
+ const rawUsername = formData.get('username');
1470
+ const normalizedUsername = rawUsername.trim().toLowerCase();
1471
+
1472
+ Scheduler.log(`Async action started`);
1473
+ await getText('Wait');
1474
+
1475
+ // Update the app with new data. This is analagous to re-rendering
1476
+ // from the root with a new RSC payload.
1477
+ startTransition(() => {
1478
+ root.render(<App promiseForUsername={getText(normalizedUsername)} />);
1479
+ });
1480
+ });
1481
+ });
1482
+ assertLog(['Async action started']);
1483
+ expect(inputRef.current.value).toBe(' AcdLite ');
1484
+
1485
+ // Finish the async action. This will trigger a re-render from the root with
1486
+ // new data from the "server", which suspends.
1487
+ //
1488
+ // The form should not reset yet because we need to update `defaultValue`
1489
+ // first. So we wait for the render to complete.
1490
+ await act(() => resolveText('Wait'));
1491
+ assertLog([]);
1492
+ // The DOM input is still dirty.
1493
+ expect(inputRef.current.value).toBe(' AcdLite ');
1494
+ // The React tree is suspended.
1495
+ expect(divRef.current.textContent).toEqual('Current username: (empty)');
1496
+
1497
+ // Unsuspend and finish rendering. Now the form should be reset.
1498
+ await act(() => resolveText('acdlite'));
1499
+ assertLog(['Current username: acdlite']);
1500
+ // The form was reset to the new value from the server.
1501
+ expect(inputRef.current.value).toBe('acdlite');
1502
+ expect(divRef.current.textContent).toEqual('Current username: acdlite');
1503
+ });
1504
+
1505
+ test(
1506
+ 'requestFormReset works with inputs that are not descendants ' +
1507
+ 'of the form element',
1508
+ async () => {
1509
+ // This is the same as the previous test, except the input is not a child
1510
+ // of the form; it's linked with <input form="myform" />
1511
+
1512
+ const formRef = React.createRef();
1513
+ const inputRef = React.createRef();
1514
+ const divRef = React.createRef();
1515
+
1516
+ function App({promiseForUsername}) {
1517
+ // Make this suspensey to simulate RSC streaming.
1518
+ const username = use(promiseForUsername);
1519
+
1520
+ return (
1521
+ <>
1522
+ <form id="myform" ref={formRef} />
1523
+ <input
1524
+ form="myform"
1525
+ ref={inputRef}
1526
+ text="text"
1527
+ name="username"
1528
+ defaultValue={username}
1529
+ />
1530
+ <div ref={divRef}>
1531
+ <Text text={'Current username: ' + username} />
1532
+ </div>
1533
+ </>
1534
+ );
1535
+ }
1536
+
1537
+ // Initial render
1538
+ const root = ReactDOMClient.createRoot(container);
1539
+ const promiseForInitialUsername = getText('(empty)');
1540
+ await resolveText('(empty)');
1541
+ await act(() =>
1542
+ root.render(<App promiseForUsername={promiseForInitialUsername} />),
1543
+ );
1544
+ assertLog(['Current username: (empty)']);
1545
+ expect(divRef.current.textContent).toEqual('Current username: (empty)');
1546
+
1547
+ // Dirty the uncontrolled input
1548
+ inputRef.current.value = ' AcdLite ';
1549
+
1550
+ // This is a userspace action. It does not trigger a real form submission.
1551
+ // The practical use case is implementing a custom action prop using
1552
+ // onSubmit without losing the built-in form resetting behavior.
1553
+ await act(() => {
1554
+ startTransition(async () => {
1555
+ const form = formRef.current;
1556
+ const formData = new FormData(form);
1557
+ requestFormReset(form);
1558
+
1559
+ const rawUsername = formData.get('username');
1560
+ const normalizedUsername = rawUsername.trim().toLowerCase();
1561
+
1562
+ Scheduler.log(`Async action started`);
1563
+ await getText('Wait');
1564
+
1565
+ // Update the app with new data. This is analagous to re-rendering
1566
+ // from the root with a new RSC payload.
1567
+ startTransition(() => {
1568
+ root.render(
1569
+ <App promiseForUsername={getText(normalizedUsername)} />,
1570
+ );
1571
+ });
1572
+ });
1573
+ });
1574
+ assertLog(['Async action started']);
1575
+ expect(inputRef.current.value).toBe(' AcdLite ');
1576
+
1577
+ // Finish the async action. This will trigger a re-render from the root with
1578
+ // new data from the "server", which suspends.
1579
+ //
1580
+ // The form should not reset yet because we need to update `defaultValue`
1581
+ // first. So we wait for the render to complete.
1582
+ await act(() => resolveText('Wait'));
1583
+ assertLog([]);
1584
+ // The DOM input is still dirty.
1585
+ expect(inputRef.current.value).toBe(' AcdLite ');
1586
+ // The React tree is suspended.
1587
+ expect(divRef.current.textContent).toEqual('Current username: (empty)');
1588
+
1589
+ // Unsuspend and finish rendering. Now the form should be reset.
1590
+ await act(() => resolveText('acdlite'));
1591
+ assertLog(['Current username: acdlite']);
1592
+ // The form was reset to the new value from the server.
1593
+ expect(inputRef.current.value).toBe('acdlite');
1594
+ expect(divRef.current.textContent).toEqual('Current username: acdlite');
1595
+ },
1596
+ );
1597
+
1598
+ test('reset multiple forms in the same transition', async () => {
1599
+ const formRefA = React.createRef();
1600
+ const formRefB = React.createRef();
1601
+
1602
+ function App({promiseForA, promiseForB}) {
1603
+ // Make these suspensey to simulate RSC streaming.
1604
+ const a = use(promiseForA);
1605
+ const b = use(promiseForB);
1606
+ return (
1607
+ <>
1608
+ <form ref={formRefA}>
1609
+ <input type="text" name="inputName" defaultValue={a} />
1610
+ </form>
1611
+ <form ref={formRefB}>
1612
+ <input type="text" name="inputName" defaultValue={b} />
1613
+ </form>
1614
+ </>
1615
+ );
1616
+ }
1617
+
1618
+ const root = ReactDOMClient.createRoot(container);
1619
+ const initialPromiseForA = getText('A1');
1620
+ const initialPromiseForB = getText('B1');
1621
+ await resolveText('A1');
1622
+ await resolveText('B1');
1623
+ await act(() =>
1624
+ root.render(
1625
+ <App
1626
+ promiseForA={initialPromiseForA}
1627
+ promiseForB={initialPromiseForB}
1628
+ />,
1629
+ ),
1630
+ );
1631
+
1632
+ // Dirty the uncontrolled inputs
1633
+ formRefA.current.elements.inputName.value = ' A2 ';
1634
+ formRefB.current.elements.inputName.value = ' B2 ';
1635
+
1636
+ // Trigger an async action that updates and reset both forms.
1637
+ await act(() => {
1638
+ startTransition(async () => {
1639
+ const currentA = formRefA.current.elements.inputName.value;
1640
+ const currentB = formRefB.current.elements.inputName.value;
1641
+
1642
+ requestFormReset(formRefA.current);
1643
+ requestFormReset(formRefB.current);
1644
+
1645
+ Scheduler.log('Async action started');
1646
+ await getText('Wait');
1647
+
1648
+ // Pretend the server did something with the data.
1649
+ const normalizedA = currentA.trim();
1650
+ const normalizedB = currentB.trim();
1651
+
1652
+ // Update the app with new data. This is analagous to re-rendering
1653
+ // from the root with a new RSC payload.
1654
+ startTransition(() => {
1655
+ root.render(
1656
+ <App
1657
+ promiseForA={getText(normalizedA)}
1658
+ promiseForB={getText(normalizedB)}
1659
+ />,
1660
+ );
1661
+ });
1662
+ });
1663
+ });
1664
+ assertLog(['Async action started']);
1665
+
1666
+ // Finish the async action. This will trigger a re-render from the root with
1667
+ // new data from the "server", which suspends.
1668
+ //
1669
+ // The forms should not reset yet because we need to update `defaultValue`
1670
+ // first. So we wait for the render to complete.
1671
+ await act(() => resolveText('Wait'));
1672
+
1673
+ // The DOM inputs are still dirty.
1674
+ expect(formRefA.current.elements.inputName.value).toBe(' A2 ');
1675
+ expect(formRefB.current.elements.inputName.value).toBe(' B2 ');
1676
+
1677
+ // Unsuspend and finish rendering. Now the forms should be reset.
1678
+ await act(() => {
1679
+ resolveText('A2');
1680
+ resolveText('B2');
1681
+ });
1682
+ // The forms were reset to the new value from the server.
1683
+ expect(formRefA.current.elements.inputName.value).toBe('A2');
1684
+ expect(formRefB.current.elements.inputName.value).toBe('B2');
1685
+ });
1686
+
1687
+ test('requestFormReset throws if the form is not managed by React', async () => {
1688
+ container.innerHTML = `
1689
+ <form id="myform">
1690
+ <input id="input" type="text" name="greeting" />
1691
+ </form>
1692
+ `;
1693
+
1694
+ const form = document.getElementById('myform');
1695
+ const input = document.getElementById('input');
1696
+
1697
+ input.value = 'Hi!!!!!!!!!!!!!';
1698
+
1699
+ expect(() => requestFormReset(form)).toThrow('Invalid form element.');
1700
+ // The form was not reset.
1701
+ expect(input.value).toBe('Hi!!!!!!!!!!!!!');
1702
+
1703
+ // Just confirming a regular form reset works fine.
1704
+ form.reset();
1705
+ expect(input.value).toBe('');
1706
+ });
1707
+
1708
+ test('requestFormReset throws on a non-form DOM element', async () => {
1709
+ const root = ReactDOMClient.createRoot(container);
1710
+ const ref = React.createRef();
1711
+ await act(() => root.render(<div ref={ref}>Hi</div>));
1712
+ const div = ref.current;
1713
+ expect(div.textContent).toBe('Hi');
1714
+
1715
+ expect(() => requestFormReset(div)).toThrow('Invalid form element.');
1716
+ });
1717
+
1718
+ test('warns if requestFormReset is called outside of a transition', async () => {
1719
+ const formRef = React.createRef();
1720
+ const inputRef = React.createRef();
1721
+
1722
+ function App() {
1723
+ return (
1724
+ <form ref={formRef}>
1725
+ <input ref={inputRef} type="text" defaultValue="Initial" />
1726
+ </form>
1727
+ );
1728
+ }
1729
+
1730
+ const root = ReactDOMClient.createRoot(container);
1731
+ await act(() => root.render(<App />));
1732
+
1733
+ // Dirty the uncontrolled input
1734
+ inputRef.current.value = ' Updated ';
1735
+
1736
+ // Trigger an async action that updates and reset both forms.
1737
+ await act(() => {
1738
+ startTransition(async () => {
1739
+ Scheduler.log('Action started');
1740
+ await getText('Wait 1');
1741
+ Scheduler.log('Request form reset');
1742
+
1743
+ // This happens after an `await`, and is not wrapped in startTransition,
1744
+ // so it will be scheduled synchronously instead of with the transition.
1745
+ // This is almost certainly a mistake, so we log a warning in dev.
1746
+ requestFormReset(formRef.current);
1747
+
1748
+ await getText('Wait 2');
1749
+ Scheduler.log('Action finished');
1750
+ });
1751
+ });
1752
+ assertLog(['Action started']);
1753
+ expect(inputRef.current.value).toBe(' Updated ');
1754
+
1755
+ // This triggers a synchronous requestFormReset, and a warning
1756
+ await expect(async () => {
1757
+ await act(() => resolveText('Wait 1'));
1758
+ }).toErrorDev(['requestFormReset was called outside a transition'], {
1759
+ withoutStack: true,
1760
+ });
1761
+ assertLog(['Request form reset']);
1762
+
1763
+ // The form was reset even though the action didn't finish.
1764
+ expect(inputRef.current.value).toBe('Initial');
1765
+ });
1766
});