main
js 170 lines 3.8 KB
Raw
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 // sanity tests to make sure act() works without a mocked scheduler
11
12 let React;
13 let ReactDOMClient;
14 let act;
15 let container;
16 let yields;
17 let prevActGlobal;
18
19 function clearLog() {
20 try {
21 return yields;
22 } finally {
23 yields = [];
24 }
25 }
26
27 beforeEach(() => {
28 prevActGlobal = global.IS_REACT_ACT_ENVIRONMENT;
29 global.IS_REACT_ACT_ENVIRONMENT = true;
30 jest.resetModules();
31 jest.unmock('scheduler');
32 yields = [];
33 React = require('react');
34 ReactDOMClient = require('react-dom/client');
35 act = React.act;
36 container = document.createElement('div');
37 document.body.appendChild(container);
38 });
39
40 afterEach(() => {
41 global.IS_REACT_ACT_ENVIRONMENT = prevActGlobal;
42 document.body.removeChild(container);
43 });
44
45 // @gate __DEV__
46 test('can use act to flush effects', async () => {
47 function App() {
48 React.useEffect(() => {
49 yields.push(100);
50 });
51 return null;
52 }
53
54 const root = ReactDOMClient.createRoot(container);
55 await act(() => {
56 root.render(<App />);
57 });
58
59 expect(clearLog()).toEqual([100]);
60 });
61
62 // @gate __DEV__
63 test('flushes effects on every call', async () => {
64 function App() {
65 const [ctr, setCtr] = React.useState(0);
66 React.useEffect(() => {
67 yields.push(ctr);
68 });
69 return (
70 <button id="button" onClick={() => setCtr(x => x + 1)}>
71 {ctr}
72 </button>
73 );
74 }
75
76 const root = ReactDOMClient.createRoot(container);
77 await act(() => {
78 root.render(<App />);
79 });
80
81 expect(clearLog()).toEqual([0]);
82
83 const button = container.querySelector('#button');
84 function click() {
85 button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
86 }
87
88 act(() => {
89 click();
90 click();
91 click();
92 });
93 // it consolidates the 3 updates, then fires the effect
94 expect(clearLog()).toEqual([3]);
95 act(click);
96 expect(clearLog()).toEqual([4]);
97 act(click);
98 expect(clearLog()).toEqual([5]);
99 expect(button.innerHTML).toEqual('5');
100 });
101
102 // @gate __DEV__
103 test("should keep flushing effects until they're done", async () => {
104 function App() {
105 const [ctr, setCtr] = React.useState(0);
106 React.useEffect(() => {
107 if (ctr < 5) {
108 setCtr(x => x + 1);
109 }
110 });
111 return ctr;
112 }
113
114 const root = ReactDOMClient.createRoot(container);
115 await act(() => {
116 root.render(<App />);
117 });
118
119 expect(container.innerHTML).toEqual('5');
120 });
121
122 // @gate __DEV__
123 test('should flush effects only on exiting the outermost act', async () => {
124 function App() {
125 React.useEffect(() => {
126 yields.push(0);
127 });
128 return null;
129 }
130 const root = ReactDOMClient.createRoot(container);
131 // let's nest a couple of act() calls
132 await act(async () => {
133 await act(() => {
134 root.render(<App />);
135 });
136 // the effect wouldn't have yielded yet because
137 // we're still inside an act() scope
138 expect(clearLog()).toEqual([]);
139 });
140 // but after exiting the last one, effects get flushed
141 expect(clearLog()).toEqual([0]);
142 });
143
144 // @gate __DEV__
145 test('can handle cascading promises', async () => {
146 // this component triggers an effect, that waits a tick,
147 // then sets state. repeats this 5 times.
148 function App() {
149 const [state, setState] = React.useState(0);
150 async function ticker() {
151 await null;
152 await act(() => {
153 setState(x => x + 1);
154 });
155 }
156 React.useEffect(() => {
157 yields.push(state);
158 ticker();
159 }, [Math.min(state, 4)]);
160 return state;
161 }
162
163 const root = ReactDOMClient.createRoot(container);
164 await act(() => {
165 root.render(<App />);
166 });
167 // all 5 ticks present and accounted for
168 expect(clearLog()).toEqual([0, 1, 2, 3, 4]);
169 expect(container.innerHTML).toBe('5');
170 });