@samitouri / QOS-React / commits / d17086c7c8

Decouple public, internal act implementation (#19745)

In the next major release, we intend to drop support for using the `act` testing helper in production. (It already fires a warning.) The rationale is that, in order for `act` to work, you must either mock the testing environment or add extra logic at runtime. Mocking the testing environment isn't ideal because it requires extra set up for the user. Extra logic at runtime is fine only in development mode — we don't want to slow down the production builds. Since most people only run their tests in development mode, dropping support for production should be fine; if there's demand, we can add it back later using a special testing build that is identical to the production build except for the additional testing logic. One blocker for removing production support is that we currently use `act` to test React itself. We must test React in both development and production modes. So, the solution is to fork `act` into separate public and internal implementations: - *public implementation of `act`* – exposed to users, only works in development mode, uses special runtime logic, does not support partial rendering - *internal implementation of `act`* – private, works in both development and productionm modes, only used by the React Core test suite, uses no special runtime logic, supports partial rendering (i.e. `toFlushAndYieldThrough`) The internal implementation should mostly match the public implementation's behavior, but since it's a private API, it doesn't have to match exactly. It works by mocking the test environment: it uses a mock build of Scheduler to flush rendering tasks, and Jest's mock timers to flush Suspense placeholders. --- In this first commit, I've added the internal forks of `act` and migrated our tests to use them. The public `act` implementation is unaffected for now; I will leave refactoring/clean-up for a later step.

Andrew Clark committed Sep 8, 2020 at 10:11 UTC d17086c7c813402a550d15a2f56dc43f1dbd1735
36 files changed +465 -298
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+1 -1
@@ -22,7 +22,7 @@ describe('ReactHooksInspectionIntegration', () => {
22 React = require('react');
23 ReactTestRenderer = require('react-test-renderer');
24 Scheduler = require('scheduler');
25 - act = ReactTestRenderer.act;
25 + act = ReactTestRenderer.unstable_concurrentAct;
26 ReactDebugTools = require('react-debug-tools');
27 });
28
packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js
+25 -20
@@ -33,6 +33,9 @@ describe('InspectedElementContext', () => {
33 let TestUtils;
34 let TreeContextController;
35
36 + let TestUtilsAct;
37 + let TestRendererAct;
38 +
39 beforeEach(() => {
40 utils = require('./utils');
41 utils.beforeEachProfiling();
@@ -47,7 +50,9 @@ describe('InspectedElementContext', () => {
50 ReactDOM = require('react-dom');
51 PropTypes = require('prop-types');
52 TestUtils = require('react-dom/test-utils');
53 + TestUtilsAct = TestUtils.unstable_concurrentAct;
54 TestRenderer = utils.requireTestRenderer();
55 + TestRendererAct = TestUtils.unstable_concurrentAct;
56
57 BridgeContext = require('react-devtools-shared/src/devtools/views/context')
58 .BridgeContext;
@@ -999,8 +1004,8 @@ describe('InspectedElementContext', () => {
1004 expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
1005
1006 inspectedElement = null;
1002 - TestUtils.act(() => {
1003 - TestRenderer.act(() => {
1007 + TestUtilsAct(() => {
1008 + TestRendererAct(() => {
1009 getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
1010 jest.runOnlyPendingTimers();
1011 });
@@ -1009,8 +1014,8 @@ describe('InspectedElementContext', () => {
1014 expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
1015
1016 inspectedElement = null;
1012 - TestUtils.act(() => {
1013 - TestRenderer.act(() => {
1017 + TestUtilsAct(() => {
1018 + TestRendererAct(() => {
1019 getInspectedElementPath(id, ['props', 'nestedObject', 'a', 'b', 'c']);
1020 jest.runOnlyPendingTimers();
1021 });
@@ -1021,8 +1026,8 @@ describe('InspectedElementContext', () => {
1026 );
1027
1028 inspectedElement = null;
1024 - TestUtils.act(() => {
1025 - TestRenderer.act(() => {
1029 + TestUtilsAct(() => {
1030 + TestRendererAct(() => {
1031 getInspectedElementPath(id, [
1032 'props',
1033 'nestedObject',
@@ -1041,8 +1046,8 @@ describe('InspectedElementContext', () => {
1046 );
1047
1048 inspectedElement = null;
1044 - TestUtils.act(() => {
1045 - TestRenderer.act(() => {
1049 + TestUtilsAct(() => {
1050 + TestRendererAct(() => {
1051 getInspectedElementPath(id, ['hooks', 0, 'value']);
1052 jest.runOnlyPendingTimers();
1053 });
@@ -1051,8 +1056,8 @@ describe('InspectedElementContext', () => {
1056 expect(inspectedElement).toMatchSnapshot('5: Inspect hooks.0.value');
1057
1058 inspectedElement = null;
1054 - TestUtils.act(() => {
1055 - TestRenderer.act(() => {
1059 + TestUtilsAct(() => {
1060 + TestRendererAct(() => {
1061 getInspectedElementPath(id, ['hooks', 0, 'value', 'foo', 'bar']);
1062 jest.runOnlyPendingTimers();
1063 });
@@ -1108,8 +1113,8 @@ describe('InspectedElementContext', () => {
1113 expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
1114
1115 inspectedElement = null;
1111 - TestUtils.act(() => {
1112 - TestRenderer.act(() => {
1116 + TestUtilsAct(() => {
1117 + TestRendererAct(() => {
1118 getInspectedElementPath(id, ['props', 'set_of_sets', 0]);
1119 jest.runOnlyPendingTimers();
1120 });
@@ -1179,7 +1184,7 @@ describe('InspectedElementContext', () => {
1184 expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
1185
1186 inspectedElement = null;
1182 - TestRenderer.act(() => {
1187 + TestRendererAct(() => {
1188 getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
1189 jest.runOnlyPendingTimers();
1190 });
@@ -1187,15 +1192,15 @@ describe('InspectedElementContext', () => {
1192 expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
1193
1194 inspectedElement = null;
1190 - TestRenderer.act(() => {
1195 + TestRendererAct(() => {
1196 getInspectedElementPath(id, ['props', 'nestedObject', 'c']);
1197 jest.runOnlyPendingTimers();
1198 });
1199 expect(inspectedElement).not.toBeNull();
1200 expect(inspectedElement).toMatchSnapshot('3: Inspect props.nestedObject.c');
1201
1197 - TestRenderer.act(() => {
1198 - TestUtils.act(() => {
1202 + TestRendererAct(() => {
1203 + TestUtilsAct(() => {
1204 ReactDOM.render(
1205 <Example
1206 nestedObject={{
@@ -1221,7 +1226,7 @@ describe('InspectedElementContext', () => {
1226 });
1227 });
1228
1224 - TestRenderer.act(() => {
1229 + TestRendererAct(() => {
1230 inspectedElement = null;
1231 jest.advanceTimersByTime(1000);
1232 });
@@ -1281,7 +1286,7 @@ describe('InspectedElementContext', () => {
1286 expect(inspectedElement).not.toBeNull();
1287 expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
1288
1284 - TestUtils.act(() => {
1289 + TestUtilsAct(() => {
1290 ReactDOM.render(
1291 <Example
1292 nestedObject={{
@@ -1300,8 +1305,8 @@ describe('InspectedElementContext', () => {
1305
1306 inspectedElement = null;
1307
1303 - TestRenderer.act(() => {
1304 - TestUtils.act(() => {
1308 + TestRendererAct(() => {
1309 + TestUtilsAct(() => {
1310 getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
1311 jest.runOnlyPendingTimers();
1312 });
packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js
+1 -1
@@ -20,7 +20,7 @@ describe('Store component filters', () => {
20 let utils;
21
22 const act = (callback: Function) => {
23 - TestUtils.act(() => {
23 + TestUtils.unstable_concurrentAct(() => {
24 callback();
25 });
26 jest.runAllTimers(); // Flush Bridge operations
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
+1 -1
@@ -28,7 +28,7 @@ describe('ReactDOMFiberAsync', () => {
28 container = document.createElement('div');
29 React = require('react');
30 ReactDOM = require('react-dom');
31 - act = require('react-dom/test-utils').act;
31 + act = require('react-dom/test-utils').unstable_concurrentAct;
32 Scheduler = require('scheduler');
33
34 document.body.appendChild(container);
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
+15 -13
@@ -17,6 +17,7 @@ let React;
17 let ReactDOM;
18 let ReactDOMServer;
19 let ReactTestUtils;
20 +let act;
21 let Scheduler;
22 let useState;
23 let useReducer;
@@ -43,6 +44,7 @@ function initModules() {
44 ReactDOMServer = require('react-dom/server');
45 ReactTestUtils = require('react-dom/test-utils');
46 Scheduler = require('scheduler');
47 + act = ReactTestUtils.unstable_concurrentAct;
48 useState = React.useState;
49 useReducer = React.useReducer;
50 useEffect = React.useEffect;
@@ -1063,7 +1065,7 @@ describe('ReactDOMServerHooks', () => {
1065 expect(domNode.children.length).toEqual(1);
1066 expect(oldClientId).not.toBeNull();
1067
1066 - await ReactTestUtils.act(async () => _setShowId(true));
1068 + await act(async () => _setShowId(true));
1069
1070 expect(domNode.children.length).toEqual(2);
1071 expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
@@ -1281,7 +1283,7 @@ describe('ReactDOMServerHooks', () => {
1283 const oldServerId = container.children[0].children[0].getAttribute('id');
1284 expect(oldServerId).not.toBeNull();
1285
1284 - await ReactTestUtils.act(async () => {
1286 + await act(async () => {
1287 _setShowDiv(true);
1288 });
1289 expect(container.children[0].children.length).toEqual(2);
@@ -1322,7 +1324,7 @@ describe('ReactDOMServerHooks', () => {
1324 const oldServerId = container.children[0].children[0].getAttribute('id');
1325 expect(oldServerId).not.toBeNull();
1326
1325 - await ReactTestUtils.act(async () => {
1327 + await act(async () => {
1328 _setShowDiv(true);
1329 });
1330 expect(container.children[0].children.length).toEqual(2);
@@ -1356,12 +1358,12 @@ describe('ReactDOMServerHooks', () => {
1358 document.body.append(container);
1359 container.innerHTML = ReactDOMServer.renderToString(<App />);
1360 const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1359 - ReactTestUtils.act(() => {
1361 + act(() => {
1362 root.render(<App />);
1363 });
1364 expect(Scheduler).toHaveYielded(['App', 'App']);
1365 // The ID goes from not being used to being added to the page
1364 - ReactTestUtils.act(() => {
1366 + act(() => {
1367 _setShow(true);
1368 });
1369 expect(Scheduler).toHaveYielded(['App', 'App']);
@@ -1391,7 +1393,7 @@ describe('ReactDOMServerHooks', () => {
1393 ReactDOM.hydrate(<App />, container);
1394 expect(Scheduler).toHaveYielded(['App', 'App']);
1395 // The ID goes from not being used to being added to the page
1394 - ReactTestUtils.act(() => {
1396 + act(() => {
1397 _setShow(true);
1398 });
1399 expect(Scheduler).toHaveYielded(['App']);
@@ -1418,12 +1420,12 @@ describe('ReactDOMServerHooks', () => {
1420 document.body.append(container);
1421 container.innerHTML = ReactDOMServer.renderToString(<App />);
1422 const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1421 - ReactTestUtils.act(() => {
1423 + act(() => {
1424 root.render(<App />);
1425 });
1426
1427 // The ID goes from not being used to being added to the page
1426 - ReactTestUtils.act(() => {
1428 + act(() => {
1429 ReactDOM.flushSync(() => {
1430 _setShow(true);
1431 });
@@ -1518,7 +1520,7 @@ describe('ReactDOMServerHooks', () => {
1520 expect(child1Ref.current).toBe(null);
1521 expect(Scheduler).toHaveYielded([]);
1522
1521 - ReactTestUtils.act(() => {
1523 + act(() => {
1524 _setShow(true);
1525
1526 // State update should trigger the ID to update, which changes the props
@@ -1603,7 +1605,7 @@ describe('ReactDOMServerHooks', () => {
1605
1606 suspend = true;
1607 const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1606 - await ReactTestUtils.act(async () => {
1608 + await act(async () => {
1609 root.render(<App />);
1610 });
1611 jest.runAllTimers();
@@ -1616,7 +1618,7 @@ describe('ReactDOMServerHooks', () => {
1618 container.children[0].children[0].getAttribute('id'),
1619 ).not.toBeNull();
1620
1619 - await ReactTestUtils.act(async () => {
1621 + await act(async () => {
1622 suspend = false;
1623 resolve();
1624 await promise;
@@ -1703,7 +1705,7 @@ describe('ReactDOMServerHooks', () => {
1705
1706 suspend = false;
1707 const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1706 - await ReactTestUtils.act(async () => {
1708 + await act(async () => {
1709 root.render(<App />);
1710 });
1711 jest.runAllTimers();
@@ -1968,7 +1970,7 @@ describe('ReactDOMServerHooks', () => {
1970 expect(Scheduler).toHaveYielded([]);
1971 expect(Scheduler).toFlushAndYield([]);
1972
1971 - ReactTestUtils.act(() => {
1973 + act(() => {
1974 _setShow(false);
1975 });
1976
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+1 -1
@@ -79,7 +79,7 @@ describe('ReactDOMServerPartialHydration', () => {
79
80 React = require('react');
81 ReactDOM = require('react-dom');
82 - act = require('react-dom/test-utils').act;
82 + act = require('react-dom/test-utils').unstable_concurrentAct;
83 ReactDOMServer = require('react-dom/server');
84 Scheduler = require('scheduler');
85 Suspense = React.Suspense;
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+3 -1
@@ -17,6 +17,7 @@ let ReactDOMServer;
17 let ReactTestUtils;
18 let Scheduler;
19 let Suspense;
20 +let act;
21
22 function dispatchMouseHoverEvent(to, from) {
23 if (!to) {
@@ -101,6 +102,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
102 ReactDOM = require('react-dom');
103 ReactDOMServer = require('react-dom/server');
104 ReactTestUtils = require('react-dom/test-utils');
105 + act = ReactTestUtils.unstable_concurrentAct;
106 Scheduler = require('scheduler');
107 Suspense = React.Suspense;
108 });
@@ -880,7 +882,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
882 const spanC = container.getElementsByTagName('span')[4];
883
884 const root = ReactDOM.createRoot(container, {hydrate: true});
883 - ReactTestUtils.act(() => {
885 + act(() => {
886 root.render(<App a="A" />);
887
888 // Hydrate the shell.
packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
+3 -1
@@ -15,6 +15,7 @@ let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 let ReactTestUtils;
18 +let act;
19
20 function initModules() {
21 // Reset warning cache.
@@ -24,6 +25,7 @@ function initModules() {
25 ReactDOM = require('react-dom');
26 ReactDOMServer = require('react-dom/server');
27 ReactTestUtils = require('react-dom/test-utils');
28 + act = ReactTestUtils.unstable_concurrentAct;
29
30 // Make them available to the helpers.
31 return {
@@ -124,7 +126,7 @@ describe('ReactDOMServerSuspense', () => {
126 expect(divB.tagName).toBe('DIV');
127 expect(divB.textContent).toBe('B');
128
127 - ReactTestUtils.act(() => {
129 + act(() => {
130 const root = ReactDOM.createBlockingRoot(parent, {hydrate: true});
131 root.render(example);
132 });
packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js
+1 -1
@@ -28,7 +28,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
28 ReactCache = require('react-cache');
29 ReactTestUtils = require('react-dom/test-utils');
30 Scheduler = require('scheduler');
31 - act = ReactTestUtils.act;
31 + act = ReactTestUtils.unstable_concurrentAct;
32 Suspense = React.Suspense;
33 container = document.createElement('div');
34 document.body.appendChild(container);
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+1 -1
@@ -44,7 +44,7 @@ describe('ReactErrorBoundaries', () => {
44 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
45 ReactDOM = require('react-dom');
46 React = require('react');
47 - act = require('react-dom/test-utils').act;
47 + act = require('react-dom/test-utils').unstable_concurrentAct;
48 Scheduler = require('scheduler');
49
50 BrokenConstructor = class extends React.Component {
packages/react-dom/src/__tests__/ReactUpdates-test.js
+1 -1
@@ -21,7 +21,7 @@ describe('ReactUpdates', () => {
21 React = require('react');
22 ReactDOM = require('react-dom');
23 ReactTestUtils = require('react-dom/test-utils');
24 - act = ReactTestUtils.act;
24 + act = ReactTestUtils.unstable_concurrentAct;
25 Scheduler = require('scheduler');
26 });
27
packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js
+2 -2
@@ -49,11 +49,11 @@ module.exports = function(initModules) {
49 function asyncReactDOMRender(reactElement, domElement, forceHydrate) {
50 return new Promise(resolve => {
51 if (forceHydrate) {
52 - ReactTestUtils.act(() => {
52 + ReactTestUtils.unstable_concurrentAct(() => {
53 ReactDOM.hydrate(reactElement, domElement);
54 });
55 } else {
56 - ReactTestUtils.act(() => {
56 + ReactTestUtils.unstable_concurrentAct(() => {
57 ReactDOM.render(reactElement, domElement);
58 });
59 }
packages/react-dom/src/client/ReactDOM.js
+3
@@ -37,6 +37,7 @@ import {
37 attemptHydrationAtCurrentPriority,
38 runWithPriority,
39 getCurrentUpdateLanePriority,
40 + act,
41 } from 'react-reconciler/src/ReactFiberReconciler';
42 import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal';
43 import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -183,7 +184,9 @@ const Internals = {
184 enqueueStateRestore,
185 restoreStateIfNeeded,
186 flushPassiveEffects,
187 + // TODO: These are related to `act`, not events. Move to separate key?
188 IsThisRendererActing,
189 + act,
190 ],
191 };
192
packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+6 -4
@@ -17,6 +17,7 @@ let ReactDOM;
17 let ReactDOMServer;
18 let Scheduler;
19 let ReactTestUtils;
20 +let act;
21
22 function dispatchEvent(element, type) {
23 const event = document.createEvent('Event');
@@ -1236,6 +1237,7 @@ describe('DOMPluginEventSystem', () => {
1237 Scheduler = require('scheduler');
1238 ReactDOMServer = require('react-dom/server');
1239 ReactTestUtils = require('react-dom/test-utils');
1240 + act = ReactTestUtils.unstable_concurrentAct;
1241 });
1242
1243 // @gate experimental
@@ -2674,7 +2676,7 @@ describe('DOMPluginEventSystem', () => {
2676
2677 const root = ReactDOM.createRoot(container2);
2678
2677 - ReactTestUtils.act(() => {
2679 + act(() => {
2680 root.render(<Component />);
2681 });
2682 jest.runAllTimers();
@@ -2686,7 +2688,7 @@ describe('DOMPluginEventSystem', () => {
2688 expect(onAfterBlur).toHaveBeenCalledTimes(0);
2689
2690 suspend = true;
2689 - ReactTestUtils.act(() => {
2691 + act(() => {
2692 root.render(<Component />);
2693 });
2694 jest.runAllTimers();
@@ -2746,7 +2748,7 @@ describe('DOMPluginEventSystem', () => {
2748 document.body.appendChild(container2);
2749
2750 const root = ReactDOM.createRoot(container2);
2749 - ReactTestUtils.act(() => {
2751 + act(() => {
2752 root.render(<Component />);
2753 });
2754
@@ -2757,7 +2759,7 @@ describe('DOMPluginEventSystem', () => {
2759
2760 // Suspend. This hides the input node, causing it to lose focus.
2761 suspend = true;
2760 - ReactTestUtils.act(() => {
2762 + act(() => {
2763 root.render(<Component />);
2764 });
2765
packages/react-dom/src/events/plugins/__tests__/ChangeEventPlugin-test.js
+1 -1
@@ -736,7 +736,7 @@ describe('ChangeEventPlugin', () => {
736
737 // @gate experimental
738 it('mouse enter/leave should be user-blocking but not discrete', async () => {
739 - const {act} = TestUtils;
739 + const {unstable_concurrentAct: act} = TestUtils;
740 const {useState} = React;
741
742 const root = ReactDOM.unstable_createRoot(container);
packages/react-dom/src/test-utils/ReactTestUtils.js
+13 -14
@@ -18,26 +18,24 @@ import {
18 import {SyntheticEvent} from '../events/SyntheticEvent';
19 import invariant from 'shared/invariant';
20 import {ELEMENT_NODE} from '../shared/HTMLNodeType';
21 -import act from './ReactTestUtilsAct';
21 +import {unstable_concurrentAct} from './ReactTestUtilsAct';
22 import {
23 rethrowCaughtError,
24 invokeGuardedCallbackAndCatchFirstError,
25 } from 'shared/ReactErrorUtils';
26
27 // Keep in sync with ReactDOM.js, and ReactTestUtilsAct.js:
28 -const [
29 - getInstanceFromNode,
30 - /* eslint-disable no-unused-vars */
31 - getNodeFromInstance,
32 - getFiberCurrentPropsFromNode,
33 - /* eslint-enable no-unused-vars */
34 - enqueueStateRestore,
35 - restoreStateIfNeeded,
36 - /* eslint-disable no-unused-vars */
37 - flushPassiveEffects,
38 - IsThisRendererActing,
39 - /* eslint-enable no-unused-vars */
40 -] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
28 +const EventInternals =
29 + ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
30 +const getInstanceFromNode = EventInternals[0];
31 +const getNodeFromInstance = EventInternals[1];
32 +const getFiberCurrentPropsFromNode = EventInternals[2];
33 +const enqueueStateRestore = EventInternals[3];
34 +const restoreStateIfNeeded = EventInternals[4];
35 +// const flushPassiveEffects = EventInternals[5];
36 +// TODO: These are related to `act`, not events. Move to separate key?
37 +// const IsThisRendererActing = EventInternals[6];
38 +const act = EventInternals[7];
39
40 function Event(suffix) {}
41
@@ -728,4 +726,5 @@ export {
726 nativeTouchData,
727 Simulate,
728 act,
729 + unstable_concurrentAct,
730 };
packages/react-dom/src/test-utils/ReactTestUtilsAct.js
+90 -161
@@ -15,200 +15,129 @@ import enqueueTask from 'shared/enqueueTask';
15 import * as Scheduler from 'scheduler';
16
17 // Keep in sync with ReactDOM.js, and ReactTestUtils.js:
18 -const [
19 - /* eslint-disable no-unused-vars */
20 - getInstanceFromNode,
21 - getNodeFromInstance,
22 - getFiberCurrentPropsFromNode,
23 - enqueueStateRestore,
24 - restoreStateIfNeeded,
25 - /* eslint-enable no-unused-vars */
26 - flushPassiveEffects,
27 - IsThisRendererActing,
28 -] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
18 +const EventInternals =
19 + ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
20 +// const getInstanceFromNode = EventInternals[0];
21 +// const getNodeFromInstance = EventInternals[1];
22 +// const getFiberCurrentPropsFromNode = EventInternals[2];
23 +// const enqueueStateRestore = EventInternals[3];
24 +// const restoreStateIfNeeded = EventInternals[4];
25 +// const flushPassiveEffects = EventInternals[5];
26 +const IsThisRendererActing = EventInternals[6];
27
28 const batchedUpdates = ReactDOM.unstable_batchedUpdates;
29
30 const {IsSomeRendererActing} = ReactSharedInternals;
31
34 -// this implementation should be exactly the same in
35 -// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
36 -
37 -const isSchedulerMocked =
38 - typeof Scheduler.unstable_flushAllWithoutAsserting === 'function';
39 -const flushWork =
40 - Scheduler.unstable_flushAllWithoutAsserting ||
41 - function() {
42 - let didFlushWork = false;
43 - while (flushPassiveEffects()) {
44 - didFlushWork = true;
45 - }
46 -
47 - return didFlushWork;
48 - };
49 -
50 -function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
51 - try {
52 - flushWork();
53 - enqueueTask(() => {
54 - if (flushWork()) {
55 - flushWorkAndMicroTasks(onDone);
56 - } else {
57 - onDone();
58 - }
59 - });
60 - } catch (err) {
61 - onDone(err);
62 - }
63 -}
64 -
65 -// we track the 'depth' of the act() calls with this counter,
66 -// so we can tell if any async act() calls try to run in parallel.
32 +// This version of `act` is only used by our tests. Unlike the public version
33 +// of `act`, it's designed to work identically in both production and
34 +// development. It may have slightly different behavior from the public
35 +// version, too, since our constraints in our test suite are not the same as
36 +// those of developers using React — we're testing React itself, as opposed to
37 +// building an app with React.
38
39 let actingUpdatesScopeDepth = 0;
69 -let didWarnAboutUsingActInProd = false;
40
71 -function act(callback: () => Thenable<mixed>): Thenable<void> {
72 - if (!__DEV__) {
73 - if (didWarnAboutUsingActInProd === false) {
74 - didWarnAboutUsingActInProd = true;
75 - // eslint-disable-next-line react-internal/no-production-logging
76 - console.error(
77 - 'act(...) is not supported in production builds of React, and might not behave as expected.',
78 - );
79 - }
41 +export function unstable_concurrentAct(scope: () => Thenable<mixed> | void) {
42 + if (Scheduler.unstable_flushAllWithoutAsserting === undefined) {
43 + throw Error(
44 + 'This version of `act` requires a special mock build of Scheduler.',
45 + );
46 + }
47 + if (setTimeout._isMockFunction !== true) {
48 + throw Error(
49 + "This version of `act` requires Jest's timer mocks " +
50 + '(i.e. jest.useFakeTimers).',
51 + );
52 }
81 - const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
82 - actingUpdatesScopeDepth++;
53
54 + const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
55 const previousIsSomeRendererActing = IsSomeRendererActing.current;
56 const previousIsThisRendererActing = IsThisRendererActing.current;
57 IsSomeRendererActing.current = true;
58 IsThisRendererActing.current = true;
59 + actingUpdatesScopeDepth++;
60
89 - function onDone() {
61 + const unwind = () => {
62 actingUpdatesScopeDepth--;
63 IsSomeRendererActing.current = previousIsSomeRendererActing;
64 IsThisRendererActing.current = previousIsThisRendererActing;
65 +
66 if (__DEV__) {
67 if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
95 - // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
68 + // if it's _less than_ previousActingUpdatesScopeDepth, then we can
69 + // assume the 'other' one has warned
70 console.error(
71 'You seem to have overlapping act() calls, this is not supported. ' +
72 'Be sure to await previous act() calls before making a new one. ',
73 );
74 }
75 }
102 - }
76 + };
77
104 - let result;
78 + // TODO: This would be way simpler if 1) we required a promise to be
79 + // returned and 2) we could use async/await. Since it's only our used in
80 + // our test suite, we should be able to.
81 try {
106 - result = batchedUpdates(callback);
107 - } catch (error) {
108 - // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth
109 - onDone();
110 - throw error;
111 - }
112 -
113 - if (
114 - result !== null &&
115 - typeof result === 'object' &&
116 - typeof result.then === 'function'
117 - ) {
118 - // setup a boolean that gets set to true only
119 - // once this act() call is await-ed
120 - let called = false;
121 - if (__DEV__) {
122 - if (typeof Promise !== 'undefined') {
123 - //eslint-disable-next-line no-undef
124 - Promise.resolve()
125 - .then(() => {})
126 - .then(() => {
127 - if (called === false) {
128 - console.error(
129 - 'You called act(async () => ...) without await. ' +
130 - 'This could lead to unexpected testing behaviour, interleaving multiple act ' +
131 - 'calls and mixing their scopes. You should - await act(async () => ...);',
82 + const thenable = batchedUpdates(scope);
83 + if (
84 + typeof thenable === 'object' &&
85 + thenable !== null &&
86 + typeof thenable.then === 'function'
87 + ) {
88 + return {
89 + then(resolve: () => void, reject: (error: mixed) => void) {
90 + thenable.then(
91 + () => {
92 + flushActWork(
93 + () => {
94 + unwind();
95 + resolve();
96 + },
97 + error => {
98 + unwind();
99 + reject(error);
100 + },
101 );
133 - }
134 - });
135 - }
136 - }
137 -
138 - // in the async case, the returned thenable runs the callback, flushes
139 - // effects and microtasks in a loop until flushPassiveEffects() === false,
140 - // and cleans up
141 - return {
142 - then(resolve, reject) {
143 - called = true;
144 - result.then(
145 - () => {
146 - if (
147 - actingUpdatesScopeDepth > 1 ||
148 - (isSchedulerMocked === true &&
149 - previousIsSomeRendererActing === true)
150 - ) {
151 - onDone();
152 - resolve();
153 - return;
154 - }
155 - // we're about to exit the act() scope,
156 - // now's the time to flush tasks/effects
157 - flushWorkAndMicroTasks((err: ?Error) => {
158 - onDone();
159 - if (err) {
160 - reject(err);
161 - } else {
162 - resolve();
163 - }
164 - });
165 - },
166 - err => {
167 - onDone();
168 - reject(err);
169 - },
170 - );
171 - },
172 - };
173 - } else {
174 - if (__DEV__) {
175 - if (result !== undefined) {
176 - console.error(
177 - 'The callback passed to act(...) function ' +
178 - 'must return undefined, or a Promise. You returned %s',
179 - result,
180 - );
102 + },
103 + error => {
104 + unwind();
105 + reject(error);
106 + },
107 + );
108 + },
109 + };
110 + } else {
111 + try {
112 + // TODO: Let's not support non-async scopes at all in our tests. Need to
113 + // migrate existing tests.
114 + let didFlushWork;
115 + do {
116 + didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
117 + } while (didFlushWork);
118 + } finally {
119 + unwind();
120 }
121 }
122 + } catch (error) {
123 + unwind();
124 + throw error;
125 + }
126 +}
127
184 - // flush effects until none remain, and cleanup
128 +function flushActWork(resolve, reject) {
129 + // TODO: Run timers to flush suspended fallbacks
130 + // jest.runOnlyPendingTimers();
131 + enqueueTask(() => {
132 try {
186 - if (
187 - actingUpdatesScopeDepth === 1 &&
188 - (isSchedulerMocked === false || previousIsSomeRendererActing === false)
189 - ) {
190 - // we're about to exit the act() scope,
191 - // now's the time to flush effects
192 - flushWork();
133 + const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
134 + if (didFlushWork) {
135 + flushActWork(resolve, reject);
136 + } else {
137 + resolve();
138 }
194 - onDone();
195 - } catch (err) {
196 - onDone();
197 - throw err;
139 + } catch (error) {
140 + reject(error);
141 }
199 -
200 - // in the sync case, the returned thenable only warns *if* await-ed
201 - return {
202 - then(resolve) {
203 - if (__DEV__) {
204 - console.error(
205 - 'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
206 - );
207 - }
208 - resolve();
209 - },
210 - };
211 - }
142 + });
143 }
213 -
214 -export default act;
packages/react-interactions/events/src/dom/create-event-handle/__tests__/useFocusWithin-test.internal.js
+1 -1
@@ -29,7 +29,7 @@ function initializeModules(hasPointerEvents) {
29 ReactDOM = require('react-dom');
30 ReactTestRenderer = require('react-test-renderer');
31 Scheduler = require('scheduler');
32 - act = ReactTestRenderer.act;
32 + act = ReactTestRenderer.unstable_concurrentAct;
33
34 // TODO: This import throws outside of experimental mode. Figure out better
35 // strategy for gated imports.
packages/react-noop-renderer/src/createReactNoop.js
+120 -2
@@ -16,7 +16,7 @@
16
17 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
18 import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
19 -import type {ReactNodeList} from 'shared/ReactTypes';
19 +import type {ReactNodeList, Thenable} from 'shared/ReactTypes';
20 import type {RootTag} from 'react-reconciler/src/ReactRootTags';
21
22 import * as Scheduler from 'scheduler/unstable_mock';
@@ -27,6 +27,10 @@ import {
27 LegacyRoot,
28 } from 'react-reconciler/src/ReactRootTags';
29
30 +import ReactSharedInternals from 'shared/ReactSharedInternals';
31 +import enqueueTask from 'shared/enqueueTask';
32 +const {IsSomeRendererActing} = ReactSharedInternals;
33 +
34 type Container = {
35 rootID: string,
36 children: Array<Instance | TextInstance>,
@@ -958,7 +962,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
962
963 flushPassiveEffects: NoopRenderer.flushPassiveEffects,
964
961 - act: NoopRenderer.act,
965 + act: noopAct,
966
967 // Logs the current state of the tree.
968 dumpTree(rootID: string = DEFAULT_ROOT_ID) {
@@ -1073,6 +1077,120 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
1077 },
1078 };
1079
1080 + // This version of `act` is only used by our tests. Unlike the public version
1081 + // of `act`, it's designed to work identically in both production and
1082 + // development. It may have slightly different behavior from the public
1083 + // version, too, since our constraints in our test suite are not the same as
1084 + // those of developers using React — we're testing React itself, as opposed to
1085 + // building an app with React.
1086 +
1087 + const {batchedUpdates, IsThisRendererActing} = NoopRenderer;
1088 + let actingUpdatesScopeDepth = 0;
1089 +
1090 + function noopAct(scope: () => Thenable<mixed> | void) {
1091 + if (Scheduler.unstable_flushAllWithoutAsserting === undefined) {
1092 + throw Error(
1093 + 'This version of `act` requires a special mock build of Scheduler.',
1094 + );
1095 + }
1096 + if (setTimeout._isMockFunction !== true) {
1097 + throw Error(
1098 + "This version of `act` requires Jest's timer mocks " +
1099 + '(i.e. jest.useFakeTimers).',
1100 + );
1101 + }
1102 +
1103 + const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
1104 + const previousIsSomeRendererActing = IsSomeRendererActing.current;
1105 + const previousIsThisRendererActing = IsThisRendererActing.current;
1106 + IsSomeRendererActing.current = true;
1107 + IsThisRendererActing.current = true;
1108 + actingUpdatesScopeDepth++;
1109 +
1110 + const unwind = () => {
1111 + actingUpdatesScopeDepth--;
1112 + IsSomeRendererActing.current = previousIsSomeRendererActing;
1113 + IsThisRendererActing.current = previousIsThisRendererActing;
1114 +
1115 + if (__DEV__) {
1116 + if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
1117 + // if it's _less than_ previousActingUpdatesScopeDepth, then we can
1118 + // assume the 'other' one has warned
1119 + console.error(
1120 + 'You seem to have overlapping act() calls, this is not supported. ' +
1121 + 'Be sure to await previous act() calls before making a new one. ',
1122 + );
1123 + }
1124 + }
1125 + };
1126 +
1127 + // TODO: This would be way simpler if 1) we required a promise to be
1128 + // returned and 2) we could use async/await. Since it's only our used in
1129 + // our test suite, we should be able to.
1130 + try {
1131 + const thenable = batchedUpdates(scope);
1132 + if (
1133 + typeof thenable === 'object' &&
1134 + thenable !== null &&
1135 + typeof thenable.then === 'function'
1136 + ) {
1137 + return {
1138 + then(resolve: () => void, reject: (error: mixed) => void) {
1139 + thenable.then(
1140 + () => {
1141 + flushActWork(
1142 + () => {
1143 + unwind();
1144 + resolve();
1145 + },
1146 + error => {
1147 + unwind();
1148 + reject(error);
1149 + },
1150 + );
1151 + },
1152 + error => {
1153 + unwind();
1154 + reject(error);
1155 + },
1156 + );
1157 + },
1158 + };
1159 + } else {
1160 + try {
1161 + // TODO: Let's not support non-async scopes at all in our tests. Need to
1162 + // migrate existing tests.
1163 + let didFlushWork;
1164 + do {
1165 + didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
1166 + } while (didFlushWork);
1167 + } finally {
1168 + unwind();
1169 + }
1170 + }
1171 + } catch (error) {
1172 + unwind();
1173 + throw error;
1174 + }
1175 + }
1176 +
1177 + function flushActWork(resolve, reject) {
1178 + // TODO: Run timers to flush suspended fallbacks
1179 + // jest.runOnlyPendingTimers();
1180 + enqueueTask(() => {
1181 + try {
1182 + const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
1183 + if (didFlushWork) {
1184 + flushActWork(resolve, reject);
1185 + } else {
1186 + resolve();
1187 + }
1188 + } catch (error) {
1189 + reject(error);
1190 + }
1191 + });
1192 + }
1193 +
1194 return ReactNoop;
1195 }
1196
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+2 -13
@@ -3622,20 +3622,9 @@ function finishPendingInteractions(root, committedLanes) {
3622 let isFlushingAct = false;
3623 let isInsideThisAct = false;
3624
3625 -// TODO: Yes, this is confusing. See above comment. We'll refactor it.
3625 function shouldForceFlushFallbacksInDEV() {
3627 - if (!__DEV__) {
3628 - // Never force flush in production. This function should get stripped out.
3629 - return false;
3630 - }
3631 - // `IsThisRendererActing.current` is used by ReactTestUtils version of `act`.
3632 - if (IsThisRendererActing.current) {
3633 - // `isInsideAct` is only used by the reconciler implementation of `act`.
3634 - // We don't want to flush suspense fallbacks until the end.
3635 - return !isInsideThisAct;
3636 - }
3637 - // Flush callbacks at the end.
3638 - return isFlushingAct;
3626 + // Never force flush in production. This function should get stripped out.
3627 + return __DEV__ && actingUpdatesScopeDepth > 0;
3628 }
3629
3630 const flushMockScheduler = Scheduler.unstable_flushAllWithoutAsserting;
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+2 -13
@@ -3579,20 +3579,9 @@ function finishPendingInteractions(root, committedLanes) {
3579 let isFlushingAct = false;
3580 let isInsideThisAct = false;
3581
3582 -// TODO: Yes, this is confusing. See above comment. We'll refactor it.
3582 function shouldForceFlushFallbacksInDEV() {
3584 - if (!__DEV__) {
3585 - // Never force flush in production. This function should get stripped out.
3586 - return false;
3587 - }
3588 - // `IsThisRendererActing.current` is used by ReactTestUtils version of `act`.
3589 - if (IsThisRendererActing.current) {
3590 - // `isInsideAct` is only used by the reconciler implementation of `act`.
3591 - // We don't want to flush suspense fallbacks until the end.
3592 - return !isInsideThisAct;
3593 - }
3594 - // Flush callbacks at the end.
3595 - return isFlushingAct;
3583 + // Never force flush in production. This function should get stripped out.
3584 + return __DEV__ && actingUpdatesScopeDepth > 0;
3585 }
3586
3587 const flushMockScheduler = Scheduler.unstable_flushAllWithoutAsserting;
packages/react-reconciler/src/SchedulerWithReactIntegration.new.js
+5 -2
@@ -165,13 +165,13 @@ export function cancelCallback(callbackNode: mixed) {
165 }
166 }
167
168 -export function flushSyncCallbackQueue() {
168 +export function flushSyncCallbackQueue(): boolean {
169 if (immediateQueueCallbackNode !== null) {
170 const node = immediateQueueCallbackNode;
171 immediateQueueCallbackNode = null;
172 Scheduler_cancelCallback(node);
173 }
174 - flushSyncCallbackQueueImpl();
174 + return flushSyncCallbackQueueImpl();
175 }
176
177 function flushSyncCallbackQueueImpl() {
@@ -237,5 +237,8 @@ function flushSyncCallbackQueueImpl() {
237 isFlushingSyncQueue = false;
238 }
239 }
240 + return true;
241 + } else {
242 + return false;
243 }
244 }
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+5 -5
@@ -31,7 +31,7 @@ describe('ReactHooks', () => {
31 ReactTestRenderer = require('react-test-renderer');
32 Scheduler = require('scheduler');
33 ReactDOMServer = require('react-dom/server');
34 - act = ReactTestRenderer.act;
34 + act = ReactTestRenderer.unstable_concurrentAct;
35 });
36
37 if (__DEV__) {
@@ -782,7 +782,7 @@ describe('ReactHooks', () => {
782 }
783
784 const root = ReactTestRenderer.create(null);
785 - ReactTestRenderer.act(() => {
785 + act(() => {
786 root.update(<Counter />);
787 });
788 expect(root).toMatchRenderedOutput('4');
@@ -806,7 +806,7 @@ describe('ReactHooks', () => {
806 }
807
808 const root = ReactTestRenderer.create(null);
809 - ReactTestRenderer.act(() => {
809 + act(() => {
810 root.update(<Counter />);
811 });
812 expect(root).toMatchRenderedOutput('4');
@@ -829,7 +829,7 @@ describe('ReactHooks', () => {
829 }
830
831 const root = ReactTestRenderer.create(null);
832 - ReactTestRenderer.act(() => {
832 + act(() => {
833 root.update(<Counter />);
834 });
835 expect(root).toMatchRenderedOutput('4');
@@ -1830,7 +1830,7 @@ describe('ReactHooks', () => {
1830 return null;
1831 }
1832
1833 - ReactTestRenderer.act(() => {
1833 + act(() => {
1834 ReactTestRenderer.create(<A />);
1835 });
1836
packages/react-reconciler/src/__tests__/ReactNoopRendererAct-test.js
-1
@@ -9,7 +9,6 @@
9
10 // sanity tests for ReactNoop.act()
11
12 -jest.useRealTimers();
12 const React = require('react');
13 const ReactNoop = require('react-noop-renderer');
14 const Scheduler = require('scheduler');
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+2 -2
@@ -21,7 +21,7 @@ describe('ReactSuspense', () => {
21 ReactFeatureFlags.enableSchedulerTracing = true;
22 React = require('react');
23 ReactTestRenderer = require('react-test-renderer');
24 - act = ReactTestRenderer.act;
24 + act = ReactTestRenderer.unstable_concurrentAct;
25 Scheduler = require('scheduler');
26 SchedulerTracing = require('scheduler/tracing');
27 ReactCache = require('react-cache');
@@ -440,7 +440,7 @@ describe('ReactSuspense', () => {
440 unstable_isConcurrent: true,
441 });
442
443 - await ReactTestRenderer.act(async () => {
443 + await act(async () => {
444 root.update(<App />);
445 expect(Scheduler).toFlushAndYield([
446 'shouldHideInParent: false',
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+1 -2
@@ -3846,8 +3846,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3846 await ReactNoop.act(async () => {
3847 await resolveText('b');
3848 });
3849 - expect(Scheduler).toHaveYielded(['Promise resolved [b]']);
3850 - expect(Scheduler).toFlushAndYield(['b']);
3849 + expect(Scheduler).toHaveYielded(['Promise resolved [b]', 'b']);
3850 // The bug was that the pending state got stuck forever.
3851 expect(root).toMatchRenderedOutput(<span prop="b" />);
3852 });
packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
+2 -2
@@ -447,7 +447,7 @@ describe('SchedulingProfiler', () => {
447 return didMount;
448 }
449
450 - ReactTestRenderer.act(() => {
450 + ReactTestRenderer.unstable_concurrentAct(() => {
451 ReactTestRenderer.create(<Example />, {unstable_isConcurrent: true});
452 });
453
@@ -480,7 +480,7 @@ describe('SchedulingProfiler', () => {
480 return didRender;
481 }
482
483 - ReactTestRenderer.act(() => {
483 + ReactTestRenderer.unstable_concurrentAct(() => {
484 ReactTestRenderer.create(<Example />, {unstable_isConcurrent: true});
485 });
486
packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
+1 -1
@@ -26,7 +26,7 @@ describe('useMutableSourceHydration', () => {
26 ReactDOMServer = require('react-dom/server');
27 Scheduler = require('scheduler');
28
29 - act = require('react-dom/test-utils').act;
29 + act = require('react-dom/test-utils').unstable_concurrentAct;
30 createMutableSource = React.unstable_createMutableSource;
31 useMutableSource = React.unstable_useMutableSource;
32 });
packages/react-refresh/src/__tests__/ReactFresh-test.js
+2 -2
@@ -29,7 +29,7 @@ describe('ReactFresh', () => {
29 ReactFreshRuntime.injectIntoGlobalHook(global);
30 ReactDOM = require('react-dom');
31 Scheduler = require('scheduler');
32 - act = require('react-dom/test-utils').act;
32 + act = require('react-dom/test-utils').unstable_concurrentAct;
33 createReactClass = require('create-react-class/factory')(
34 React.Component,
35 React.isValidElement,
@@ -3748,7 +3748,7 @@ describe('ReactFresh', () => {
3748 React = require('react');
3749 ReactDOM = require('react-dom');
3750 Scheduler = require('scheduler');
3751 - act = require('react-dom/test-utils').act;
3751 + act = require('react-dom/test-utils').unstable_concurrentAct;
3752
3753 // Important! Inject into the global hook *after* ReactDOM runs:
3754 ReactFreshRuntime = require('react-refresh/runtime');
packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
+1 -1
@@ -30,7 +30,7 @@ describe('ReactFreshIntegration', () => {
30 ReactFreshRuntime = require('react-refresh/runtime');
31 ReactFreshRuntime.injectIntoGlobalHook(global);
32 ReactDOM = require('react-dom');
33 - act = require('react-dom/test-utils').act;
33 + act = require('react-dom/test-utils').unstable_concurrentAct;
34 container = document.createElement('div');
35 document.body.appendChild(container);
36 exportsObj = undefined;
packages/react-test-renderer/src/ReactTestRenderer.js
+120
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +import type {Thenable} from 'shared/ReactTypes';
11 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
12 import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
13 import type {Instance, TextInstance} from './ReactTestHostConfig';
@@ -20,6 +21,7 @@ import {
21 injectIntoDevTools,
22 batchedUpdates,
23 act,
24 + IsThisRendererActing,
25 } from 'react-reconciler/src/ReactFiberReconciler';
26 import {findCurrentFiberUsingSlowPath} from 'react-reconciler/src/ReactFiberTreeReflection';
27 import {
@@ -44,10 +46,14 @@ import {
46 import invariant from 'shared/invariant';
47 import getComponentName from 'shared/getComponentName';
48 import ReactVersion from 'shared/ReactVersion';
49 +import ReactSharedInternals from 'shared/ReactSharedInternals';
50 +import enqueueTask from 'shared/enqueueTask';
51
52 import {getPublicInstance} from './ReactTestHostConfig';
53 import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
54
55 +const {IsSomeRendererActing} = ReactSharedInternals;
56 +
57 type TestRendererOptions = {
58 createNodeMock: (element: React$Element<any>) => any,
59 unstable_isConcurrent: boolean,
@@ -581,10 +587,124 @@ injectIntoDevTools({
587 rendererPackageName: 'react-test-renderer',
588 });
589
590 +let actingUpdatesScopeDepth = 0;
591 +
592 +// This version of `act` is only used by our tests. Unlike the public version
593 +// of `act`, it's designed to work identically in both production and
594 +// development. It may have slightly different behavior from the public
595 +// version, too, since our constraints in our test suite are not the same as
596 +// those of developers using React — we're testing React itself, as opposed to
597 +// building an app with React.
598 +// TODO: Migrate our tests to use ReactNoop. Although we would need to figure
599 +// out a solution for Relay, which has some Concurrent Mode tests.
600 +function unstable_concurrentAct(scope: () => Thenable<mixed> | void) {
601 + if (Scheduler.unstable_flushAllWithoutAsserting === undefined) {
602 + throw Error(
603 + 'This version of `act` requires a special mock build of Scheduler.',
604 + );
605 + }
606 + if (setTimeout._isMockFunction !== true) {
607 + throw Error(
608 + "This version of `act` requires Jest's timer mocks " +
609 + '(i.e. jest.useFakeTimers).',
610 + );
611 + }
612 +
613 + const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
614 + const previousIsSomeRendererActing = IsSomeRendererActing.current;
615 + const previousIsThisRendererActing = IsThisRendererActing.current;
616 + IsSomeRendererActing.current = true;
617 + IsThisRendererActing.current = true;
618 + actingUpdatesScopeDepth++;
619 +
620 + const unwind = () => {
621 + actingUpdatesScopeDepth--;
622 + IsSomeRendererActing.current = previousIsSomeRendererActing;
623 + IsThisRendererActing.current = previousIsThisRendererActing;
624 + if (__DEV__) {
625 + if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
626 + // if it's _less than_ previousActingUpdatesScopeDepth, then we can
627 + // assume the 'other' one has warned
628 + console.error(
629 + 'You seem to have overlapping act() calls, this is not supported. ' +
630 + 'Be sure to await previous act() calls before making a new one. ',
631 + );
632 + }
633 + }
634 + };
635 +
636 + // TODO: This would be way simpler if 1) we required a promise to be
637 + // returned and 2) we could use async/await. Since it's only our used in
638 + // our test suite, we should be able to.
639 + try {
640 + const thenable = batchedUpdates(scope);
641 + if (
642 + typeof thenable === 'object' &&
643 + thenable !== null &&
644 + typeof thenable.then === 'function'
645 + ) {
646 + return {
647 + then(resolve: () => void, reject: (error: mixed) => void) {
648 + thenable.then(
649 + () => {
650 + flushActWork(
651 + () => {
652 + unwind();
653 + resolve();
654 + },
655 + error => {
656 + unwind();
657 + reject(error);
658 + },
659 + );
660 + },
661 + error => {
662 + unwind();
663 + reject(error);
664 + },
665 + );
666 + },
667 + };
668 + } else {
669 + try {
670 + // TODO: Let's not support non-async scopes at all in our tests. Need to
671 + // migrate existing tests.
672 + let didFlushWork;
673 + do {
674 + didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
675 + } while (didFlushWork);
676 + } finally {
677 + unwind();
678 + }
679 + }
680 + } catch (error) {
681 + unwind();
682 + throw error;
683 + }
684 +}
685 +
686 +function flushActWork(resolve, reject) {
687 + // TODO: Run timers to flush suspended fallbacks
688 + // jest.runOnlyPendingTimers();
689 + enqueueTask(() => {
690 + try {
691 + const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
692 + if (didFlushWork) {
693 + flushActWork(resolve, reject);
694 + } else {
695 + resolve();
696 + }
697 + } catch (error) {
698 + reject(error);
699 + }
700 + });
701 +}
702 +
703 export {
704 Scheduler as _Scheduler,
705 create,
706 /* eslint-disable-next-line camelcase */
707 batchedUpdates as unstable_batchedUpdates,
708 act,
709 + unstable_concurrentAct,
710 };
packages/react-transport-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js
+1 -1
@@ -18,7 +18,7 @@ describe('ReactFlightDOMRelay', () => {
18 beforeEach(() => {
19 jest.resetModules();
20
21 - act = require('react-dom/test-utils').act;
21 + act = require('react-dom/test-utils').unstable_concurrentAct;
22 React = require('react');
23 ReactDOM = require('react-dom');
24 ReactDOMFlightRelayServer = require('react-transport-dom-relay/server');
packages/react-transport-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+1 -1
@@ -37,7 +37,7 @@ describe('ReactFlightDOM', () => {
37 jest.resetModules();
38 webpackModules = {};
39 webpackMap = {};
40 - act = require('react-dom/test-utils').act;
40 + act = require('react-dom/test-utils').unstable_concurrentAct;
41 Stream = require('stream');
42 React = require('react');
43 ReactDOM = require('react-dom');
packages/react/src/__tests__/ReactDOMTracing-test.internal.js
+9 -6
@@ -16,6 +16,7 @@ let ReactFeatureFlags;
16 let Scheduler;
17 let SchedulerTracing;
18 let TestUtils;
19 +let act;
20 let onInteractionScheduledWorkCompleted;
21 let onInteractionTraced;
22 let onWorkCanceled;
@@ -42,6 +43,8 @@ function loadModules() {
43 SchedulerTracing = require('scheduler/tracing');
44 TestUtils = require('react-dom/test-utils');
45
46 + act = TestUtils.unstable_concurrentAct;
47 +
48 onInteractionScheduledWorkCompleted = jest.fn();
49 onInteractionTraced = jest.fn();
50 onWorkCanceled = jest.fn();
@@ -118,7 +121,7 @@ describe('ReactDOMTracing', () => {
121 const root = ReactDOM.createRoot(container);
122 SchedulerTracing.unstable_trace('initialization', 0, () => {
123 interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
121 - TestUtils.act(() => {
124 + act(() => {
125 root.render(
126 <React.Profiler id="test" onRender={onRender}>
127 <App />
@@ -190,7 +193,7 @@ describe('ReactDOMTracing', () => {
193 SchedulerTracing.unstable_trace('initialization', 0, () => {
194 interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
195
193 - TestUtils.act(() => {
196 + act(() => {
197 root.render(
198 <React.Profiler id="test" onRender={onRender}>
199 <App />
@@ -269,7 +272,7 @@ describe('ReactDOMTracing', () => {
272 const root = ReactDOM.createRoot(container);
273 SchedulerTracing.unstable_trace('initialization', 0, () => {
274 interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
272 - TestUtils.act(() => {
275 + act(() => {
276 root.render(
277 <React.Profiler id="test" onRender={onRender}>
278 <App />
@@ -364,7 +367,7 @@ describe('ReactDOMTracing', () => {
367 const root = ReactDOM.createRoot(container);
368
369 // Schedule some idle work without any interactions.
367 - TestUtils.act(() => {
370 + act(() => {
371 root.render(
372 <React.Profiler id="test" onRender={onRender}>
373 <App />
@@ -468,7 +471,7 @@ describe('ReactDOMTracing', () => {
471 const container = document.createElement('div');
472 const root = ReactDOM.createRoot(container);
473
471 - TestUtils.act(() => {
474 + act(() => {
475 root.render(
476 <React.Profiler id="test" onRender={onRender}>
477 <App />
@@ -568,7 +571,7 @@ describe('ReactDOMTracing', () => {
571
572 let interaction;
573
571 - TestUtils.act(() => {
574 + act(() => {
575 SchedulerTracing.unstable_trace('initialization', 0, () => {
576 interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
577 // This render is only CPU bound. Nothing suspends.
packages/react/src/__tests__/ReactProfiler-test.internal.js
+21 -18
@@ -16,6 +16,7 @@ let ReactNoop;
16 let Scheduler;
17 let ReactCache;
18 let ReactTestRenderer;
19 +let ReactTestRendererAct;
20 let SchedulerTracing;
21 let AdvanceTime;
22 let AsyncText;
@@ -45,9 +46,11 @@ function loadModules({
46 if (useNoopRenderer) {
47 ReactNoop = require('react-noop-renderer');
48 ReactTestRenderer = null;
49 + ReactTestRendererAct = null;
50 } else {
51 ReactNoop = null;
52 ReactTestRenderer = require('react-test-renderer');
53 + ReactTestRendererAct = ReactTestRenderer.unstable_concurrentAct;
54 }
55
56 AdvanceTime = class extends React.Component {
@@ -374,7 +377,7 @@ describe('Profiler', () => {
377 Scheduler.unstable_advanceTime(20); // 30 -> 50
378
379 // Updating a sibling should not report a re-render.
377 - ReactTestRenderer.act(updateProfilerSibling);
380 + ReactTestRendererAct(updateProfilerSibling);
381
382 expect(callback).not.toHaveBeenCalled();
383 });
@@ -1495,7 +1498,7 @@ describe('Profiler', () => {
1498 const setCountRef = React.createRef(null);
1499
1500 let renderer = null;
1498 - ReactTestRenderer.act(() => {
1501 + ReactTestRendererAct(() => {
1502 renderer = ReactTestRenderer.create(
1503 <React.Profiler id="root-mount" onCommit={callback}>
1504 <React.Profiler id="a">
@@ -1523,7 +1526,7 @@ describe('Profiler', () => {
1526 expect(call[3]).toBe(2); // commit start time (before mutations or effects)
1527 expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
1528
1526 - ReactTestRenderer.act(() => setCountRef.current(count => count + 1));
1529 + ReactTestRendererAct(() => setCountRef.current(count => count + 1));
1530
1531 expect(callback).toHaveBeenCalledTimes(2);
1532
@@ -1536,7 +1539,7 @@ describe('Profiler', () => {
1539 expect(call[3]).toBe(1013); // commit start time (before mutations or effects)
1540 expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
1541
1539 - ReactTestRenderer.act(() => {
1542 + ReactTestRendererAct(() => {
1543 renderer.update(
1544 <React.Profiler id="root-update" onCommit={callback}>
1545 <React.Profiler id="b">
@@ -1596,7 +1599,7 @@ describe('Profiler', () => {
1599
1600 // Test an error that happens during an effect
1601
1599 - ReactTestRenderer.act(() => {
1602 + ReactTestRendererAct(() => {
1603 ReactTestRenderer.create(
1604 <React.Profiler id="root" onCommit={callback}>
1605 <ErrorBoundary
@@ -1684,7 +1687,7 @@ describe('Profiler', () => {
1687
1688 let renderer = null;
1689
1687 - ReactTestRenderer.act(() => {
1690 + ReactTestRendererAct(() => {
1691 renderer = ReactTestRenderer.create(
1692 <React.Profiler id="root" onCommit={callback}>
1693 <ErrorBoundary
@@ -1727,7 +1730,7 @@ describe('Profiler', () => {
1730
1731 // Test an error that happens during an cleanup function
1732
1730 - ReactTestRenderer.act(() => {
1733 + ReactTestRendererAct(() => {
1734 renderer.update(
1735 <React.Profiler id="root" onCommit={callback}>
1736 <ErrorBoundary
@@ -1875,7 +1878,7 @@ describe('Profiler', () => {
1878 Scheduler.unstable_advanceTime(1);
1879
1880 let renderer;
1878 - ReactTestRenderer.act(() => {
1881 + ReactTestRendererAct(() => {
1882 renderer = ReactTestRenderer.create(
1883 <React.Profiler id="mount-test" onPostCommit={callback}>
1884 <ComponentWithEffects />
@@ -1897,7 +1900,7 @@ describe('Profiler', () => {
1900
1901 Scheduler.unstable_advanceTime(1);
1902
1900 - ReactTestRenderer.act(() => {
1903 + ReactTestRendererAct(() => {
1904 renderer.update(
1905 <React.Profiler id="update-test" onPostCommit={callback}>
1906 <ComponentWithEffects />
@@ -1919,7 +1922,7 @@ describe('Profiler', () => {
1922
1923 Scheduler.unstable_advanceTime(1);
1924
1922 - ReactTestRenderer.act(() => {
1925 + ReactTestRendererAct(() => {
1926 renderer.update(
1927 <React.Profiler id="unmount-test" onPostCommit={callback} />,
1928 );
@@ -1961,7 +1964,7 @@ describe('Profiler', () => {
1964
1965 Scheduler.unstable_advanceTime(1);
1966
1964 - ReactTestRenderer.act(() => {
1967 + ReactTestRendererAct(() => {
1968 ReactTestRenderer.create(
1969 <React.Profiler id="mount-test" onPostCommit={callback}>
1970 <ComponentWithEffects />
@@ -2015,7 +2018,7 @@ describe('Profiler', () => {
2018 const setCountRef = React.createRef(null);
2019
2020 let renderer = null;
2018 - ReactTestRenderer.act(() => {
2021 + ReactTestRendererAct(() => {
2022 renderer = ReactTestRenderer.create(
2023 <React.Profiler id="root-mount" onPostCommit={callback}>
2024 <React.Profiler id="a">
@@ -2043,7 +2046,7 @@ describe('Profiler', () => {
2046 expect(call[3]).toBe(2); // commit start time (before mutations or effects)
2047 expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
2048
2046 - ReactTestRenderer.act(() => setCountRef.current(count => count + 1));
2049 + ReactTestRendererAct(() => setCountRef.current(count => count + 1));
2050
2051 expect(callback).toHaveBeenCalledTimes(2);
2052
@@ -2056,7 +2059,7 @@ describe('Profiler', () => {
2059 expect(call[3]).toBe(1013); // commit start time (before mutations or effects)
2060 expect(call[4]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
2061
2059 - ReactTestRenderer.act(() => {
2062 + ReactTestRendererAct(() => {
2063 renderer.update(
2064 <React.Profiler id="root-update" onPostCommit={callback}>
2065 <React.Profiler id="b">
@@ -2116,7 +2119,7 @@ describe('Profiler', () => {
2119
2120 // Test an error that happens during an effect
2121
2119 - ReactTestRenderer.act(() => {
2122 + ReactTestRendererAct(() => {
2123 ReactTestRenderer.create(
2124 <React.Profiler id="root" onPostCommit={callback}>
2125 <ErrorBoundary
@@ -2205,7 +2208,7 @@ describe('Profiler', () => {
2208
2209 let renderer = null;
2210
2208 - ReactTestRenderer.act(() => {
2211 + ReactTestRendererAct(() => {
2212 renderer = ReactTestRenderer.create(
2213 <React.Profiler id="root" onPostCommit={callback}>
2214 <ErrorBoundary
@@ -2248,7 +2251,7 @@ describe('Profiler', () => {
2251
2252 // Test an error that happens during an cleanup function
2253
2251 - ReactTestRenderer.act(() => {
2254 + ReactTestRendererAct(() => {
2255 renderer.update(
2256 <React.Profiler id="root" onPostCommit={callback}>
2257 <ErrorBoundary
@@ -2327,7 +2330,7 @@ describe('Profiler', () => {
2330
2331 Scheduler.unstable_advanceTime(1);
2332
2330 - ReactTestRenderer.act(() => {
2333 + ReactTestRendererAct(() => {
2334 SchedulerTracing.unstable_trace(
2335 interaction.name,
2336 interaction.timestamp,
packages/use-subscription/src/__tests__/useSubscription-test.js
+1 -1
@@ -27,7 +27,7 @@ describe('useSubscription', () => {
27 ReactTestRenderer = require('react-test-renderer');
28 Scheduler = require('scheduler');
29
30 - act = ReactTestRenderer.act;
30 + act = ReactTestRenderer.unstable_concurrentAct;
31
32 BehaviorSubject = require('rxjs').BehaviorSubject;
33 ReplaySubject = require('rxjs').ReplaySubject;