@samitouri / QOS-React-2 / commits / 996da67b29

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.

Andrew Clark committed Oct 18, 2021 at 11:59 UTC 996da67b2991f480b6219471e38aeaa7280bd00c
9 files changed +205 -47
packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+2
@@ -17,6 +17,8 @@ import {
17 } from '../../constants';
18 import REACT_VERSION from 'shared/ReactVersion';
19
20 +global.IS_REACT_ACT_ENVIRONMENT = true;
21 +
22 describe('getLanesFromTransportDecimalBitmask', () => {
23 it('should return array of lane numbers from bitmask string', () => {
24 expect(getLanesFromTransportDecimalBitmask('1')).toEqual([0]);
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+2
@@ -38,6 +38,8 @@ describe('InspectedElement', () => {
38 let ErrorBoundary;
39 let errorBoundaryInstance;
40
41 + global.IS_REACT_ACT_ENVIRONMENT = true;
42 +
43 beforeEach(() => {
44 utils = require('./utils');
45 utils.beforeEachProfiling();
packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js
+3 -23
@@ -76,9 +76,7 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
76 // Dispatch a click event on the Disable-button.
77 const firstEvent = document.createEvent('Event');
78 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);
80
81 // Discrete events should be flushed in a microtask.
82 // Verify that the second button was removed.
@@ -134,9 +132,7 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
132 // Dispatch a click event on the Disable-button.
133 const firstEvent = document.createEvent('Event');
134 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);
136
137 // There should now be a pending update to disable the form.
138 // This should not have flushed yet since it's in concurrent mode.
@@ -196,9 +192,7 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
192 // Dispatch a click event on the Enable-button.
193 const firstEvent = document.createEvent('Event');
194 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);
196
197 // There should now be a pending update to enable the form.
198 // This should not have flushed yet since it's in concurrent mode.
@@ -344,9 +338,6 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
338 });
339 expect(container.textContent).toEqual('Count: 0');
340
347 - // Ignore act warning. We can't use act because it forces batched updates.
348 - spyOnDev(console, 'error');
349 -
341 const pressEvent = document.createEvent('Event');
342 pressEvent.initEvent('click', true, true);
343 dispatchAndSetCurrentEvent(target.current, pressEvent);
@@ -355,17 +346,6 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
346 await null;
347 // If this is 2, that means the `setCount` calls were not batched.
348 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 - }
349 });
350
351 it('should not flush discrete events at the end of outermost batchedUpdates', async () => {
packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
+2
@@ -16,6 +16,8 @@ let container;
16
17 jest.useRealTimers();
18
19 +global.IS_REACT_ACT_ENVIRONMENT = true;
20 +
21 function sleep(period) {
22 return new Promise(resolve => {
23 setTimeout(() => {
packages/react-reconciler/src/ReactFiberAct.new.js
+31 -12
@@ -8,7 +8,13 @@
8 */
9
10 import type {Fiber} from './ReactFiber.new';
11 +
12 +import ReactSharedInternals from 'shared/ReactSharedInternals';
13 +
14 import {warnsIfNotActing} from './ReactFiberHostConfig';
15 +import {ConcurrentMode} from './ReactTypeOfMode';
16 +
17 +const {ReactCurrentActQueue} = ReactSharedInternals;
18
19 export function isActEnvironment(fiber: Fiber) {
20 if (__DEV__) {
@@ -18,18 +24,31 @@ export function isActEnvironment(fiber: Fiber) {
24 ? IS_REACT_ACT_ENVIRONMENT
25 : undefined;
26
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 + }
52 }
53 return false;
54 }
packages/react-reconciler/src/ReactFiberAct.old.js
+31 -12
@@ -8,7 +8,13 @@
8 */
9
10 import type {Fiber} from './ReactFiber.old';
11 +
12 +import ReactSharedInternals from 'shared/ReactSharedInternals';
13 +
14 import {warnsIfNotActing} from './ReactFiberHostConfig';
15 +import {ConcurrentMode} from './ReactTypeOfMode';
16 +
17 +const {ReactCurrentActQueue} = ReactSharedInternals;
18
19 export function isActEnvironment(fiber: Fiber) {
20 if (__DEV__) {
@@ -18,18 +24,31 @@ export function isActEnvironment(fiber: Fiber) {
24 ? IS_REACT_ACT_ENVIRONMENT
25 : undefined;
26
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 + }
52 }
53 return false;
54 }
packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js
+2
@@ -19,6 +19,8 @@ describe('DebugTracing', () => {
19 const DEFAULT_LANE_STRING = '0b0000000000000000000000000010000';
20 const RETRY_LANE_STRING = '0b0000000010000000000000000000000';
21
22 + global.IS_REACT_ACT_ENVIRONMENT = true;
23 +
24 beforeEach(() => {
25 jest.resetModules();
26
packages/react-reconciler/src/__tests__/ReactActWarnings-test.js new
+130
@@ -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 +});
packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js
+2
@@ -21,6 +21,8 @@ describe('SchedulingProfiler labels', () => {
21 let featureDetectionMarkName = null;
22 let marks;
23
24 + global.IS_REACT_ACT_ENVIRONMENT = true;
25 +
26 function polyfillJSDomUserTiming() {
27 featureDetectionMarkName = null;
28