@samitouri / QOS-React-2 / commits / d278a3ff8b

`act()` - s / flushPassiveEffects / Scheduler.unstable_flushWithoutYielding (#15591)

* s/flushPassiveEffects/unstable_flushWithoutYielding a first crack at flushing the scheduler manually from inside act(). uses unstable_flushWithoutYielding(). The tests that changed, mostly replaced toFlushAndYield(...) with toHaveYielded(). For some tests that tested the state of the tree before flushing effects (but still after updates), I replaced act() with bacthedUpdates(). * ugh lint * pass build, flushPassiveEffects returns nothing now * pass test-fire * flush all work (not just effects), add a compatibility mode of note, unstable_flushWithoutYielding now returns a boolean much like flushPassiveEffects * umd build for scheduler/unstable_mock, pass the fixture with it * add a comment to Shcduler.umd.js for why we're exporting unstable_flushWithoutYielding * run testsutilsact tests in both sync/concurrent modes * augh lint * use a feature flag for the missing mock scheduler warning I also tried writing a test for it, but couldn't get the scheduler to unmock. included the failing test. * Update ReactTestUtilsAct-test.js - pass the mock scheduler warning test, - rewrite some tests to use Scheduler.yieldValue - structure concurrent/legacy suites neatly * pass failing tests in batchedmode-test * fix pretty/lint/import errors * pass test-build * nit: pull .create(null) out of the act() call

Sunil Pai committed May 16, 2019 at 17:12 UTC d278a3ff8b2863afb9ab7807e42d31f33c84eb78
23 files changed +642 -494
fixtures/dom/.gitignore
+2
@@ -8,6 +8,8 @@ coverage
8
9 # production
10 build
11 +public/scheduler-unstable_mock.development.js
12 +public/scheduler-unstable_mock.production.min.js
13 public/react.development.js
14 public/react.production.min.js
15 public/react-dom.development.js
fixtures/dom/package.json
+1 -1
@@ -18,7 +18,7 @@
18 },
19 "scripts": {
20 "start": "react-scripts start",
21 - "prestart": "cp ../../build/node_modules/react/umd/react.development.js ../../build/node_modules/react-dom/umd/react-dom.development.js ../../build/node_modules/react/umd/react.production.min.js ../../build/node_modules/react-dom/umd/react-dom.production.min.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.development.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.production.min.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.development.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.production.min.js public/",
21 + "prestart": "cp ../../build/node_modules/scheduler/umd/scheduler-unstable_mock.development.js ../../build/node_modules/scheduler/umd/scheduler-unstable_mock.production.min.js ../../build/node_modules/react/umd/react.development.js ../../build/node_modules/react-dom/umd/react-dom.development.js ../../build/node_modules/react/umd/react.production.min.js ../../build/node_modules/react-dom/umd/react-dom.production.min.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.development.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.production.min.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.development.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.production.min.js public/",
22 "build": "react-scripts build && cp build/index.html build/200.html",
23 "test": "react-scripts test --env=jsdom",
24 "eject": "react-scripts eject"
fixtures/dom/public/act-dom.html
+4
@@ -7,7 +7,11 @@
7 this page tests whether act runs properly in a browser.
8 <br/>
9 your console should say "5"
10 + <script src='scheduler-unstable_mock.development.js'></script>
11 <script src='react.development.js'></script>
12 + <script type="text/javascript">
13 + window.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler = window.SchedulerMock
14 + </script>
15 <script src='react-dom.development.js'></script>
16 <script src='react-dom-test-utils.development.js'></script>
17 <script>
packages/react-dom/src/__tests__/ReactTestUtilsAct-test.internal.js new
+28
@@ -0,0 +1,28 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 +let ReactFeatureFlags;
11 +let act;
12 +describe('mocked scheduler', () => {
13 + beforeEach(() => {
14 + jest.resetModules();
15 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
16 + ReactFeatureFlags.warnAboutMissingMockScheduler = true;
17 + jest.unmock('scheduler');
18 + act = require('react-dom/test-utils').act;
19 + });
20 + it("should warn when the scheduler isn't mocked", () => {
21 + expect(() => act(() => {})).toWarnDev(
22 + [
23 + 'Starting from React v17, the "scheduler" module will need to be mocked',
24 + ],
25 + {withoutStack: true},
26 + );
27 + });
28 +});
packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
+400 -391
@@ -11,6 +11,7 @@ let React;
11 let ReactDOM;
12 let ReactTestUtils;
13 let SchedulerTracing;
14 +let Scheduler;
15 let act;
16 let container;
17
@@ -25,482 +26,490 @@ function sleep(period) {
26 }
27
28 describe('ReactTestUtils.act()', () => {
28 - beforeEach(() => {
29 - jest.resetModules();
30 - React = require('react');
31 - ReactDOM = require('react-dom');
32 - ReactTestUtils = require('react-dom/test-utils');
33 - SchedulerTracing = require('scheduler/tracing');
34 - act = ReactTestUtils.act;
35 - container = document.createElement('div');
36 - document.body.appendChild(container);
37 - });
38 - afterEach(() => {
39 - ReactDOM.unmountComponentAtNode(container);
40 - document.body.removeChild(container);
41 - });
42 -
43 - describe('sync', () => {
44 - it('can use act to flush effects', () => {
45 - function App(props) {
46 - React.useEffect(props.callback);
47 - return null;
48 - }
49 -
50 - let calledLog = [];
51 - act(() => {
52 - ReactDOM.render(
53 - <App
54 - callback={() => {
55 - calledLog.push(calledLog.length);
56 - }}
57 - />,
58 - document.createElement('div'),
59 - );
60 - });
29 + // first we run all the tests with concurrent mode
30 + let concurrentRoot;
31 + function renderConcurrent(el, dom) {
32 + concurrentRoot = ReactDOM.unstable_createRoot(dom);
33 + concurrentRoot.render(el);
34 + }
35 + function unmountConcurrent(_dom) {
36 + if (concurrentRoot !== null) {
37 + concurrentRoot.unmount();
38 + concurrentRoot = null;
39 + }
40 + }
41 + runActTests('concurrent mode', renderConcurrent, unmountConcurrent);
42 +
43 + // and then in sync mode
44 + function renderSync(el, dom) {
45 + ReactDOM.render(el, dom);
46 + }
47 + function unmountSync(dom) {
48 + ReactDOM.unmountComponentAtNode(dom);
49 + }
50 + runActTests('legacy sync mode', renderSync, unmountSync);
51 +});
52
62 - expect(calledLog).toEqual([0]);
53 +function runActTests(label, render, unmount) {
54 + describe(label, () => {
55 + beforeEach(() => {
56 + jest.resetModules();
57 + React = require('react');
58 + ReactDOM = require('react-dom');
59 + ReactTestUtils = require('react-dom/test-utils');
60 + SchedulerTracing = require('scheduler/tracing');
61 + Scheduler = require('scheduler');
62 + act = ReactTestUtils.act;
63 + container = document.createElement('div');
64 + document.body.appendChild(container);
65 });
64 -
65 - it('flushes effects on every call', () => {
66 - function App(props) {
67 - let [ctr, setCtr] = React.useState(0);
68 - React.useEffect(() => {
69 - props.callback(ctr);
70 - });
71 - return (
72 - <button id="button" onClick={() => setCtr(x => x + 1)}>
73 - {ctr}
74 - </button>
75 - );
76 - }
77 -
78 - let calledCounter = 0;
79 - act(() => {
80 - ReactDOM.render(
81 - <App
82 - callback={val => {
83 - calledCounter = val;
84 - }}
85 - />,
86 - container,
87 - );
88 - });
89 - const button = document.getElementById('button');
90 - function click() {
91 - button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
92 - }
93 -
94 - act(() => {
95 - click();
96 - click();
97 - click();
98 - });
99 - expect(calledCounter).toBe(3);
100 - act(click);
101 - expect(calledCounter).toBe(4);
102 - act(click);
103 - expect(calledCounter).toBe(5);
104 - expect(button.innerHTML).toBe('5');
66 + afterEach(() => {
67 + unmount(container);
68 + document.body.removeChild(container);
69 });
70 + describe('sync', () => {
71 + it('can use act to flush effects', () => {
72 + function App() {
73 + React.useEffect(() => {
74 + Scheduler.yieldValue(100);
75 + });
76 + return null;
77 + }
78
107 - it('should flush effects recursively', () => {
108 - function App() {
109 - let [ctr, setCtr] = React.useState(0);
110 - React.useEffect(() => {
111 - if (ctr < 5) {
112 - setCtr(x => x + 1);
113 - }
79 + act(() => {
80 + render(<App />, container);
81 });
115 - return ctr;
116 - }
82
118 - act(() => {
119 - ReactDOM.render(<App />, container);
83 + expect(Scheduler).toHaveYielded([100]);
84 });
85
122 - expect(container.innerHTML).toBe('5');
123 - });
124 -
125 - it('detects setState being called outside of act(...)', () => {
126 - let setValue = null;
127 - function App() {
128 - let [value, _setValue] = React.useState(0);
129 - setValue = _setValue;
130 - return (
131 - <button id="button" onClick={() => setValue(2)}>
132 - {value}
133 - </button>
134 - );
135 - }
136 - let button;
137 - act(() => {
138 - ReactDOM.render(<App />, container);
139 - button = container.querySelector('#button');
140 - button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
141 - });
142 - expect(button.innerHTML).toBe('2');
143 - expect(() => setValue(1)).toWarnDev([
144 - 'An update to App inside a test was not wrapped in act(...).',
145 - ]);
146 - });
147 - describe('fake timers', () => {
148 - beforeEach(() => {
149 - jest.useFakeTimers();
150 - });
151 - afterEach(() => {
152 - jest.useRealTimers();
153 - });
154 - it('lets a ticker update', () => {
86 + it('flushes effects on every call', () => {
87 function App() {
156 - let [toggle, setToggle] = React.useState(0);
88 + let [ctr, setCtr] = React.useState(0);
89 React.useEffect(() => {
158 - let timeout = setTimeout(() => {
159 - setToggle(1);
160 - }, 200);
161 - return () => clearTimeout(timeout);
162 - }, []);
163 - return toggle;
90 + Scheduler.yieldValue(ctr);
91 + });
92 + return (
93 + <button id="button" onClick={() => setCtr(x => x + 1)}>
94 + {ctr}
95 + </button>
96 + );
97 }
98
99 act(() => {
167 - ReactDOM.render(<App />, container);
100 + render(<App />, container);
101 });
102 + expect(Scheduler).toHaveYielded([0]);
103 + const button = container.querySelector('#button');
104 + function click() {
105 + button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
106 + }
107 +
108 act(() => {
170 - jest.runAllTimers();
109 + click();
110 + click();
111 + click();
112 });
172 -
173 - expect(container.innerHTML).toBe('1');
113 + // it consolidates the 3 updates, then fires the effect
114 + expect(Scheduler).toHaveYielded([3]);
115 + act(click);
116 + expect(Scheduler).toHaveYielded([4]);
117 + act(click);
118 + expect(Scheduler).toHaveYielded([5]);
119 + expect(button.innerHTML).toBe('5');
120 });
175 - it('can use the async version to catch microtasks', async () => {
121 +
122 + it("should keep flushing effects until the're done", () => {
123 function App() {
177 - let [toggle, setToggle] = React.useState(0);
124 + let [ctr, setCtr] = React.useState(0);
125 React.useEffect(() => {
179 - // just like the previous test, except we
180 - // use a promise and schedule the update
181 - // after it resolves
182 - sleep(200).then(() => setToggle(1));
183 - }, []);
184 - return toggle;
126 + if (ctr < 5) {
127 + setCtr(x => x + 1);
128 + }
129 + });
130 + return ctr;
131 }
132
133 act(() => {
188 - ReactDOM.render(<App />, container);
189 - });
190 - await act(async () => {
191 - jest.runAllTimers();
134 + render(<App />, container);
135 });
136
194 - expect(container.innerHTML).toBe('1');
137 + expect(container.innerHTML).toBe('5');
138 });
196 - it('can handle cascading promises with fake timers', async () => {
197 - // this component triggers an effect, that waits a tick,
198 - // then sets state. repeats this 5 times.
139 +
140 + it('warns if a setState is called outside of act(...)', () => {
141 + let setValue = null;
142 function App() {
200 - let [state, setState] = React.useState(0);
201 - async function ticker() {
202 - await null;
203 - setState(x => x + 1);
204 - }
205 - React.useEffect(
206 - () => {
207 - ticker();
208 - },
209 - [Math.min(state, 4)],
210 - );
211 - return state;
143 + let [value, _setValue] = React.useState(0);
144 + setValue = _setValue;
145 + return value;
146 }
213 - const el = document.createElement('div');
214 - await act(async () => {
215 - ReactDOM.render(<App />, el);
147 +
148 + act(() => {
149 + render(<App />, container);
150 });
151
218 - // all 5 ticks present and accounted for
219 - expect(el.innerHTML).toBe('5');
152 + expect(() => setValue(1)).toWarnDev([
153 + 'An update to App inside a test was not wrapped in act(...).',
154 + ]);
155 });
221 - it('flushes immediate re-renders with act', () => {
222 - function App() {
223 - let [ctr, setCtr] = React.useState(0);
224 - React.useEffect(() => {
225 - if (ctr === 0) {
226 - setCtr(1);
156 + describe('fake timers', () => {
157 + beforeEach(() => {
158 + jest.useFakeTimers();
159 + });
160 + afterEach(() => {
161 + jest.useRealTimers();
162 + });
163 + it('lets a ticker update', () => {
164 + function App() {
165 + let [toggle, setToggle] = React.useState(0);
166 + React.useEffect(() => {
167 + let timeout = setTimeout(() => {
168 + setToggle(1);
169 + }, 200);
170 + return () => clearTimeout(timeout);
171 + }, []);
172 + return toggle;
173 + }
174 +
175 + act(() => {
176 + render(<App />, container);
177 + });
178 + act(() => {
179 + jest.runAllTimers();
180 + });
181 +
182 + expect(container.innerHTML).toBe('1');
183 + });
184 + it('can use the async version to catch microtasks', async () => {
185 + function App() {
186 + let [toggle, setToggle] = React.useState(0);
187 + React.useEffect(() => {
188 + // just like the previous test, except we
189 + // use a promise and schedule the update
190 + // after it resolves
191 + sleep(200).then(() => setToggle(1));
192 + }, []);
193 + return toggle;
194 + }
195 +
196 + act(() => {
197 + render(<App />, container);
198 + });
199 + await act(async () => {
200 + jest.runAllTimers();
201 + });
202 +
203 + expect(container.innerHTML).toBe('1');
204 + });
205 + it('can handle cascading promises with fake timers', async () => {
206 + // this component triggers an effect, that waits a tick,
207 + // then sets state. repeats this 5 times.
208 + function App() {
209 + let [state, setState] = React.useState(0);
210 + async function ticker() {
211 + await null;
212 + setState(x => x + 1);
213 }
228 - const timeout = setTimeout(() => setCtr(2), 1000);
229 - return () => clearTimeout(timeout);
214 + React.useEffect(
215 + () => {
216 + ticker();
217 + },
218 + [Math.min(state, 4)],
219 + );
220 + return state;
221 + }
222 +
223 + await act(async () => {
224 + render(<App />, container);
225 });
231 - return ctr;
232 - }
226
234 - act(() => {
235 - ReactDOM.render(<App />, container);
236 - // Since the effects won't be flushed yet, this does not advance the timer
237 - jest.runAllTimers();
227 + // all 5 ticks present and accounted for
228 + expect(container.innerHTML).toBe('5');
229 });
230 + it('flushes immediate re-renders with act', () => {
231 + function App() {
232 + let [ctr, setCtr] = React.useState(0);
233 + React.useEffect(() => {
234 + if (ctr === 0) {
235 + setCtr(1);
236 + }
237 + const timeout = setTimeout(() => setCtr(2), 1000);
238 + return () => clearTimeout(timeout);
239 + });
240 + return ctr;
241 + }
242
240 - expect(container.innerHTML).toBe('1');
243 + act(() => {
244 + render(<App />, container);
245 + // Since effects haven't been flushed yet, this does not advance the timer
246 + jest.runAllTimers();
247 + });
248
242 - act(() => {
243 - jest.runAllTimers();
249 + expect(container.innerHTML).toBe('1');
250 +
251 + act(() => {
252 + jest.runAllTimers();
253 + });
254 +
255 + expect(container.innerHTML).toBe('2');
256 });
257 + });
258
246 - expect(container.innerHTML).toBe('2');
259 + it('warns if you return a value inside act', () => {
260 + expect(() => act(() => null)).toWarnDev(
261 + [
262 + 'The callback passed to act(...) function must return undefined, or a Promise.',
263 + ],
264 + {withoutStack: true},
265 + );
266 + expect(() => act(() => 123)).toWarnDev(
267 + [
268 + 'The callback passed to act(...) function must return undefined, or a Promise.',
269 + ],
270 + {withoutStack: true},
271 + );
272 });
248 - });
273
250 - it('warns if you return a value inside act', () => {
251 - expect(() => act(() => null)).toWarnDev(
252 - [
253 - 'The callback passed to act(...) function must return undefined, or a Promise.',
254 - ],
255 - {withoutStack: true},
256 - );
257 - expect(() => act(() => 123)).toWarnDev(
258 - [
259 - 'The callback passed to act(...) function must return undefined, or a Promise.',
260 - ],
261 - {withoutStack: true},
262 - );
274 + it('warns if you try to await a sync .act call', () => {
275 + expect(() => act(() => {}).then(() => {})).toWarnDev(
276 + [
277 + 'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
278 + ],
279 + {withoutStack: true},
280 + );
281 + });
282 });
283 + describe('asynchronous tests', () => {
284 + it('can handle timers', async () => {
285 + function App() {
286 + let [ctr, setCtr] = React.useState(0);
287 + function doSomething() {
288 + setTimeout(() => {
289 + setCtr(1);
290 + }, 50);
291 + }
292
265 - it('warns if you try to await an .act call', () => {
266 - expect(() => act(() => {}).then(() => {})).toWarnDev(
267 - [
268 - 'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
269 - ],
270 - {withoutStack: true},
271 - );
272 - });
273 - });
274 - describe('asynchronous tests', () => {
275 - it('can handle timers', async () => {
276 - function App() {
277 - let [ctr, setCtr] = React.useState(0);
278 - function doSomething() {
279 - setTimeout(() => {
280 - setCtr(1);
281 - }, 50);
293 + React.useEffect(() => {
294 + doSomething();
295 + }, []);
296 + return ctr;
297 }
283 -
284 - React.useEffect(() => {
285 - doSomething();
286 - }, []);
287 - return ctr;
288 - }
289 - const el = document.createElement('div');
290 - await act(async () => {
298 act(() => {
292 - ReactDOM.render(<App />, el);
299 + render(<App />, container);
300 });
294 -
295 - await sleep(100);
296 - expect(el.innerHTML).toBe('1');
301 + await act(async () => {
302 + await sleep(100);
303 + });
304 + expect(container.innerHTML).toBe('1');
305 });
298 - });
306
300 - it('can handle async/await', async () => {
301 - function App() {
302 - let [ctr, setCtr] = React.useState(0);
303 - async function someAsyncFunction() {
304 - // queue a bunch of promises to be sure they all flush
305 - await null;
306 - await null;
307 - await null;
308 - setCtr(1);
307 + it('can handle async/await', async () => {
308 + function App() {
309 + let [ctr, setCtr] = React.useState(0);
310 + async function someAsyncFunction() {
311 + // queue a bunch of promises to be sure they all flush
312 + await null;
313 + await null;
314 + await null;
315 + setCtr(1);
316 + }
317 + React.useEffect(() => {
318 + someAsyncFunction();
319 + }, []);
320 + return ctr;
321 }
310 - React.useEffect(() => {
311 - someAsyncFunction();
312 - }, []);
313 - return ctr;
314 - }
315 - const el = document.createElement('div');
322
317 - await act(async () => {
318 - act(() => {
319 - ReactDOM.render(<App />, el);
323 + await act(async () => {
324 + act(() => {
325 + render(<App />, container);
326 + });
327 + // pending promises will close before this ends
328 });
321 - // pending promises will close before this ends
329 + expect(container.innerHTML).toEqual('1');
330 });
323 - expect(el.innerHTML).toEqual('1');
324 - });
331
326 - it('warns if you do not await an act call', async () => {
327 - spyOnDevAndProd(console, 'error');
328 - act(async () => {});
329 - // it's annoying that we have to wait a tick before this warning comes in
330 - await sleep(0);
331 - if (__DEV__) {
332 - expect(console.error.calls.count()).toEqual(1);
333 - expect(console.error.calls.argsFor(0)[0]).toMatch(
334 - 'You called act(async () => ...) without await.',
335 - );
336 - }
337 - });
332 + it('warns if you do not await an act call', async () => {
333 + spyOnDevAndProd(console, 'error');
334 + act(async () => {});
335 + // it's annoying that we have to wait a tick before this warning comes in
336 + await sleep(0);
337 + if (__DEV__) {
338 + expect(console.error.calls.count()).toEqual(1);
339 + expect(console.error.calls.argsFor(0)[0]).toMatch(
340 + 'You called act(async () => ...) without await.',
341 + );
342 + }
343 + });
344 +
345 + it('warns if you try to interleave multiple act calls', async () => {
346 + spyOnDevAndProd(console, 'error');
347 + // let's try to cheat and spin off a 'thread' with an act call
348 + (async () => {
349 + await act(async () => {
350 + await sleep(50);
351 + });
352 + })();
353
339 - it('warns if you try to interleave multiple act calls', async () => {
340 - spyOnDevAndProd(console, 'error');
341 - // let's try to cheat and spin off a 'thread' with an act call
342 - (async () => {
354 await act(async () => {
344 - await sleep(50);
355 + await sleep(100);
356 });
346 - })();
357
348 - await act(async () => {
349 - await sleep(100);
358 + await sleep(150);
359 + if (__DEV__) {
360 + expect(console.error).toHaveBeenCalledTimes(1);
361 + }
362 });
363
352 - await sleep(150);
353 - if (__DEV__) {
354 - expect(console.error).toHaveBeenCalledTimes(1);
355 - }
356 - });
357 -
358 - it('commits and effects are guaranteed to be flushed', async () => {
359 - function App(props) {
360 - let [state, setState] = React.useState(0);
361 - async function something() {
362 - await null;
363 - setState(1);
364 + it('commits and effects are guaranteed to be flushed', async () => {
365 + function App() {
366 + let [state, setState] = React.useState(0);
367 + async function something() {
368 + await null;
369 + setState(1);
370 + }
371 + React.useEffect(() => {
372 + something();
373 + }, []);
374 + React.useEffect(() => {
375 + Scheduler.yieldValue(state);
376 + });
377 + return state;
378 }
365 - React.useEffect(() => {
366 - something();
367 - }, []);
368 - React.useEffect(() => {
369 - props.callback();
370 - });
371 - return state;
372 - }
373 - let ctr = 0;
374 - const div = document.createElement('div');
379
376 - await act(async () => {
377 - act(() => {
378 - ReactDOM.render(<App callback={() => ctr++} />, div);
380 + await act(async () => {
381 + act(() => {
382 + render(<App />, container);
383 + });
384 + expect(container.innerHTML).toBe('0');
385 + expect(Scheduler).toHaveYielded([0]);
386 });
380 - expect(div.innerHTML).toBe('0');
381 - expect(ctr).toBe(1);
387 + // this may seem odd, but it matches user behaviour -
388 + // a flash of "0" followed by "1"
389 +
390 + expect(container.innerHTML).toBe('1');
391 + expect(Scheduler).toHaveYielded([1]);
392 });
383 - // this may seem odd, but it matches user behaviour -
384 - // a flash of "0" followed by "1"
393
386 - expect(div.innerHTML).toBe('1');
387 - expect(ctr).toBe(2);
388 - });
394 + it('propagates errors', async () => {
395 + let err;
396 + try {
397 + await act(async () => {
398 + await sleep(100);
399 + throw new Error('some error');
400 + });
401 + } catch (_err) {
402 + err = _err;
403 + } finally {
404 + expect(err instanceof Error).toBe(true);
405 + expect(err.message).toBe('some error');
406 + }
407 + });
408 + it('can handle cascading promises', async () => {
409 + // this component triggers an effect, that waits a tick,
410 + // then sets state. repeats this 5 times.
411 + function App() {
412 + let [state, setState] = React.useState(0);
413 + async function ticker() {
414 + await null;
415 + setState(x => x + 1);
416 + }
417 + React.useEffect(
418 + () => {
419 + Scheduler.yieldValue(state);
420 + ticker();
421 + },
422 + [Math.min(state, 4)],
423 + );
424 + return state;
425 + }
426
390 - it('propagates errors', async () => {
391 - let err;
392 - try {
427 await act(async () => {
394 - throw new Error('some error');
428 + render(<App />, container);
429 });
396 - } catch (_err) {
397 - err = _err;
398 - } finally {
399 - expect(err instanceof Error).toBe(true);
400 - expect(err.message).toBe('some error');
401 - }
402 - });
403 - it('can handle cascading promises', async () => {
404 - // this component triggers an effect, that waits a tick,
405 - // then sets state. repeats this 5 times.
406 - function App() {
407 - let [state, setState] = React.useState(0);
408 - async function ticker() {
409 - await null;
410 - setState(x => x + 1);
411 - }
412 - React.useEffect(
413 - () => {
414 - ticker();
415 - },
416 - [Math.min(state, 4)],
417 - );
418 - return state;
419 - }
420 - const el = document.createElement('div');
421 - await act(async () => {
422 - ReactDOM.render(<App />, el);
430 + // all 5 ticks present and accounted for
431 + expect(Scheduler).toHaveYielded([0, 1, 2, 3, 4]);
432 + expect(container.innerHTML).toBe('5');
433 });
424 - // all 5 ticks present and accounted for
425 - expect(el.innerHTML).toBe('5');
434 });
427 - });
435
429 - describe('interaction tracing', () => {
430 - if (__DEV__) {
431 - it('should correctly trace interactions for sync roots', () => {
432 - let expectedInteraction;
436 + describe('interaction tracing', () => {
437 + if (__DEV__) {
438 + it('should correctly trace interactions for sync roots', () => {
439 + let expectedInteraction;
440 +
441 + const Component = jest.fn(() => {
442 + expect(expectedInteraction).toBeDefined();
443
434 - const Component = jest.fn(() => {
435 - expect(expectedInteraction).toBeDefined();
444 + const interactions = SchedulerTracing.unstable_getCurrent();
445 + expect(interactions.size).toBe(1);
446 + expect(interactions).toContain(expectedInteraction);
447
437 - const interactions = SchedulerTracing.unstable_getCurrent();
438 - expect(interactions.size).toBe(1);
439 - expect(interactions).toContain(expectedInteraction);
448 + return null;
449 + });
450
441 - return null;
442 - });
451 + act(() => {
452 + SchedulerTracing.unstable_trace(
453 + 'mount traced inside act',
454 + performance.now(),
455 + () => {
456 + const interactions = SchedulerTracing.unstable_getCurrent();
457 + expect(interactions.size).toBe(1);
458 + expectedInteraction = Array.from(interactions)[0];
459 +
460 + render(<Component />, container);
461 + },
462 + );
463 + });
464 +
465 + act(() => {
466 + SchedulerTracing.unstable_trace(
467 + 'update traced inside act',
468 + performance.now(),
469 + () => {
470 + const interactions = SchedulerTracing.unstable_getCurrent();
471 + expect(interactions.size).toBe(1);
472 + expectedInteraction = Array.from(interactions)[0];
473 +
474 + render(<Component />, container);
475 + },
476 + );
477 + });
478 +
479 + const secondContainer = document.createElement('div');
480
444 - act(() => {
481 SchedulerTracing.unstable_trace(
446 - 'mount traced inside act',
482 + 'mount traced outside act',
483 performance.now(),
484 () => {
449 - const interactions = SchedulerTracing.unstable_getCurrent();
450 - expect(interactions.size).toBe(1);
451 - expectedInteraction = Array.from(interactions)[0];
485 + act(() => {
486 + const interactions = SchedulerTracing.unstable_getCurrent();
487 + expect(interactions.size).toBe(1);
488 + expectedInteraction = Array.from(interactions)[0];
489
453 - ReactDOM.render(<Component />, container);
490 + render(<Component />, secondContainer);
491 + });
492 },
493 );
456 - });
494
458 - act(() => {
495 SchedulerTracing.unstable_trace(
460 - 'update traced inside act',
496 + 'update traced outside act',
497 performance.now(),
498 () => {
463 - const interactions = SchedulerTracing.unstable_getCurrent();
464 - expect(interactions.size).toBe(1);
465 - expectedInteraction = Array.from(interactions)[0];
499 + act(() => {
500 + const interactions = SchedulerTracing.unstable_getCurrent();
501 + expect(interactions.size).toBe(1);
502 + expectedInteraction = Array.from(interactions)[0];
503
467 - ReactDOM.render(<Component />, container);
504 + render(<Component />, secondContainer);
505 + });
506 },
507 );
470 - });
471 -
472 - const secondContainer = document.createElement('div');
473 -
474 - SchedulerTracing.unstable_trace(
475 - 'mount traced outside act',
476 - performance.now(),
477 - () => {
478 - act(() => {
479 - const interactions = SchedulerTracing.unstable_getCurrent();
480 - expect(interactions.size).toBe(1);
481 - expectedInteraction = Array.from(interactions)[0];
508
483 - ReactDOM.render(<Component />, secondContainer);
484 - });
485 - },
486 - );
487 -
488 - SchedulerTracing.unstable_trace(
489 - 'update traced outside act',
490 - performance.now(),
491 - () => {
492 - act(() => {
493 - const interactions = SchedulerTracing.unstable_getCurrent();
494 - expect(interactions.size).toBe(1);
495 - expectedInteraction = Array.from(interactions)[0];
496 -
497 - ReactDOM.render(<Component />, secondContainer);
498 - });
499 - },
500 - );
501 -
502 - expect(Component).toHaveBeenCalledTimes(4);
503 - });
504 - }
509 + expect(Component).toHaveBeenCalledTimes(4);
510 + unmount(secondContainer);
511 + });
512 + }
513 + });
514 });
506 -});
515 +}
packages/react-dom/src/test-utils/ReactTestUtilsAct.js
+33 -9
@@ -12,7 +12,9 @@ import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
12 import warningWithoutStack from 'shared/warningWithoutStack';
13 import ReactDOM from 'react-dom';
14 import ReactSharedInternals from 'shared/ReactSharedInternals';
15 +import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags';
16 import enqueueTask from 'shared/enqueueTask';
17 +import * as Scheduler from 'scheduler';
18
19 // Keep in sync with ReactDOMUnstableNativeDependencies.js
20 // ReactDOM.js, and ReactTestUtils.js:
@@ -40,16 +42,33 @@ const {ReactShouldWarnActingUpdates} = ReactSharedInternals;
42 // this implementation should be exactly the same in
43 // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
44
43 -// we track the 'depth' of the act() calls with this counter,
44 -// so we can tell if any async act() calls try to run in parallel.
45 -let actingUpdatesScopeDepth = 0;
45 +let hasWarnedAboutMissingMockScheduler = false;
46 +const flushWork =
47 + Scheduler.unstable_flushWithoutYielding ||
48 + function() {
49 + if (warnAboutMissingMockScheduler === true) {
50 + if (hasWarnedAboutMissingMockScheduler === false) {
51 + warningWithoutStack(
52 + null,
53 + 'Starting from React v17, the "scheduler" module will need to be mocked ' +
54 + 'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' +
55 + "to the top of your tests, or in your framework's global config file -\n\n" +
56 + 'As an example, for jest - \n' +
57 + "jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" +
58 + 'For more info, visit https://fb.me/react-mock-scheduler',
59 + );
60 + hasWarnedAboutMissingMockScheduler = true;
61 + }
62 + }
63 + while (flushPassiveEffects()) {}
64 + };
65
47 -function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
66 +function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
67 try {
49 - flushPassiveEffects();
68 + flushWork();
69 enqueueTask(() => {
51 - if (flushPassiveEffects()) {
52 - flushEffectsAndMicroTasks(onDone);
70 + if (flushWork()) {
71 + flushWorkAndMicroTasks(onDone);
72 } else {
73 onDone();
74 }
@@ -59,6 +78,11 @@ function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
78 }
79 }
80
81 +// we track the 'depth' of the act() calls with this counter,
82 +// so we can tell if any async act() calls try to run in parallel.
83 +
84 +let actingUpdatesScopeDepth = 0;
85 +
86 function act(callback: () => Thenable) {
87 let previousActingUpdatesScopeDepth;
88 if (__DEV__) {
@@ -119,7 +143,7 @@ function act(callback: () => Thenable) {
143 called = true;
144 result.then(
145 () => {
122 - flushEffectsAndMicroTasks((err: ?Error) => {
146 + flushWorkAndMicroTasks((err: ?Error) => {
147 onDone();
148 if (err) {
149 reject(err);
@@ -147,7 +171,7 @@ function act(callback: () => Thenable) {
171
172 // flush effects until none remain, and cleanup
173 try {
150 - while (flushPassiveEffects()) {}
174 + flushWork();
175 onDone();
176 } catch (err) {
177 onDone();
packages/react-noop-renderer/src/createReactNoop.js
+35 -8
@@ -32,7 +32,10 @@ import warning from 'shared/warning';
32 import enqueueTask from 'shared/enqueueTask';
33 import ReactSharedInternals from 'shared/ReactSharedInternals';
34 import warningWithoutStack from 'shared/warningWithoutStack';
35 -import {enableEventAPI} from 'shared/ReactFeatureFlags';
35 +import {
36 + warnAboutMissingMockScheduler,
37 + enableEventAPI,
38 +} from 'shared/ReactFeatureFlags';
39 import {ConcurrentRoot, BatchedRoot, LegacyRoot} from 'shared/ReactRootTags';
40
41 type EventTargetChildElement = {
@@ -652,14 +655,33 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
655 // this act() implementation should be exactly the same in
656 // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
657
655 - let actingUpdatesScopeDepth = 0;
658 + let hasWarnedAboutMissingMockScheduler = false;
659 + const flushWork =
660 + Scheduler.unstable_flushWithoutYielding ||
661 + function() {
662 + if (warnAboutMissingMockScheduler === true) {
663 + if (hasWarnedAboutMissingMockScheduler === false) {
664 + warningWithoutStack(
665 + null,
666 + 'Starting from React v17, the "scheduler" module will need to be mocked ' +
667 + 'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' +
668 + "to the top of your tests, or in your framework's global config file -\n\n" +
669 + 'As an example, for jest - \n' +
670 + "jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" +
671 + 'For more info, visit https://fb.me/react-mock-scheduler',
672 + );
673 + hasWarnedAboutMissingMockScheduler = true;
674 + }
675 + }
676 + while (flushPassiveEffects()) {}
677 + };
678
657 - function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
679 + function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
680 try {
659 - flushPassiveEffects();
681 + flushWork();
682 enqueueTask(() => {
661 - if (flushPassiveEffects()) {
662 - flushEffectsAndMicroTasks(onDone);
683 + if (flushWork()) {
684 + flushWorkAndMicroTasks(onDone);
685 } else {
686 onDone();
687 }
@@ -669,6 +691,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
691 }
692 }
693
694 + // we track the 'depth' of the act() calls with this counter,
695 + // so we can tell if any async act() calls try to run in parallel.
696 +
697 + let actingUpdatesScopeDepth = 0;
698 +
699 function act(callback: () => Thenable) {
700 let previousActingUpdatesScopeDepth;
701 if (__DEV__) {
@@ -729,7 +756,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
756 called = true;
757 result.then(
758 () => {
732 - flushEffectsAndMicroTasks((err: ?Error) => {
759 + flushWorkAndMicroTasks((err: ?Error) => {
760 onDone();
761 if (err) {
762 reject(err);
@@ -757,7 +784,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
784
785 // flush effects until none remain, and cleanup
786 try {
760 - while (flushPassiveEffects()) {}
787 + flushWork();
788 onDone();
789 } catch (err) {
790 onDone();
packages/react-reconciler/src/__tests__/ReactBatchedMode-test.internal.js
+2 -4
@@ -1,7 +1,6 @@
1 let React;
2 let ReactFeatureFlags;
3 let ReactNoop;
4 -let act;
4 let Scheduler;
5 let ReactCache;
6 let Suspense;
@@ -15,7 +14,6 @@ describe('ReactBatchedMode', () => {
14 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
15 React = require('react');
16 ReactNoop = require('react-noop-renderer');
18 - act = ReactNoop.act;
17 Scheduler = require('scheduler');
18 ReactCache = require('react-cache');
19 Suspense = React.Suspense;
@@ -146,10 +144,10 @@ describe('ReactBatchedMode', () => {
144 expect(root).toMatchRenderedOutput('A0B0');
145
146 // Schedule a batched update to the first sibling
149 - act(() => foo1.current.setStep(1));
147 + ReactNoop.batchedUpdates(() => foo1.current.setStep(1));
148
149 // Before it flushes, update the second sibling inside flushSync
152 - act(() =>
150 + ReactNoop.batchedUpdates(() =>
151 ReactNoop.flushSync(() => {
152 foo2.current.setStep(1);
153 }),
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+37 -34
@@ -95,7 +95,7 @@ describe('ReactHooks', () => {
95 setCounter2(1);
96 });
97
98 - expect(Scheduler).toFlushAndYield([
98 + expect(Scheduler).toHaveYielded([
99 'Parent: 1, 1',
100 'Child: 1, 1',
101 'Effect: 1, 1',
@@ -103,7 +103,7 @@ describe('ReactHooks', () => {
103
104 // Update that bails out.
105 act(() => setCounter1(1));
106 - expect(Scheduler).toFlushAndYield(['Parent: 1, 1']);
106 + expect(Scheduler).toHaveYielded(['Parent: 1, 1']);
107
108 // This time, one of the state updates but the other one doesn't. So we
109 // can't bail out.
@@ -112,7 +112,7 @@ describe('ReactHooks', () => {
112 setCounter2(2);
113 });
114
115 - expect(Scheduler).toFlushAndYield([
115 + expect(Scheduler).toHaveYielded([
116 'Parent: 1, 2',
117 'Child: 1, 2',
118 'Effect: 1, 2',
@@ -130,14 +130,15 @@ describe('ReactHooks', () => {
130
131 // Because the final values are the same as the current values, the
132 // component bails out.
133 - expect(Scheduler).toFlushAndYield(['Parent: 1, 2']);
133 + expect(Scheduler).toHaveYielded(['Parent: 1, 2']);
134
135 // prepare to check SameValue
136 act(() => {
137 setCounter1(0 / -1);
138 setCounter2(NaN);
139 });
140 - expect(Scheduler).toFlushAndYield([
140 +
141 + expect(Scheduler).toHaveYielded([
142 'Parent: 0, NaN',
143 'Child: 0, NaN',
144 'Effect: 0, NaN',
@@ -151,13 +152,13 @@ describe('ReactHooks', () => {
152 setCounter2(NaN);
153 });
154
154 - expect(Scheduler).toFlushAndYield(['Parent: 0, NaN']);
155 + expect(Scheduler).toHaveYielded(['Parent: 0, NaN']);
156
157 // check if changing negative 0 to positive 0 does not bail out
158 act(() => {
159 setCounter1(0);
160 });
160 - expect(Scheduler).toFlushAndYield([
161 + expect(Scheduler).toHaveYielded([
162 'Parent: 0, NaN',
163 'Child: 0, NaN',
164 'Effect: 0, NaN',
@@ -201,14 +202,14 @@ describe('ReactHooks', () => {
202 setCounter2(1);
203 });
204
204 - expect(Scheduler).toFlushAndYield([
205 + expect(Scheduler).toHaveYielded([
206 'Parent: 1, 1 (light)',
207 'Child: 1, 1 (light)',
208 ]);
209
210 // Update that bails out.
211 act(() => setCounter1(1));
211 - expect(Scheduler).toFlushAndYield(['Parent: 1, 1 (light)']);
212 + expect(Scheduler).toHaveYielded(['Parent: 1, 1 (light)']);
213
214 // This time, one of the state updates but the other one doesn't. So we
215 // can't bail out.
@@ -217,7 +218,7 @@ describe('ReactHooks', () => {
218 setCounter2(2);
219 });
220
220 - expect(Scheduler).toFlushAndYield([
221 + expect(Scheduler).toHaveYielded([
222 'Parent: 1, 2 (light)',
223 'Child: 1, 2 (light)',
224 ]);
@@ -227,10 +228,10 @@ describe('ReactHooks', () => {
228 act(() => {
229 setCounter1(1);
230 setCounter2(2);
231 + root.update(<Parent theme="dark" />);
232 });
233
232 - root.update(<Parent theme="dark" />);
233 - expect(Scheduler).toFlushAndYield([
234 + expect(Scheduler).toHaveYielded([
235 'Parent: 1, 2 (dark)',
236 'Child: 1, 2 (dark)',
237 ]);
@@ -239,10 +240,10 @@ describe('ReactHooks', () => {
240 act(() => {
241 setCounter1(1);
242 setCounter2(2);
243 + root.update(<Parent theme="dark" />);
244 });
245
244 - root.update(<Parent theme="dark" />);
245 - expect(Scheduler).toFlushAndYield(['Parent: 1, 2 (dark)']);
246 + expect(Scheduler).toHaveYielded(['Parent: 1, 2 (dark)']);
247 });
248
249 it('warns about setState second argument', () => {
@@ -275,7 +276,7 @@ describe('ReactHooks', () => {
276 'declare it in the component body with useEffect().',
277 {withoutStack: true},
278 );
278 - expect(Scheduler).toFlushAndYield(['Count: 1']);
279 + expect(Scheduler).toHaveYielded(['Count: 1']);
280 expect(root).toMatchRenderedOutput('1');
281 });
282
@@ -309,7 +310,7 @@ describe('ReactHooks', () => {
310 'declare it in the component body with useEffect().',
311 {withoutStack: true},
312 );
312 - expect(Scheduler).toFlushAndYield(['Count: 1']);
313 + expect(Scheduler).toHaveYielded(['Count: 1']);
314 expect(root).toMatchRenderedOutput('1');
315 });
316
@@ -347,14 +348,16 @@ describe('ReactHooks', () => {
348 });
349 return <Child text={text} />;
350 }
350 -
351 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
352 - root.update(
353 - <ThemeProvider>
354 - <Parent />
355 - </ThemeProvider>,
356 - );
357 - expect(Scheduler).toFlushAndYield([
352 + act(() => {
353 + root.update(
354 + <ThemeProvider>
355 + <Parent />
356 + </ThemeProvider>,
357 + );
358 + });
359 +
360 + expect(Scheduler).toHaveYielded([
361 'Theme: light',
362 'Parent: 0 (light)',
363 'Child: 0 (light)',
@@ -370,7 +373,7 @@ describe('ReactHooks', () => {
373
374 // Normal update
375 act(() => setCounter(1));
373 - expect(Scheduler).toFlushAndYield([
376 + expect(Scheduler).toHaveYielded([
377 'Parent: 1 (light)',
378 'Child: 1 (light)',
379 'Effect: 1 (light)',
@@ -379,7 +382,7 @@ describe('ReactHooks', () => {
382
383 // Update that doesn't change state, so it bails out
384 act(() => setCounter(1));
382 - expect(Scheduler).toFlushAndYield(['Parent: 1 (light)']);
385 + expect(Scheduler).toHaveYielded(['Parent: 1 (light)']);
386 expect(root).toMatchRenderedOutput('1 (light)');
387
388 // Update that doesn't change state, but the context changes, too, so it
@@ -389,7 +392,7 @@ describe('ReactHooks', () => {
392 setTheme('dark');
393 });
394
392 - expect(Scheduler).toFlushAndYield([
395 + expect(Scheduler).toHaveYielded([
396 'Theme: dark',
397 'Parent: 1 (dark)',
398 'Child: 1 (dark)',
@@ -424,7 +427,7 @@ describe('ReactHooks', () => {
427
428 // Normal update
429 act(() => setCounter(1));
427 - expect(Scheduler).toFlushAndYield(['Parent: 1', 'Child: 1', 'Effect: 1']);
430 + expect(Scheduler).toHaveYielded(['Parent: 1', 'Child: 1', 'Effect: 1']);
431 expect(root).toMatchRenderedOutput('1');
432
433 // Update to the same state. React doesn't know if the queue is empty
@@ -432,7 +435,7 @@ describe('ReactHooks', () => {
435 // enter the render phase before we can bail out. But we bail out before
436 // rendering the child, and we don't fire any effects.
437 act(() => setCounter(1));
435 - expect(Scheduler).toFlushAndYield(['Parent: 1']);
438 + expect(Scheduler).toHaveYielded(['Parent: 1']);
439 expect(root).toMatchRenderedOutput('1');
440
441 // Update to the same state again. This times, neither fiber has pending
@@ -443,14 +446,14 @@ describe('ReactHooks', () => {
446
447 // This changes the state to something different so it renders normally.
448 act(() => setCounter(2));
446 - expect(Scheduler).toFlushAndYield(['Parent: 2', 'Child: 2', 'Effect: 2']);
449 + expect(Scheduler).toHaveYielded(['Parent: 2', 'Child: 2', 'Effect: 2']);
450 expect(root).toMatchRenderedOutput('2');
451
452 // prepare to check SameValue
453 act(() => {
454 setCounter(0);
455 });
453 - expect(Scheduler).toFlushAndYield(['Parent: 0', 'Child: 0', 'Effect: 0']);
456 + expect(Scheduler).toHaveYielded(['Parent: 0', 'Child: 0', 'Effect: 0']);
457 expect(root).toMatchRenderedOutput('0');
458
459 // Update to the same state for the first time to flush the queue
@@ -458,7 +461,7 @@ describe('ReactHooks', () => {
461 setCounter(0);
462 });
463
461 - expect(Scheduler).toFlushAndYield(['Parent: 0']);
464 + expect(Scheduler).toHaveYielded(['Parent: 0']);
465 expect(root).toMatchRenderedOutput('0');
466
467 // Update again to the same state. Should bail out.
@@ -472,7 +475,7 @@ describe('ReactHooks', () => {
475 act(() => {
476 setCounter(0 / -1);
477 });
475 - expect(Scheduler).toFlushAndYield(['Parent: 0', 'Child: 0', 'Effect: 0']);
478 + expect(Scheduler).toHaveYielded(['Parent: 0', 'Child: 0', 'Effect: 0']);
479 expect(root).toMatchRenderedOutput('0');
480 });
481
@@ -503,7 +506,7 @@ describe('ReactHooks', () => {
506 return value;
507 });
508 };
506 - act(() => {
509 + ReactTestRenderer.unstable_batchedUpdates(() => {
510 update(0);
511 update(0);
512 update(0);
@@ -564,7 +567,7 @@ describe('ReactHooks', () => {
567 };
568
569 // Update at normal priority
567 - act(() => update(n => n * 100));
570 + ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100));
571
572 // The new state is eagerly computed.
573 expect(Scheduler).toHaveYielded(['Compute state (1 -> 100)']);
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+33 -27
@@ -80,7 +80,7 @@ describe('ReactHooksWithNoopRenderer', () => {
80 expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
81
82 // Schedule some updates
83 - act(() => {
83 + ReactNoop.batchedUpdates(() => {
84 counter.current.updateCount(1);
85 counter.current.updateCount(count => count + 10);
86 });
@@ -189,11 +189,11 @@ describe('ReactHooksWithNoopRenderer', () => {
189 expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
190
191 act(() => counter.current.updateCount(1));
192 - expect(Scheduler).toFlushAndYield(['Count: 1']);
192 + expect(Scheduler).toHaveYielded(['Count: 1']);
193 expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
194
195 act(() => counter.current.updateCount(count => count + 10));
196 - expect(Scheduler).toFlushAndYield(['Count: 11']);
196 + expect(Scheduler).toHaveYielded(['Count: 11']);
197 expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
198 });
199
@@ -213,7 +213,7 @@ describe('ReactHooksWithNoopRenderer', () => {
213 expect(ReactNoop.getChildren()).toEqual([span('Count: 42')]);
214
215 act(() => counter.current.updateCount(7));
216 - expect(Scheduler).toFlushAndYield(['Count: 7']);
216 + expect(Scheduler).toHaveYielded(['Count: 7']);
217 expect(ReactNoop.getChildren()).toEqual([span('Count: 7')]);
218 });
219
@@ -231,10 +231,10 @@ describe('ReactHooksWithNoopRenderer', () => {
231 expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
232
233 act(() => counter.current.updateCount(7));
234 - expect(Scheduler).toFlushAndYield(['Count: 7']);
234 + expect(Scheduler).toHaveYielded(['Count: 7']);
235
236 act(() => counter.current.updateLabel('Total'));
237 - expect(Scheduler).toFlushAndYield(['Total: 7']);
237 + expect(Scheduler).toHaveYielded(['Total: 7']);
238 });
239
240 it('returns the same updater function every time', () => {
@@ -249,11 +249,11 @@ describe('ReactHooksWithNoopRenderer', () => {
249 expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
250
251 act(() => updaters[0](1));
252 - expect(Scheduler).toFlushAndYield(['Count: 1']);
252 + expect(Scheduler).toHaveYielded(['Count: 1']);
253 expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
254
255 act(() => updaters[0](count => count + 10));
256 - expect(Scheduler).toFlushAndYield(['Count: 11']);
256 + expect(Scheduler).toHaveYielded(['Count: 11']);
257 expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
258
259 expect(updaters).toEqual([updaters[0], updaters[0], updaters[0]]);
@@ -298,7 +298,7 @@ describe('ReactHooksWithNoopRenderer', () => {
298 expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
299
300 act(() => _updateCount(1));
301 - expect(Scheduler).toFlushAndYield(['Count: 1']);
301 + expect(Scheduler).toHaveYielded(['Count: 1']);
302 expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
303 });
304 });
@@ -484,7 +484,7 @@ describe('ReactHooksWithNoopRenderer', () => {
484 counter.current.dispatch('reset');
485 });
486 ReactNoop.render(<Counter ref={counter} />);
487 - expect(Scheduler).toFlushAndYield([
487 + expect(Scheduler).toHaveYielded([
488 'Render: 0',
489 'Render: 1',
490 'Render: 11',
@@ -524,7 +524,7 @@ describe('ReactHooksWithNoopRenderer', () => {
524 expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
525
526 act(() => counter.current.dispatch(INCREMENT));
527 - expect(Scheduler).toFlushAndYield(['Count: 1']);
527 + expect(Scheduler).toHaveYielded(['Count: 1']);
528 expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
529 act(() => {
530 counter.current.dispatch(DECREMENT);
@@ -532,7 +532,7 @@ describe('ReactHooksWithNoopRenderer', () => {
532 counter.current.dispatch(DECREMENT);
533 });
534
535 - expect(Scheduler).toFlushAndYield(['Count: -2']);
535 + expect(Scheduler).toHaveYielded(['Count: -2']);
536 expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]);
537 });
538
@@ -566,7 +566,7 @@ describe('ReactHooksWithNoopRenderer', () => {
566 expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]);
567
568 act(() => counter.current.dispatch(INCREMENT));
569 - expect(Scheduler).toFlushAndYield(['Count: 11']);
569 + expect(Scheduler).toHaveYielded(['Count: 11']);
570 expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
571
572 act(() => {
@@ -575,7 +575,7 @@ describe('ReactHooksWithNoopRenderer', () => {
575 counter.current.dispatch(DECREMENT);
576 });
577
578 - expect(Scheduler).toFlushAndYield(['Count: 8']);
578 + expect(Scheduler).toHaveYielded(['Count: 8']);
579 expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]);
580 });
581
@@ -600,7 +600,7 @@ describe('ReactHooksWithNoopRenderer', () => {
600 expect(Scheduler).toFlushAndYield(['Count: 0']);
601 expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
602
603 - act(() => {
603 + ReactNoop.batchedUpdates(() => {
604 counter.current.dispatch(INCREMENT);
605 counter.current.dispatch(INCREMENT);
606 counter.current.dispatch(INCREMENT);
@@ -884,8 +884,12 @@ describe('ReactHooksWithNoopRenderer', () => {
884
885 // A discrete event forces the passive effect to be flushed --
886 // updateCount(1) happens first, so 2 wins.
887 +
888 ReactNoop.interactiveUpdates(() => {
888 - act(() => _updateCount(2));
889 + // (use batchedUpdates to silence the act() warning)
890 + ReactNoop.batchedUpdates(() => {
891 + _updateCount(2);
892 + });
893 });
894 expect(Scheduler).toHaveYielded(['Will set count to 1']);
895 expect(Scheduler).toFlushAndYield(['Count: 2']);
@@ -936,7 +940,8 @@ describe('ReactHooksWithNoopRenderer', () => {
940 // A discrete event forces the passive effect to be flushed --
941 // updateCount(1) happens first, so 2 wins.
942 ReactNoop.interactiveUpdates(() => {
939 - act(() => _updateCount(2));
943 + // use batchedUpdates to silence the act warning
944 + ReactNoop.batchedUpdates(() => _updateCount(2));
945 });
946 expect(Scheduler).toHaveYielded(['Will set count to 1']);
947 expect(Scheduler).toFlushAndYield(['Count: 2']);
@@ -1527,7 +1532,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1532 ]);
1533
1534 act(button.current.increment);
1530 - expect(Scheduler).toFlushAndYield([
1535 + expect(Scheduler).toHaveYielded([
1536 // Button should not re-render, because its props haven't changed
1537 // 'Increment',
1538 'Count: 1',
@@ -1551,7 +1556,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1556
1557 // Callback should have updated
1558 act(button.current.increment);
1554 - expect(Scheduler).toFlushAndYield(['Count: 11']);
1559 + expect(Scheduler).toHaveYielded(['Count: 11']);
1560 expect(ReactNoop.getChildren()).toEqual([
1561 span('Increment'),
1562 span('Count: 11'),
@@ -1754,7 +1759,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1759 act(() => {
1760 counter.current.dispatch(INCREMENT);
1761 });
1757 - expect(Scheduler).toFlushAndYield(['Count: 1']);
1762 + expect(Scheduler).toHaveYielded(['Count: 1']);
1763 expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1764 // Intentionally not updated because of [] deps:
1765 expect(counter.current.count).toBe(0);
@@ -1784,7 +1789,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1789 act(() => {
1790 counter.current.dispatch(INCREMENT);
1791 });
1787 - expect(Scheduler).toFlushAndYield(['Count: 1']);
1792 + expect(Scheduler).toHaveYielded(['Count: 1']);
1793 expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1794 expect(counter.current.count).toBe(1);
1795 });
@@ -1821,7 +1826,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1826 act(() => {
1827 counter.current.dispatch(INCREMENT);
1828 });
1824 - expect(Scheduler).toFlushAndYield(['Count: 1']);
1829 + expect(Scheduler).toHaveYielded(['Count: 1']);
1830 expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1831 expect(counter.current.count).toBe(1);
1832 expect(totalRefUpdates).toBe(2);
@@ -1868,7 +1873,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1873 updateB(3);
1874 });
1875
1871 - expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: [not loaded]']);
1876 + expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: [not loaded]']);
1877 expect(ReactNoop.getChildren()).toEqual([
1878 span('A: 2, B: 3, C: [not loaded]'),
1879 ]);
@@ -1929,7 +1934,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1934 updateB(3);
1935 updateC(4);
1936 });
1932 - expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 4']);
1937 + expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: 4']);
1938 expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]);
1939 ReactNoop.render(<App loadC={false} />);
1940 expect(Scheduler).toFlushAndThrow(
@@ -2035,7 +2040,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2040 act(() => {
2041 setCounter(2);
2042 });
2038 - expect(Scheduler).toFlushAndYield([
2043 + expect(Scheduler).toHaveYielded([
2044 'Render: 1',
2045 'Effect: 2',
2046 'Reducer: 2',
@@ -2074,7 +2079,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2079 expect(ReactNoop).toMatchRenderedOutput('0');
2080
2081 act(() => dispatch());
2077 - expect(Scheduler).toFlushAndYield(['Step: 5, Shadow: 5']);
2082 + expect(Scheduler).toHaveYielded(['Step: 5, Shadow: 5']);
2083 expect(ReactNoop).toMatchRenderedOutput('5');
2084 });
2085
@@ -2113,7 +2118,8 @@ describe('ReactHooksWithNoopRenderer', () => {
2118
2119 // Enqueuing this update forces the passive effect to be flushed --
2120 // updateCount(1) happens first, so 2 wins.
2116 - act(() => _updateCount(2));
2121 + // (use batchedUpdates to silence the act() warning)
2122 + ReactNoop.batchedUpdates(() => _updateCount(2));
2123 expect(Scheduler).toHaveYielded(['Will set count to 1']);
2124 expect(Scheduler).toFlushAndYield(['Count: 2']);
2125 expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.internal.js
+3 -3
@@ -104,14 +104,14 @@ describe('ReactIncrementalScheduling', () => {
104 ReactNoop.renderToRootWithID(<Text text="b:1" />, 'b');
105 ReactNoop.renderToRootWithID(<Text text="c:1" />, 'c');
106 });
107 - expect(Scheduler).toFlushAndYield(['a:1', 'b:1', 'c:1']);
107 + expect(Scheduler).toHaveYielded(['a:1', 'b:1', 'c:1']);
108
109 expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
110 expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
111 expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:1');
112
113 // Schedule deferred work in the reverse order
114 - ReactNoop.act(() => {
114 + ReactNoop.batchedUpdates(() => {
115 ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
116 ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
117 });
@@ -122,7 +122,7 @@ describe('ReactIncrementalScheduling', () => {
122 expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
123 expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
124 // Schedule last bit of work, it will get processed the last
125 - ReactNoop.act(() => {
125 + ReactNoop.batchedUpdates(() => {
126 ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
127 });
128 // Keep performing work in the order it was scheduled
packages/react-reconciler/src/__tests__/ReactNoopRendererAct-test.js
+3 -6
@@ -42,6 +42,7 @@ describe('ReactNoop.act()', () => {
42 Scheduler.yieldValue('stage 1');
43 await null;
44 Scheduler.yieldValue('stage 2');
45 + await null;
46 setCtr(1);
47 }
48 React.useEffect(() => {
@@ -50,13 +51,9 @@ describe('ReactNoop.act()', () => {
51 return ctr;
52 }
53 await ReactNoop.act(async () => {
53 - ReactNoop.act(() => {
54 - ReactNoop.render(<App />);
55 - });
56 - await null;
57 - expect(Scheduler).toFlushAndYield(['stage 1']);
54 + ReactNoop.render(<App />);
55 });
59 - expect(Scheduler).toHaveYielded(['stage 2']);
56 + expect(Scheduler).toHaveYielded(['stage 1', 'stage 2']);
57 expect(Scheduler).toFlushWithoutYielding();
58 expect(ReactNoop.getChildren()).toEqual([{text: '1', hidden: false}]);
59 });
packages/react-test-renderer/src/ReactTestRendererAct.js
+33 -9
@@ -14,23 +14,42 @@ import {
14 } from 'react-reconciler/inline.test';
15 import ReactSharedInternals from 'shared/ReactSharedInternals';
16 import warningWithoutStack from 'shared/warningWithoutStack';
17 +import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags';
18 import enqueueTask from 'shared/enqueueTask';
19 +import * as Scheduler from 'scheduler';
20
21 const {ReactShouldWarnActingUpdates} = ReactSharedInternals;
22
23 // this implementation should be exactly the same in
24 // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
25
24 -// we track the 'depth' of the act() calls with this counter,
25 -// so we can tell if any async act() calls try to run in parallel.
26 -let actingUpdatesScopeDepth = 0;
26 +let hasWarnedAboutMissingMockScheduler = false;
27 +const flushWork =
28 + Scheduler.unstable_flushWithoutYielding ||
29 + function() {
30 + if (warnAboutMissingMockScheduler === true) {
31 + if (hasWarnedAboutMissingMockScheduler === false) {
32 + warningWithoutStack(
33 + null,
34 + 'Starting from React v17, the "scheduler" module will need to be mocked ' +
35 + 'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' +
36 + "to the top of your tests, or in your framework's global config file -\n\n" +
37 + 'As an example, for jest - \n' +
38 + "jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" +
39 + 'For more info, visit https://fb.me/react-mock-scheduler',
40 + );
41 + hasWarnedAboutMissingMockScheduler = true;
42 + }
43 + }
44 + while (flushPassiveEffects()) {}
45 + };
46
28 -function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
47 +function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
48 try {
30 - flushPassiveEffects();
49 + flushWork();
50 enqueueTask(() => {
32 - if (flushPassiveEffects()) {
33 - flushEffectsAndMicroTasks(onDone);
51 + if (flushWork()) {
52 + flushWorkAndMicroTasks(onDone);
53 } else {
54 onDone();
55 }
@@ -40,6 +59,11 @@ function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
59 }
60 }
61
62 +// we track the 'depth' of the act() calls with this counter,
63 +// so we can tell if any async act() calls try to run in parallel.
64 +
65 +let actingUpdatesScopeDepth = 0;
66 +
67 function act(callback: () => Thenable) {
68 let previousActingUpdatesScopeDepth;
69 if (__DEV__) {
@@ -100,7 +124,7 @@ function act(callback: () => Thenable) {
124 called = true;
125 result.then(
126 () => {
103 - flushEffectsAndMicroTasks((err: ?Error) => {
127 + flushWorkAndMicroTasks((err: ?Error) => {
128 onDone();
129 if (err) {
130 reject(err);
@@ -128,7 +152,7 @@ function act(callback: () => Thenable) {
152
153 // flush effects until none remain, and cleanup
154 try {
131 - while (flushPassiveEffects()) {}
155 + flushWork();
156 onDone();
157 } catch (err) {
158 onDone();
packages/scheduler/src/forks/SchedulerHostConfig.mock.js
+5 -1
@@ -103,12 +103,15 @@ export function unstable_flushExpired() {
103 }
104 }
105
106 -export function unstable_flushWithoutYielding(): void {
106 +export function unstable_flushWithoutYielding(): boolean {
107 if (isFlushing) {
108 throw new Error('Already flushing work.');
109 }
110 isFlushing = true;
111 try {
112 + if (scheduledCallback === null) {
113 + return false;
114 + }
115 while (scheduledCallback !== null) {
116 const cb = scheduledCallback;
117 scheduledCallback = null;
@@ -117,6 +120,7 @@ export function unstable_flushWithoutYielding(): void {
120 scheduledCallbackExpiration <= currentTime;
121 cb(didTimeout);
122 }
123 + return true;
124 } finally {
125 expectedNumberOfYields = -1;
126 didStop = false;
packages/shared/ReactFeatureFlags.js
+3
@@ -68,5 +68,8 @@ export const enableEventAPI = false;
68 // New API for JSX transforms to target - https://github.com/reactjs/rfcs/pull/107
69 export const enableJSXTransformAPI = false;
70
71 +// We will enforce mocking scheduler with scheduler/unstable_mock at some point. (v17?)
72 +// Till then, we warn about the missing mock, but still fallback to a sync mode compatible version
73 +export const warnAboutMissingMockScheduler = false;
74 // Temporary flag to revert the fix in #15650
75 export const revertPassiveEffectsChange = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -32,6 +32,7 @@ export const warnAboutDeprecatedLifecycles = true;
32 export const warnAboutDeprecatedSetNativeProps = true;
33 export const enableEventAPI = false;
34 export const enableJSXTransformAPI = false;
35 +export const warnAboutMissingMockScheduler = true;
36 export const revertPassiveEffectsChange = false;
37
38 // Only used in www builds.
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -29,6 +29,7 @@ export const enableSchedulerDebugging = false;
29 export const warnAboutDeprecatedSetNativeProps = false;
30 export const enableEventAPI = false;
31 export const enableJSXTransformAPI = false;
32 +export const warnAboutMissingMockScheduler = false;
33 export const revertPassiveEffectsChange = false;
34
35 // Only used in www builds.
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -29,6 +29,7 @@ export const enableSchedulerDebugging = false;
29 export const warnAboutDeprecatedSetNativeProps = false;
30 export const enableEventAPI = false;
31 export const enableJSXTransformAPI = false;
32 +export const warnAboutMissingMockScheduler = true;
33 export const revertPassiveEffectsChange = false;
34
35 // Only used in www builds.
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -29,6 +29,7 @@ export const enableSchedulerDebugging = false;
29 export const warnAboutDeprecatedSetNativeProps = false;
30 export const enableEventAPI = false;
31 export const enableJSXTransformAPI = false;
32 +export const warnAboutMissingMockScheduler = false;
33 export const revertPassiveEffectsChange = false;
34
35 // Only used in www builds.
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -30,6 +30,7 @@ export const disableJavaScriptURLs = false;
30 export const disableYielding = false;
31 export const enableEventAPI = true;
32 export const enableJSXTransformAPI = true;
33 +export const warnAboutMissingMockScheduler = true;
34
35 // Only used in www builds.
36 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -72,6 +72,8 @@ export const enableEventAPI = true;
72
73 export const enableJSXTransformAPI = true;
74
75 +export const warnAboutMissingMockScheduler = true;
76 +
77 // Flow magic to verify the exports of this file match the original version.
78 // eslint-disable-next-line no-unused-vars
79 type Check<_X, Y: _X, X: Y = _X> = null;
packages/shared/forks/Scheduler.umd.js
+5
@@ -28,6 +28,10 @@ const {
28 unstable_LowPriority,
29 unstable_IdlePriority,
30 unstable_forceFrameRate,
31 +
32 + // this doesn't actually exist on the scheduler, but it *does*
33 + // on scheduler/unstable_mock, which we'll need inside act().
34 + unstable_flushWithoutYielding,
35 } = ReactInternals.Scheduler;
36
37 export {
@@ -47,4 +51,5 @@ export {
51 unstable_LowPriority,
52 unstable_IdlePriority,
53 unstable_forceFrameRate,
54 + unstable_flushWithoutYielding,
55 };
scripts/rollup/bundles.js
+8 -1
@@ -418,7 +418,14 @@ const bundles = [
418
419 /******* React Scheduler Mock (experimental) *******/
420 {
421 - bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
421 + bundleTypes: [
422 + UMD_DEV,
423 + UMD_PROD,
424 + NODE_DEV,
425 + NODE_PROD,
426 + FB_WWW_DEV,
427 + FB_WWW_PROD,
428 + ],
429 moduleType: ISOMORPHIC,
430 entry: 'scheduler/unstable_mock',
431 global: 'SchedulerMock',