Skip to content

Commit d17086c

Browse files
authored
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.
1 parent d38ec17 commit d17086c

File tree

36 files changed

+465
-298
lines changed

36 files changed

+465
-298
lines changed

packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ describe('ReactHooksInspectionIntegration', () => {
2222
React = require('react');
2323
ReactTestRenderer = require('react-test-renderer');
2424
Scheduler = require('scheduler');
25-
act = ReactTestRenderer.act;
25+
act = ReactTestRenderer.unstable_concurrentAct;
2626
ReactDebugTools = require('react-debug-tools');
2727
});
2828

packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js

+25-20
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ describe('InspectedElementContext', () => {
3333
let TestUtils;
3434
let TreeContextController;
3535

36+
let TestUtilsAct;
37+
let TestRendererAct;
38+
3639
beforeEach(() => {
3740
utils = require('./utils');
3841
utils.beforeEachProfiling();
@@ -47,7 +50,9 @@ describe('InspectedElementContext', () => {
4750
ReactDOM = require('react-dom');
4851
PropTypes = require('prop-types');
4952
TestUtils = require('react-dom/test-utils');
53+
TestUtilsAct = TestUtils.unstable_concurrentAct;
5054
TestRenderer = utils.requireTestRenderer();
55+
TestRendererAct = TestUtils.unstable_concurrentAct;
5156

5257
BridgeContext = require('react-devtools-shared/src/devtools/views/context')
5358
.BridgeContext;
@@ -999,8 +1004,8 @@ describe('InspectedElementContext', () => {
9991004
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
10001005

10011006
inspectedElement = null;
1002-
TestUtils.act(() => {
1003-
TestRenderer.act(() => {
1007+
TestUtilsAct(() => {
1008+
TestRendererAct(() => {
10041009
getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
10051010
jest.runOnlyPendingTimers();
10061011
});
@@ -1009,8 +1014,8 @@ describe('InspectedElementContext', () => {
10091014
expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
10101015

10111016
inspectedElement = null;
1012-
TestUtils.act(() => {
1013-
TestRenderer.act(() => {
1017+
TestUtilsAct(() => {
1018+
TestRendererAct(() => {
10141019
getInspectedElementPath(id, ['props', 'nestedObject', 'a', 'b', 'c']);
10151020
jest.runOnlyPendingTimers();
10161021
});
@@ -1021,8 +1026,8 @@ describe('InspectedElementContext', () => {
10211026
);
10221027

10231028
inspectedElement = null;
1024-
TestUtils.act(() => {
1025-
TestRenderer.act(() => {
1029+
TestUtilsAct(() => {
1030+
TestRendererAct(() => {
10261031
getInspectedElementPath(id, [
10271032
'props',
10281033
'nestedObject',
@@ -1041,8 +1046,8 @@ describe('InspectedElementContext', () => {
10411046
);
10421047

10431048
inspectedElement = null;
1044-
TestUtils.act(() => {
1045-
TestRenderer.act(() => {
1049+
TestUtilsAct(() => {
1050+
TestRendererAct(() => {
10461051
getInspectedElementPath(id, ['hooks', 0, 'value']);
10471052
jest.runOnlyPendingTimers();
10481053
});
@@ -1051,8 +1056,8 @@ describe('InspectedElementContext', () => {
10511056
expect(inspectedElement).toMatchSnapshot('5: Inspect hooks.0.value');
10521057

10531058
inspectedElement = null;
1054-
TestUtils.act(() => {
1055-
TestRenderer.act(() => {
1059+
TestUtilsAct(() => {
1060+
TestRendererAct(() => {
10561061
getInspectedElementPath(id, ['hooks', 0, 'value', 'foo', 'bar']);
10571062
jest.runOnlyPendingTimers();
10581063
});
@@ -1108,8 +1113,8 @@ describe('InspectedElementContext', () => {
11081113
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
11091114

11101115
inspectedElement = null;
1111-
TestUtils.act(() => {
1112-
TestRenderer.act(() => {
1116+
TestUtilsAct(() => {
1117+
TestRendererAct(() => {
11131118
getInspectedElementPath(id, ['props', 'set_of_sets', 0]);
11141119
jest.runOnlyPendingTimers();
11151120
});
@@ -1179,23 +1184,23 @@ describe('InspectedElementContext', () => {
11791184
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
11801185

11811186
inspectedElement = null;
1182-
TestRenderer.act(() => {
1187+
TestRendererAct(() => {
11831188
getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
11841189
jest.runOnlyPendingTimers();
11851190
});
11861191
expect(inspectedElement).not.toBeNull();
11871192
expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
11881193

11891194
inspectedElement = null;
1190-
TestRenderer.act(() => {
1195+
TestRendererAct(() => {
11911196
getInspectedElementPath(id, ['props', 'nestedObject', 'c']);
11921197
jest.runOnlyPendingTimers();
11931198
});
11941199
expect(inspectedElement).not.toBeNull();
11951200
expect(inspectedElement).toMatchSnapshot('3: Inspect props.nestedObject.c');
11961201

1197-
TestRenderer.act(() => {
1198-
TestUtils.act(() => {
1202+
TestRendererAct(() => {
1203+
TestUtilsAct(() => {
11991204
ReactDOM.render(
12001205
<Example
12011206
nestedObject={{
@@ -1221,7 +1226,7 @@ describe('InspectedElementContext', () => {
12211226
});
12221227
});
12231228

1224-
TestRenderer.act(() => {
1229+
TestRendererAct(() => {
12251230
inspectedElement = null;
12261231
jest.advanceTimersByTime(1000);
12271232
});
@@ -1281,7 +1286,7 @@ describe('InspectedElementContext', () => {
12811286
expect(inspectedElement).not.toBeNull();
12821287
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
12831288

1284-
TestUtils.act(() => {
1289+
TestUtilsAct(() => {
12851290
ReactDOM.render(
12861291
<Example
12871292
nestedObject={{
@@ -1300,8 +1305,8 @@ describe('InspectedElementContext', () => {
13001305

13011306
inspectedElement = null;
13021307

1303-
TestRenderer.act(() => {
1304-
TestUtils.act(() => {
1308+
TestRendererAct(() => {
1309+
TestUtilsAct(() => {
13051310
getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
13061311
jest.runOnlyPendingTimers();
13071312
});

packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('Store component filters', () => {
2020
let utils;
2121

2222
const act = (callback: Function) => {
23-
TestUtils.act(() => {
23+
TestUtils.unstable_concurrentAct(() => {
2424
callback();
2525
});
2626
jest.runAllTimers(); // Flush Bridge operations

packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('ReactDOMFiberAsync', () => {
2828
container = document.createElement('div');
2929
React = require('react');
3030
ReactDOM = require('react-dom');
31-
act = require('react-dom/test-utils').act;
31+
act = require('react-dom/test-utils').unstable_concurrentAct;
3232
Scheduler = require('scheduler');
3333

3434
document.body.appendChild(container);

packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js

+15-13
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let React;
1717
let ReactDOM;
1818
let ReactDOMServer;
1919
let ReactTestUtils;
20+
let act;
2021
let Scheduler;
2122
let useState;
2223
let useReducer;
@@ -43,6 +44,7 @@ function initModules() {
4344
ReactDOMServer = require('react-dom/server');
4445
ReactTestUtils = require('react-dom/test-utils');
4546
Scheduler = require('scheduler');
47+
act = ReactTestUtils.unstable_concurrentAct;
4648
useState = React.useState;
4749
useReducer = React.useReducer;
4850
useEffect = React.useEffect;
@@ -1063,7 +1065,7 @@ describe('ReactDOMServerHooks', () => {
10631065
expect(domNode.children.length).toEqual(1);
10641066
expect(oldClientId).not.toBeNull();
10651067

1066-
await ReactTestUtils.act(async () => _setShowId(true));
1068+
await act(async () => _setShowId(true));
10671069

10681070
expect(domNode.children.length).toEqual(2);
10691071
expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
@@ -1281,7 +1283,7 @@ describe('ReactDOMServerHooks', () => {
12811283
const oldServerId = container.children[0].children[0].getAttribute('id');
12821284
expect(oldServerId).not.toBeNull();
12831285

1284-
await ReactTestUtils.act(async () => {
1286+
await act(async () => {
12851287
_setShowDiv(true);
12861288
});
12871289
expect(container.children[0].children.length).toEqual(2);
@@ -1322,7 +1324,7 @@ describe('ReactDOMServerHooks', () => {
13221324
const oldServerId = container.children[0].children[0].getAttribute('id');
13231325
expect(oldServerId).not.toBeNull();
13241326

1325-
await ReactTestUtils.act(async () => {
1327+
await act(async () => {
13261328
_setShowDiv(true);
13271329
});
13281330
expect(container.children[0].children.length).toEqual(2);
@@ -1356,12 +1358,12 @@ describe('ReactDOMServerHooks', () => {
13561358
document.body.append(container);
13571359
container.innerHTML = ReactDOMServer.renderToString(<App />);
13581360
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1359-
ReactTestUtils.act(() => {
1361+
act(() => {
13601362
root.render(<App />);
13611363
});
13621364
expect(Scheduler).toHaveYielded(['App', 'App']);
13631365
// The ID goes from not being used to being added to the page
1364-
ReactTestUtils.act(() => {
1366+
act(() => {
13651367
_setShow(true);
13661368
});
13671369
expect(Scheduler).toHaveYielded(['App', 'App']);
@@ -1391,7 +1393,7 @@ describe('ReactDOMServerHooks', () => {
13911393
ReactDOM.hydrate(<App />, container);
13921394
expect(Scheduler).toHaveYielded(['App', 'App']);
13931395
// The ID goes from not being used to being added to the page
1394-
ReactTestUtils.act(() => {
1396+
act(() => {
13951397
_setShow(true);
13961398
});
13971399
expect(Scheduler).toHaveYielded(['App']);
@@ -1418,12 +1420,12 @@ describe('ReactDOMServerHooks', () => {
14181420
document.body.append(container);
14191421
container.innerHTML = ReactDOMServer.renderToString(<App />);
14201422
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1421-
ReactTestUtils.act(() => {
1423+
act(() => {
14221424
root.render(<App />);
14231425
});
14241426

14251427
// The ID goes from not being used to being added to the page
1426-
ReactTestUtils.act(() => {
1428+
act(() => {
14271429
ReactDOM.flushSync(() => {
14281430
_setShow(true);
14291431
});
@@ -1518,7 +1520,7 @@ describe('ReactDOMServerHooks', () => {
15181520
expect(child1Ref.current).toBe(null);
15191521
expect(Scheduler).toHaveYielded([]);
15201522

1521-
ReactTestUtils.act(() => {
1523+
act(() => {
15221524
_setShow(true);
15231525

15241526
// State update should trigger the ID to update, which changes the props
@@ -1603,7 +1605,7 @@ describe('ReactDOMServerHooks', () => {
16031605

16041606
suspend = true;
16051607
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1606-
await ReactTestUtils.act(async () => {
1608+
await act(async () => {
16071609
root.render(<App />);
16081610
});
16091611
jest.runAllTimers();
@@ -1616,7 +1618,7 @@ describe('ReactDOMServerHooks', () => {
16161618
container.children[0].children[0].getAttribute('id'),
16171619
).not.toBeNull();
16181620

1619-
await ReactTestUtils.act(async () => {
1621+
await act(async () => {
16201622
suspend = false;
16211623
resolve();
16221624
await promise;
@@ -1703,7 +1705,7 @@ describe('ReactDOMServerHooks', () => {
17031705

17041706
suspend = false;
17051707
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1706-
await ReactTestUtils.act(async () => {
1708+
await act(async () => {
17071709
root.render(<App />);
17081710
});
17091711
jest.runAllTimers();
@@ -1968,7 +1970,7 @@ describe('ReactDOMServerHooks', () => {
19681970
expect(Scheduler).toHaveYielded([]);
19691971
expect(Scheduler).toFlushAndYield([]);
19701972

1971-
ReactTestUtils.act(() => {
1973+
act(() => {
19721974
_setShow(false);
19731975
});
19741976

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ describe('ReactDOMServerPartialHydration', () => {
7979

8080
React = require('react');
8181
ReactDOM = require('react-dom');
82-
act = require('react-dom/test-utils').act;
82+
act = require('react-dom/test-utils').unstable_concurrentAct;
8383
ReactDOMServer = require('react-dom/server');
8484
Scheduler = require('scheduler');
8585
Suspense = React.Suspense;

packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let ReactDOMServer;
1717
let ReactTestUtils;
1818
let Scheduler;
1919
let Suspense;
20+
let act;
2021

2122
function dispatchMouseHoverEvent(to, from) {
2223
if (!to) {
@@ -101,6 +102,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
101102
ReactDOM = require('react-dom');
102103
ReactDOMServer = require('react-dom/server');
103104
ReactTestUtils = require('react-dom/test-utils');
105+
act = ReactTestUtils.unstable_concurrentAct;
104106
Scheduler = require('scheduler');
105107
Suspense = React.Suspense;
106108
});
@@ -880,7 +882,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
880882
const spanC = container.getElementsByTagName('span')[4];
881883

882884
const root = ReactDOM.createRoot(container, {hydrate: true});
883-
ReactTestUtils.act(() => {
885+
act(() => {
884886
root.render(<App a="A" />);
885887

886888
// Hydrate the shell.

packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ let React;
1515
let ReactDOM;
1616
let ReactDOMServer;
1717
let ReactTestUtils;
18+
let act;
1819

1920
function initModules() {
2021
// Reset warning cache.
@@ -24,6 +25,7 @@ function initModules() {
2425
ReactDOM = require('react-dom');
2526
ReactDOMServer = require('react-dom/server');
2627
ReactTestUtils = require('react-dom/test-utils');
28+
act = ReactTestUtils.unstable_concurrentAct;
2729

2830
// Make them available to the helpers.
2931
return {
@@ -124,7 +126,7 @@ describe('ReactDOMServerSuspense', () => {
124126
expect(divB.tagName).toBe('DIV');
125127
expect(divB.textContent).toBe('B');
126128

127-
ReactTestUtils.act(() => {
129+
act(() => {
128130
const root = ReactDOM.createBlockingRoot(parent, {hydrate: true});
129131
root.render(example);
130132
});

packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
2828
ReactCache = require('react-cache');
2929
ReactTestUtils = require('react-dom/test-utils');
3030
Scheduler = require('scheduler');
31-
act = ReactTestUtils.act;
31+
act = ReactTestUtils.unstable_concurrentAct;
3232
Suspense = React.Suspense;
3333
container = document.createElement('div');
3434
document.body.appendChild(container);

packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('ReactErrorBoundaries', () => {
4444
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
4545
ReactDOM = require('react-dom');
4646
React = require('react');
47-
act = require('react-dom/test-utils').act;
47+
act = require('react-dom/test-utils').unstable_concurrentAct;
4848
Scheduler = require('scheduler');
4949

5050
BrokenConstructor = class extends React.Component {

packages/react-dom/src/__tests__/ReactUpdates-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe('ReactUpdates', () => {
2121
React = require('react');
2222
ReactDOM = require('react-dom');
2323
ReactTestUtils = require('react-dom/test-utils');
24-
act = ReactTestUtils.act;
24+
act = ReactTestUtils.unstable_concurrentAct;
2525
Scheduler = require('scheduler');
2626
});
2727

packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ module.exports = function(initModules) {
4949
function asyncReactDOMRender(reactElement, domElement, forceHydrate) {
5050
return new Promise(resolve => {
5151
if (forceHydrate) {
52-
ReactTestUtils.act(() => {
52+
ReactTestUtils.unstable_concurrentAct(() => {
5353
ReactDOM.hydrate(reactElement, domElement);
5454
});
5555
} else {
56-
ReactTestUtils.act(() => {
56+
ReactTestUtils.unstable_concurrentAct(() => {
5757
ReactDOM.render(reactElement, domElement);
5858
});
5959
}

0 commit comments

Comments
 (0)