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

flush work on exiting outermost act(), with nested act()s from different renderers (#16181)

Given this snippet: ```jsx TestRenderer.act(() => { TestUtils.act(() => { TestRenderer.create(<Effecty />); }); }); ``` We want to make sure that all work is only flushed on exiting the outermost act(). Now, naively doing this based on actingScopeDepth would work with a mocked scheduler, where flushAll() would flush all work across renderers. This doesn't work without mocking the scheduler though; and where flushing work only works per renderer. So we disable this behaviour for a non-mocked scenario. This seems like an ok tradeoff.

Sunil Pai committed Jul 24, 2019 at 00:20 UTC c73e1f236f932c76fbed5be87bf0fd91da6c0549
4 files changed +237 -156
fixtures/dom/src/index.test.js
+182 -126
@@ -7,15 +7,14 @@
7 * @emails react-core
8 */
9
10 -import React from 'react';
11 -import ReactDOM from 'react-dom';
12 -import ReactART from 'react-art';
13 -import ARTSVGMode from 'art/modes/svg';
14 -import ARTCurrentMode from 'art/modes/current';
15 -import TestUtils from 'react-dom/test-utils';
16 -import TestRenderer from 'react-test-renderer';
17 -
18 -ARTCurrentMode.setCurrent(ARTSVGMode);
10 +let React;
11 +let ReactDOM;
12 +let ReactART;
13 +let ARTSVGMode;
14 +let ARTCurrentMode;
15 +let TestUtils;
16 +let TestRenderer;
17 +let ARTTest;
18
19 global.__DEV__ = process.env.NODE_ENV !== 'production';
20
@@ -25,153 +24,210 @@ function App(props) {
24 return 'hello world';
25 }
26
28 -it("doesn't warn when you use the right act + renderer: dom", () => {
29 - TestUtils.act(() => {
30 - TestUtils.renderIntoDocument(<App />);
31 - });
27 +describe('legacy mode', () => {
28 + runTests();
29 });
30
34 -it("doesn't warn when you use the right act + renderer: test", () => {
35 - TestRenderer.act(() => {
36 - TestRenderer.create(<App />);
31 +describe('mocked scheduler', () => {
32 + beforeEach(() => {
33 + jest.mock('scheduler', () =>
34 + require.requireActual('scheduler/unstable_mock')
35 + );
36 });
37 + afterEach(() => {
38 + jest.unmock('scheduler');
39 + });
40 + runTests();
41 });
42
40 -it('resets correctly across renderers', () => {
41 - function Effecty() {
42 - React.useEffect(() => {}, []);
43 - return null;
44 - }
45 - TestUtils.act(() => {
46 - TestRenderer.act(() => {});
47 - expect(() => {
48 - TestRenderer.create(<Effecty />);
49 - }).toWarnDev(["It looks like you're using the wrong act()"], {
50 - withoutStack: true,
43 +function runTests() {
44 + beforeEach(() => {
45 + jest.resetModules();
46 + React = require('react');
47 + ReactDOM = require('react-dom');
48 + ReactART = require('react-art');
49 + ARTSVGMode = require('art/modes/svg');
50 + ARTCurrentMode = require('art/modes/current');
51 + TestUtils = require('react-dom/test-utils');
52 + TestRenderer = require('react-test-renderer');
53 +
54 + ARTCurrentMode.setCurrent(ARTSVGMode);
55 +
56 + ARTTest = function ARTTest(props) {
57 + return (
58 + <ReactART.Surface width={150} height={200}>
59 + <ReactART.Group>
60 + <ReactART.Shape
61 + d="M0,0l50,0l0,50l-50,0z"
62 + fill={new ReactART.LinearGradient(['black', 'white'])}
63 + key="a"
64 + width={50}
65 + height={50}
66 + x={50}
67 + y={50}
68 + opacity={0.1}
69 + />
70 + <ReactART.Shape
71 + fill="#3C5A99"
72 + key="b"
73 + scale={0.5}
74 + x={50}
75 + y={50}
76 + title="This is an F"
77 + cursor="pointer">
78 + M64.564,38.583H54l0.008-5.834c0-3.035,0.293-4.666,4.657-4.666
79 + h5.833V16.429h-9.33c-11.213,0-15.159,5.654-15.159,15.16v6.994
80 + h-6.99v11.652h6.99v33.815H54V50.235h9.331L64.564,38.583z
81 + </ReactART.Shape>
82 + </ReactART.Group>
83 + </ReactART.Surface>
84 + );
85 + };
86 + });
87 + it("doesn't warn when you use the right act + renderer: dom", () => {
88 + TestUtils.act(() => {
89 + TestUtils.renderIntoDocument(<App />);
90 });
91 });
53 -});
92
55 -it('warns when using createRoot() + .render', () => {
56 - const root = ReactDOM.unstable_createRoot(document.createElement('div'));
57 - expect(() => {
93 + it("doesn't warn when you use the right act + renderer: test", () => {
94 TestRenderer.act(() => {
59 - root.render(<App />);
95 + TestRenderer.create(<App />);
96 });
61 - }).toWarnDev(["It looks like you're using the wrong act()"], {
62 - withoutStack: true,
97 });
64 -});
98
66 -it('warns when using the wrong act version - test + dom: render', () => {
67 - expect(() => {
68 - TestRenderer.act(() => {
69 - TestUtils.renderIntoDocument(<App />);
99 + it('resets correctly across renderers', () => {
100 + function Effecty() {
101 + React.useEffect(() => {}, []);
102 + return null;
103 + }
104 + TestUtils.act(() => {
105 + TestRenderer.act(() => {});
106 + expect(() => {
107 + TestRenderer.create(<Effecty />);
108 + }).toWarnDev(["It looks like you're using the wrong act()"], {
109 + withoutStack: true,
110 + });
111 });
71 - }).toWarnDev(["It looks like you're using the wrong act()"], {
72 - withoutStack: true,
112 });
74 -});
113
76 -it('warns when using the wrong act version - test + dom: updates', () => {
77 - let setCtr;
78 - function Counter(props) {
79 - const [ctr, _setCtr] = React.useState(0);
80 - setCtr = _setCtr;
81 - return ctr;
82 - }
83 - TestUtils.renderIntoDocument(<Counter />);
84 - expect(() => {
85 - TestRenderer.act(() => {
86 - setCtr(1);
114 + it('warns when using createRoot() + .render', () => {
115 + const root = ReactDOM.unstable_createRoot(document.createElement('div'));
116 + expect(() => {
117 + TestRenderer.act(() => {
118 + root.render(<App />);
119 + });
120 + }).toWarnDev(["It looks like you're using the wrong act()"], {
121 + withoutStack: true,
122 });
88 - }).toWarnDev(["It looks like you're using the wrong act()"]);
89 -});
123 + });
124
91 -it('warns when using the wrong act version - dom + test: .create()', () => {
92 - expect(() => {
93 - TestUtils.act(() => {
94 - TestRenderer.create(<App />);
125 + it('warns when using the wrong act version - test + dom: render', () => {
126 + expect(() => {
127 + TestRenderer.act(() => {
128 + TestUtils.renderIntoDocument(<App />);
129 + });
130 + }).toWarnDev(["It looks like you're using the wrong act()"], {
131 + withoutStack: true,
132 });
96 - }).toWarnDev(["It looks like you're using the wrong act()"], {
97 - withoutStack: true,
133 });
99 -});
134
101 -it('warns when using the wrong act version - dom + test: .update()', () => {
102 - const root = TestRenderer.create(<App key="one" />);
103 - expect(() => {
104 - TestUtils.act(() => {
105 - root.update(<App key="two" />);
135 + it('warns when using the wrong act version - test + dom: updates', () => {
136 + let setCtr;
137 + function Counter(props) {
138 + const [ctr, _setCtr] = React.useState(0);
139 + setCtr = _setCtr;
140 + return ctr;
141 + }
142 + TestUtils.renderIntoDocument(<Counter />);
143 + expect(() => {
144 + TestRenderer.act(() => {
145 + setCtr(1);
146 + });
147 + }).toWarnDev(["It looks like you're using the wrong act()"]);
148 + });
149 +
150 + it('warns when using the wrong act version - dom + test: .create()', () => {
151 + expect(() => {
152 + TestUtils.act(() => {
153 + TestRenderer.create(<App />);
154 + });
155 + }).toWarnDev(["It looks like you're using the wrong act()"], {
156 + withoutStack: true,
157 });
107 - }).toWarnDev(["It looks like you're using the wrong act()"], {
108 - withoutStack: true,
158 });
110 -});
159
112 -it('warns when using the wrong act version - dom + test: updates', () => {
113 - let setCtr;
114 - function Counter(props) {
115 - const [ctr, _setCtr] = React.useState(0);
116 - setCtr = _setCtr;
117 - return ctr;
118 - }
119 - const root = TestRenderer.create(<Counter />);
120 - expect(() => {
121 - TestUtils.act(() => {
122 - setCtr(1);
160 + it('warns when using the wrong act version - dom + test: .update()', () => {
161 + const root = TestRenderer.create(<App key="one" />);
162 + expect(() => {
163 + TestUtils.act(() => {
164 + root.update(<App key="two" />);
165 + });
166 + }).toWarnDev(["It looks like you're using the wrong act()"], {
167 + withoutStack: true,
168 });
124 - }).toWarnDev(["It looks like you're using the wrong act()"]);
125 -});
169 + });
170
127 -const {Surface, Group, Shape} = ReactART;
128 -function ARTTest(props) {
129 - return (
130 - <Surface width={150} height={200}>
131 - <Group>
132 - <Shape
133 - d="M0,0l50,0l0,50l-50,0z"
134 - fill={new ReactART.LinearGradient(['black', 'white'])}
135 - key="a"
136 - width={50}
137 - height={50}
138 - x={50}
139 - y={50}
140 - opacity={0.1}
141 - />
142 - <Shape
143 - fill="#3C5A99"
144 - key="b"
145 - scale={0.5}
146 - x={50}
147 - y={50}
148 - title="This is an F"
149 - cursor="pointer">
150 - M64.564,38.583H54l0.008-5.834c0-3.035,0.293-4.666,4.657-4.666
151 - h5.833V16.429h-9.33c-11.213,0-15.159,5.654-15.159,15.16v6.994
152 - h-6.99v11.652h6.99v33.815H54V50.235h9.331L64.564,38.583z
153 - </Shape>
154 - </Group>
155 - </Surface>
156 - );
157 -}
171 + it('warns when using the wrong act version - dom + test: updates', () => {
172 + let setCtr;
173 + function Counter(props) {
174 + const [ctr, _setCtr] = React.useState(0);
175 + setCtr = _setCtr;
176 + return ctr;
177 + }
178 + const root = TestRenderer.create(<Counter />);
179 + expect(() => {
180 + TestUtils.act(() => {
181 + setCtr(1);
182 + });
183 + }).toWarnDev(["It looks like you're using the wrong act()"]);
184 + });
185 +
186 + it('does not warn when nesting react-act inside react-dom', () => {
187 + TestUtils.act(() => {
188 + TestUtils.renderIntoDocument(<ARTTest />);
189 + });
190 + });
191
159 -it('does not warn when nesting react-act inside react-dom', () => {
160 - TestUtils.act(() => {
161 - TestUtils.renderIntoDocument(<ARTTest />);
192 + it('does not warn when nesting react-act inside react-test-renderer', () => {
193 + TestRenderer.act(() => {
194 + TestRenderer.create(<ARTTest />);
195 + });
196 });
163 -});
197
165 -it('does not warn when nesting react-act inside react-test-renderer', () => {
166 - TestRenderer.act(() => {
167 - TestRenderer.create(<ARTTest />);
198 + it("doesn't warn if you use nested acts from different renderers", () => {
199 + TestRenderer.act(() => {
200 + TestUtils.act(() => {
201 + TestRenderer.create(<App />);
202 + });
203 + });
204 });
169 -});
205
171 -it("doesn't warn if you use nested acts from different renderers", () => {
172 - TestRenderer.act(() => {
206 + it('flushes work only outside the outermost act(), even when nested from different renderers', () => {
207 + const log = [];
208 + function Effecty() {
209 + React.useEffect(() => {
210 + log.push('called');
211 + }, []);
212 + return null;
213 + }
214 + // in legacy mode, this tests whether an act only flushes its own effects
215 + // with a mocked scheduler, this tests whether it flushes all work only on the outermost act
216 + TestRenderer.act(() => {
217 + TestUtils.act(() => {
218 + TestRenderer.create(<Effecty />);
219 + });
220 + expect(log).toEqual([]);
221 + });
222 + expect(log).toEqual(['called']);
223 +
224 + log.splice(0);
225 + // for doublechecking, we flip it inside out, and assert on the outermost
226 TestUtils.act(() => {
174 - TestRenderer.create(<App />);
227 + TestRenderer.act(() => {
228 + TestRenderer.create(<Effecty />);
229 + });
230 });
231 + expect(log).toEqual(['called']);
232 });
177 -});
233 +}
packages/react-dom/src/test-utils/ReactTestUtilsAct.js
+18 -10
@@ -44,6 +44,8 @@ const {IsSomeRendererActing} = ReactSharedInternals;
44 // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
45
46 let hasWarnedAboutMissingMockScheduler = false;
47 +const isSchedulerMocked =
48 + typeof Scheduler.unstable_flushAllWithoutAsserting === 'function';
49 const flushWork =
50 Scheduler.unstable_flushAllWithoutAsserting ||
51 function() {
@@ -89,18 +91,17 @@ function act(callback: () => Thenable) {
91 let previousIsSomeRendererActing;
92 let previousIsThisRendererActing;
93 actingUpdatesScopeDepth++;
92 - if (__DEV__) {
93 - previousIsSomeRendererActing = IsSomeRendererActing.current;
94 - previousIsThisRendererActing = IsThisRendererActing.current;
95 - IsSomeRendererActing.current = true;
96 - IsThisRendererActing.current = true;
97 - }
94 +
95 + previousIsSomeRendererActing = IsSomeRendererActing.current;
96 + previousIsThisRendererActing = IsThisRendererActing.current;
97 + IsSomeRendererActing.current = true;
98 + IsThisRendererActing.current = true;
99
100 function onDone() {
101 actingUpdatesScopeDepth--;
102 + IsSomeRendererActing.current = previousIsSomeRendererActing;
103 + IsThisRendererActing.current = previousIsThisRendererActing;
104 if (__DEV__) {
102 - IsSomeRendererActing.current = previousIsSomeRendererActing;
103 - IsThisRendererActing.current = previousIsThisRendererActing;
105 if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
106 // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
107 warningWithoutStack(
@@ -155,7 +156,11 @@ function act(callback: () => Thenable) {
156 called = true;
157 result.then(
158 () => {
158 - if (actingUpdatesScopeDepth > 1) {
159 + if (
160 + actingUpdatesScopeDepth > 1 ||
161 + (isSchedulerMocked === true &&
162 + previousIsSomeRendererActing === true)
163 + ) {
164 onDone();
165 resolve();
166 return;
@@ -190,7 +195,10 @@ function act(callback: () => Thenable) {
195
196 // flush effects until none remain, and cleanup
197 try {
193 - if (actingUpdatesScopeDepth === 1) {
198 + if (
199 + actingUpdatesScopeDepth === 1 &&
200 + (isSchedulerMocked === false || previousIsSomeRendererActing === false)
201 + ) {
202 // we're about to exit the act() scope,
203 // now's the time to flush effects
204 flushWork();
packages/react-noop-renderer/src/createReactNoop.js
+19 -10
@@ -600,6 +600,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
600 // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
601
602 let hasWarnedAboutMissingMockScheduler = false;
603 + const isSchedulerMocked =
604 + typeof Scheduler.unstable_flushAllWithoutAsserting === 'function';
605 const flushWork =
606 Scheduler.unstable_flushAllWithoutAsserting ||
607 function() {
@@ -645,18 +647,17 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
647 let previousIsSomeRendererActing;
648 let previousIsThisRendererActing;
649 actingUpdatesScopeDepth++;
648 - if (__DEV__) {
649 - previousIsSomeRendererActing = IsSomeRendererActing.current;
650 - previousIsThisRendererActing = IsThisRendererActing.current;
651 - IsSomeRendererActing.current = true;
652 - IsThisRendererActing.current = true;
653 - }
650 +
651 + previousIsSomeRendererActing = IsSomeRendererActing.current;
652 + previousIsThisRendererActing = IsThisRendererActing.current;
653 + IsSomeRendererActing.current = true;
654 + IsThisRendererActing.current = true;
655
656 function onDone() {
657 actingUpdatesScopeDepth--;
658 + IsSomeRendererActing.current = previousIsSomeRendererActing;
659 + IsThisRendererActing.current = previousIsThisRendererActing;
660 if (__DEV__) {
658 - IsSomeRendererActing.current = previousIsSomeRendererActing;
659 - IsThisRendererActing.current = previousIsThisRendererActing;
661 if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
662 // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
663 warningWithoutStack(
@@ -711,7 +712,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
712 called = true;
713 result.then(
714 () => {
714 - if (actingUpdatesScopeDepth > 1) {
715 + if (
716 + actingUpdatesScopeDepth > 1 ||
717 + (isSchedulerMocked === true &&
718 + previousIsSomeRendererActing === true)
719 + ) {
720 onDone();
721 resolve();
722 return;
@@ -746,7 +751,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
751
752 // flush effects until none remain, and cleanup
753 try {
749 - if (actingUpdatesScopeDepth === 1) {
754 + if (
755 + actingUpdatesScopeDepth === 1 &&
756 + (isSchedulerMocked === false ||
757 + previousIsSomeRendererActing === false)
758 + ) {
759 // we're about to exit the act() scope,
760 // now's the time to flush effects
761 flushWork();
packages/react-test-renderer/src/ReactTestRendererAct.js
+18 -10
@@ -25,6 +25,8 @@ const {IsSomeRendererActing} = ReactSharedInternals;
25 // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
26
27 let hasWarnedAboutMissingMockScheduler = false;
28 +const isSchedulerMocked =
29 + typeof Scheduler.unstable_flushAllWithoutAsserting === 'function';
30 const flushWork =
31 Scheduler.unstable_flushAllWithoutAsserting ||
32 function() {
@@ -70,18 +72,17 @@ function act(callback: () => Thenable) {
72 let previousIsSomeRendererActing;
73 let previousIsThisRendererActing;
74 actingUpdatesScopeDepth++;
73 - if (__DEV__) {
74 - previousIsSomeRendererActing = IsSomeRendererActing.current;
75 - previousIsThisRendererActing = IsThisRendererActing.current;
76 - IsSomeRendererActing.current = true;
77 - IsThisRendererActing.current = true;
78 - }
75 +
76 + previousIsSomeRendererActing = IsSomeRendererActing.current;
77 + previousIsThisRendererActing = IsThisRendererActing.current;
78 + IsSomeRendererActing.current = true;
79 + IsThisRendererActing.current = true;
80
81 function onDone() {
82 actingUpdatesScopeDepth--;
83 + IsSomeRendererActing.current = previousIsSomeRendererActing;
84 + IsThisRendererActing.current = previousIsThisRendererActing;
85 if (__DEV__) {
83 - IsSomeRendererActing.current = previousIsSomeRendererActing;
84 - IsThisRendererActing.current = previousIsThisRendererActing;
86 if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
87 // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
88 warningWithoutStack(
@@ -136,7 +137,11 @@ function act(callback: () => Thenable) {
137 called = true;
138 result.then(
139 () => {
139 - if (actingUpdatesScopeDepth > 1) {
140 + if (
141 + actingUpdatesScopeDepth > 1 ||
142 + (isSchedulerMocked === true &&
143 + previousIsSomeRendererActing === true)
144 + ) {
145 onDone();
146 resolve();
147 return;
@@ -171,7 +176,10 @@ function act(callback: () => Thenable) {
176
177 // flush effects until none remain, and cleanup
178 try {
174 - if (actingUpdatesScopeDepth === 1) {
179 + if (
180 + actingUpdatesScopeDepth === 1 &&
181 + (isSchedulerMocked === false || previousIsSomeRendererActing === false)
182 + ) {
183 // we're about to exit the act() scope,
184 // now's the time to flush effects
185 flushWork();