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();