Skip to content

Commit 996da67

Browse files
authored
Replace global jest heuristic with IS_REACT_ACT_ENVIRONMENT (#22562)
* Remove `jest` global check in concurrent roots In concurrent mode, instead of checking `jest`, we check the new `IS_REACT_ACT_ENVIRONMENT` global. The default behavior is `false`. Legacy mode behavior is unchanged. React's own internal test suite use a custom version of `act` that works by mocking the Scheduler — rather than the "real" act used publicly. So we don't enable the flag in our repo. * Warn if `act` is called in wrong environment Adds a warning if `act` is called but `IS_REACT_ACT_ENVIRONMENT` is not enabled. The goal is to prompt users to correctly configure their testing environment, so that if they forget to use `act` in a different test, we can detect and warn about. It's expected that the environment flag will be configured by the testing framework. For example, a Jest plugin. We will link to the relevant documentation page, once it exists. The warning only fires in concurrent mode. Legacy roots will keep the existing behavior.
1 parent 163e81c commit 996da67

File tree

9 files changed

+205
-47
lines changed

9 files changed

+205
-47
lines changed

Diff for: packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
} from '../../constants';
1818
import REACT_VERSION from 'shared/ReactVersion';
1919

20+
global.IS_REACT_ACT_ENVIRONMENT = true;
21+
2022
describe('getLanesFromTransportDecimalBitmask', () => {
2123
it('should return array of lane numbers from bitmask string', () => {
2224
expect(getLanesFromTransportDecimalBitmask('1')).toEqual([0]);

Diff for: packages/react-devtools-shared/src/__tests__/inspectedElement-test.js

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ describe('InspectedElement', () => {
3838
let ErrorBoundary;
3939
let errorBoundaryInstance;
4040

41+
global.IS_REACT_ACT_ENVIRONMENT = true;
42+
4143
beforeEach(() => {
4244
utils = require('./utils');
4345
utils.beforeEachProfiling();

Diff for: packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js

+3-23
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,7 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
7676
// Dispatch a click event on the Disable-button.
7777
const firstEvent = document.createEvent('Event');
7878
firstEvent.initEvent('click', true, true);
79-
expect(() =>
80-
dispatchAndSetCurrentEvent(disableButton, firstEvent),
81-
).toErrorDev(['An update to Form inside a test was not wrapped in act']);
79+
dispatchAndSetCurrentEvent(disableButton, firstEvent);
8280

8381
// Discrete events should be flushed in a microtask.
8482
// Verify that the second button was removed.
@@ -134,9 +132,7 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
134132
// Dispatch a click event on the Disable-button.
135133
const firstEvent = document.createEvent('Event');
136134
firstEvent.initEvent('click', true, true);
137-
expect(() => {
138-
dispatchAndSetCurrentEvent(disableButton, firstEvent);
139-
}).toErrorDev(['An update to Form inside a test was not wrapped in act']);
135+
dispatchAndSetCurrentEvent(disableButton, firstEvent);
140136

141137
// There should now be a pending update to disable the form.
142138
// This should not have flushed yet since it's in concurrent mode.
@@ -196,9 +192,7 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
196192
// Dispatch a click event on the Enable-button.
197193
const firstEvent = document.createEvent('Event');
198194
firstEvent.initEvent('click', true, true);
199-
expect(() => {
200-
dispatchAndSetCurrentEvent(enableButton, firstEvent);
201-
}).toErrorDev(['An update to Form inside a test was not wrapped in act']);
195+
dispatchAndSetCurrentEvent(enableButton, firstEvent);
202196

203197
// There should now be a pending update to enable the form.
204198
// This should not have flushed yet since it's in concurrent mode.
@@ -344,9 +338,6 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
344338
});
345339
expect(container.textContent).toEqual('Count: 0');
346340

347-
// Ignore act warning. We can't use act because it forces batched updates.
348-
spyOnDev(console, 'error');
349-
350341
const pressEvent = document.createEvent('Event');
351342
pressEvent.initEvent('click', true, true);
352343
dispatchAndSetCurrentEvent(target.current, pressEvent);
@@ -355,17 +346,6 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
355346
await null;
356347
// If this is 2, that means the `setCount` calls were not batched.
357348
expect(container.textContent).toEqual('Count: 1');
358-
359-
// Assert that the `act` warnings were the only ones that fired.
360-
if (__DEV__) {
361-
expect(console.error).toHaveBeenCalledTimes(2);
362-
expect(console.error.calls.argsFor(0)[0]).toContain(
363-
'was not wrapped in act',
364-
);
365-
expect(console.error.calls.argsFor(1)[0]).toContain(
366-
'was not wrapped in act',
367-
);
368-
}
369349
});
370350

371351
it('should not flush discrete events at the end of outermost batchedUpdates', async () => {

Diff for: packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ let container;
1616

1717
jest.useRealTimers();
1818

19+
global.IS_REACT_ACT_ENVIRONMENT = true;
20+
1921
function sleep(period) {
2022
return new Promise(resolve => {
2123
setTimeout(() => {

Diff for: packages/react-reconciler/src/ReactFiberAct.new.js

+31-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
*/
99

1010
import type {Fiber} from './ReactFiber.new';
11+
12+
import ReactSharedInternals from 'shared/ReactSharedInternals';
13+
1114
import {warnsIfNotActing} from './ReactFiberHostConfig';
15+
import {ConcurrentMode} from './ReactTypeOfMode';
16+
17+
const {ReactCurrentActQueue} = ReactSharedInternals;
1218

1319
export function isActEnvironment(fiber: Fiber) {
1420
if (__DEV__) {
@@ -18,18 +24,31 @@ export function isActEnvironment(fiber: Fiber) {
1824
? IS_REACT_ACT_ENVIRONMENT
1925
: undefined;
2026

21-
// TODO: Only check `jest` in legacy mode. In concurrent mode, this
22-
// heuristic is replaced by IS_REACT_ACT_ENVIRONMENT.
23-
// $FlowExpectedError - Flow doesn't know about jest
24-
const jestIsDefined = typeof jest !== 'undefined';
25-
return (
26-
warnsIfNotActing &&
27-
jestIsDefined &&
28-
// Legacy mode assumes an act environment whenever `jest` is defined, but
29-
// you can still turn off spurious warnings by setting
30-
// IS_REACT_ACT_ENVIRONMENT explicitly to false.
31-
isReactActEnvironmentGlobal !== false
32-
);
27+
if (fiber.mode & ConcurrentMode) {
28+
if (
29+
!isReactActEnvironmentGlobal &&
30+
ReactCurrentActQueue.current !== null
31+
) {
32+
// TODO: Include link to relevant documentation page.
33+
console.error(
34+
'The current testing environment is not configured to support ' +
35+
'act(...)',
36+
);
37+
}
38+
return isReactActEnvironmentGlobal;
39+
} else {
40+
// Legacy mode. We preserve the behavior of React 17's act. It assumes an
41+
// act environment whenever `jest` is defined, but you can still turn off
42+
// spurious warnings by setting IS_REACT_ACT_ENVIRONMENT explicitly
43+
// to false.
44+
// $FlowExpectedError - Flow doesn't know about jest
45+
const jestIsDefined = typeof jest !== 'undefined';
46+
return (
47+
warnsIfNotActing &&
48+
jestIsDefined &&
49+
isReactActEnvironmentGlobal !== false
50+
);
51+
}
3352
}
3453
return false;
3554
}

Diff for: packages/react-reconciler/src/ReactFiberAct.old.js

+31-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
*/
99

1010
import type {Fiber} from './ReactFiber.old';
11+
12+
import ReactSharedInternals from 'shared/ReactSharedInternals';
13+
1114
import {warnsIfNotActing} from './ReactFiberHostConfig';
15+
import {ConcurrentMode} from './ReactTypeOfMode';
16+
17+
const {ReactCurrentActQueue} = ReactSharedInternals;
1218

1319
export function isActEnvironment(fiber: Fiber) {
1420
if (__DEV__) {
@@ -18,18 +24,31 @@ export function isActEnvironment(fiber: Fiber) {
1824
? IS_REACT_ACT_ENVIRONMENT
1925
: undefined;
2026

21-
// TODO: Only check `jest` in legacy mode. In concurrent mode, this
22-
// heuristic is replaced by IS_REACT_ACT_ENVIRONMENT.
23-
// $FlowExpectedError - Flow doesn't know about jest
24-
const jestIsDefined = typeof jest !== 'undefined';
25-
return (
26-
warnsIfNotActing &&
27-
jestIsDefined &&
28-
// Legacy mode assumes an act environment whenever `jest` is defined, but
29-
// you can still turn off spurious warnings by setting
30-
// IS_REACT_ACT_ENVIRONMENT explicitly to false.
31-
isReactActEnvironmentGlobal !== false
32-
);
27+
if (fiber.mode & ConcurrentMode) {
28+
if (
29+
!isReactActEnvironmentGlobal &&
30+
ReactCurrentActQueue.current !== null
31+
) {
32+
// TODO: Include link to relevant documentation page.
33+
console.error(
34+
'The current testing environment is not configured to support ' +
35+
'act(...)',
36+
);
37+
}
38+
return isReactActEnvironmentGlobal;
39+
} else {
40+
// Legacy mode. We preserve the behavior of React 17's act. It assumes an
41+
// act environment whenever `jest` is defined, but you can still turn off
42+
// spurious warnings by setting IS_REACT_ACT_ENVIRONMENT explicitly
43+
// to false.
44+
// $FlowExpectedError - Flow doesn't know about jest
45+
const jestIsDefined = typeof jest !== 'undefined';
46+
return (
47+
warnsIfNotActing &&
48+
jestIsDefined &&
49+
isReactActEnvironmentGlobal !== false
50+
);
51+
}
3352
}
3453
return false;
3554
}

Diff for: packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ describe('DebugTracing', () => {
1919
const DEFAULT_LANE_STRING = '0b0000000000000000000000000010000';
2020
const RETRY_LANE_STRING = '0b0000000010000000000000000000000';
2121

22+
global.IS_REACT_ACT_ENVIRONMENT = true;
23+
2224
beforeEach(() => {
2325
jest.resetModules();
2426

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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+
* @jest-environment node
8+
*/
9+
10+
let React;
11+
let Scheduler;
12+
let ReactNoop;
13+
let useState;
14+
let act;
15+
16+
// These tests are mostly concerned with concurrent roots. The legacy root
17+
// behavior is covered by other older test suites and is unchanged from
18+
// React 17.
19+
describe('act warnings', () => {
20+
beforeEach(() => {
21+
jest.resetModules();
22+
React = require('react');
23+
Scheduler = require('scheduler');
24+
ReactNoop = require('react-noop-renderer');
25+
act = React.unstable_act;
26+
useState = React.useState;
27+
});
28+
29+
function Text(props) {
30+
Scheduler.unstable_yieldValue(props.text);
31+
return props.text;
32+
}
33+
34+
function withActEnvironment(value, scope) {
35+
const prevValue = global.IS_REACT_ACT_ENVIRONMENT;
36+
global.IS_REACT_ACT_ENVIRONMENT = value;
37+
try {
38+
return scope();
39+
} finally {
40+
global.IS_REACT_ACT_ENVIRONMENT = prevValue;
41+
}
42+
}
43+
44+
test('warns about unwrapped updates only if environment flag is enabled', () => {
45+
let setState;
46+
function App() {
47+
const [state, _setState] = useState(0);
48+
setState = _setState;
49+
return <Text text={state} />;
50+
}
51+
52+
const root = ReactNoop.createRoot();
53+
root.render(<App />);
54+
expect(Scheduler).toFlushAndYield([0]);
55+
expect(root).toMatchRenderedOutput('0');
56+
57+
// Default behavior. Flag is undefined. No warning.
58+
expect(global.IS_REACT_ACT_ENVIRONMENT).toBe(undefined);
59+
setState(1);
60+
expect(Scheduler).toFlushAndYield([1]);
61+
expect(root).toMatchRenderedOutput('1');
62+
63+
// Flag is true. Warn.
64+
withActEnvironment(true, () => {
65+
expect(() => setState(2)).toErrorDev(
66+
'An update to App inside a test was not wrapped in act',
67+
);
68+
expect(Scheduler).toFlushAndYield([2]);
69+
expect(root).toMatchRenderedOutput('2');
70+
});
71+
72+
// Flag is false. No warning.
73+
withActEnvironment(false, () => {
74+
setState(3);
75+
expect(Scheduler).toFlushAndYield([3]);
76+
expect(root).toMatchRenderedOutput('3');
77+
});
78+
});
79+
80+
// @gate __DEV__
81+
test('act warns if the environment flag is not enabled', () => {
82+
let setState;
83+
function App() {
84+
const [state, _setState] = useState(0);
85+
setState = _setState;
86+
return <Text text={state} />;
87+
}
88+
89+
const root = ReactNoop.createRoot();
90+
root.render(<App />);
91+
expect(Scheduler).toFlushAndYield([0]);
92+
expect(root).toMatchRenderedOutput('0');
93+
94+
// Default behavior. Flag is undefined. Warn.
95+
expect(global.IS_REACT_ACT_ENVIRONMENT).toBe(undefined);
96+
expect(() => {
97+
act(() => {
98+
setState(1);
99+
});
100+
}).toErrorDev(
101+
'The current testing environment is not configured to support act(...)',
102+
{withoutStack: true},
103+
);
104+
expect(Scheduler).toHaveYielded([1]);
105+
expect(root).toMatchRenderedOutput('1');
106+
107+
// Flag is true. Don't warn.
108+
withActEnvironment(true, () => {
109+
act(() => {
110+
setState(2);
111+
});
112+
expect(Scheduler).toHaveYielded([2]);
113+
expect(root).toMatchRenderedOutput('2');
114+
});
115+
116+
// Flag is false. Warn.
117+
withActEnvironment(false, () => {
118+
expect(() => {
119+
act(() => {
120+
setState(1);
121+
});
122+
}).toErrorDev(
123+
'The current testing environment is not configured to support act(...)',
124+
{withoutStack: true},
125+
);
126+
expect(Scheduler).toHaveYielded([1]);
127+
expect(root).toMatchRenderedOutput('1');
128+
});
129+
});
130+
});

Diff for: packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ describe('SchedulingProfiler labels', () => {
2121
let featureDetectionMarkName = null;
2222
let marks;
2323

24+
global.IS_REACT_ACT_ENVIRONMENT = true;
25+
2426
function polyfillJSDomUserTiming() {
2527
featureDetectionMarkName = null;
2628

0 commit comments

Comments
 (0)