Skip to content

Commit eb9908c

Browse files
committed
Warn when rendering tests in concurrent/batched mode without a mocked scheduler
Concurrent/Batched mode tests should always be run with a mocked scheduler (v17 or not). This PR adds a warning for the same. I'll put up a separate PR to the docs with a page detailing how to mock the scheduler.
1 parent 5b08f7b commit eb9908c

File tree

7 files changed

+216
-111
lines changed

7 files changed

+216
-111
lines changed

fixtures/dom/src/index.test.js

Lines changed: 93 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,7 @@ function App(props) {
2424
return 'hello world';
2525
}
2626

27-
describe('legacy mode', () => {
28-
runTests();
29-
});
30-
31-
describe('mocked scheduler', () => {
32-
beforeEach(() => {
33-
jest.mock('scheduler', () =>
34-
require.requireActual('scheduler/unstable_mock')
35-
);
36-
});
37-
afterEach(() => {
38-
jest.unmock('scheduler');
39-
});
40-
runTests();
41-
});
42-
43-
function runTests() {
27+
describe('wrong act warnings', () => {
4428
beforeEach(() => {
4529
jest.resetModules();
4630
React = require('react');
@@ -111,17 +95,6 @@ function runTests() {
11195
});
11296
});
11397

114-
it('warns when using createRoot() + .render', () => {
115-
const root = ReactDOM.unstable_createRoot(document.createElement('div'));
116-
expect(() => {
117-
TestRenderer.act(() => {
118-
root.render(<App />);
119-
});
120-
}).toWarnDev(["It looks like you're using the wrong act()"], {
121-
withoutStack: true,
122-
});
123-
});
124-
12598
it('warns when using the wrong act version - test + dom: render', () => {
12699
expect(() => {
127100
TestRenderer.act(() => {
@@ -203,31 +176,103 @@ function runTests() {
203176
});
204177
});
205178

206-
it('flushes work only outside the outermost act(), even when nested from different renderers', () => {
207-
const log = [];
208-
function Effecty() {
209-
React.useEffect(() => {
210-
log.push('called');
211-
}, []);
212-
return null;
213-
}
214-
// in legacy mode, this tests whether an act only flushes its own effects
215-
// with a mocked scheduler, this tests whether it flushes all work only on the outermost act
216-
TestRenderer.act(() => {
179+
it('warns when using createRoot() + .render', () => {
180+
const root = ReactDOM.unstable_createRoot(document.createElement('div'));
181+
expect(() => {
182+
TestRenderer.act(() => {
183+
root.render(<App />);
184+
});
185+
}).toWarnDev(
186+
[
187+
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked',
188+
"It looks like you're using the wrong act()",
189+
],
190+
{
191+
withoutStack: true,
192+
}
193+
);
194+
});
195+
});
196+
197+
describe('flush work on nested act', () => {
198+
describe('unmocked scheduler', () => {
199+
beforeEach(() => {
200+
jest.resetModules();
201+
React = require('react');
202+
ReactDOM = require('react-dom');
203+
TestUtils = require('react-dom/test-utils');
204+
TestRenderer = require('react-test-renderer');
205+
});
206+
207+
it('flushes work only outside the outermost act(), even when nested from different renderers', () => {
208+
const log = [];
209+
function Effecty() {
210+
React.useEffect(() => {
211+
log.push('called');
212+
}, []);
213+
return null;
214+
}
215+
// in legacy mode, this tests whether an act only flushes its own effects
216+
TestRenderer.act(() => {
217+
TestUtils.act(() => {
218+
TestRenderer.create(<Effecty />);
219+
});
220+
expect(log).toEqual([]);
221+
});
222+
expect(log).toEqual(['called']);
223+
224+
log.splice(0);
225+
// for doublechecking, we flip it inside out, and assert on the outermost
217226
TestUtils.act(() => {
218-
TestRenderer.create(<Effecty />);
227+
TestRenderer.act(() => {
228+
TestRenderer.create(<Effecty />);
229+
});
219230
});
220-
expect(log).toEqual([]);
231+
expect(log).toEqual(['called']);
221232
});
222-
expect(log).toEqual(['called']);
233+
});
223234

224-
log.splice(0);
225-
// for doublechecking, we flip it inside out, and assert on the outermost
226-
TestUtils.act(() => {
235+
describe('mocked scheduler', () => {
236+
beforeEach(() => {
237+
jest.resetModules();
238+
jest.mock('scheduler', () =>
239+
require.requireActual('scheduler/unstable_mock')
240+
);
241+
React = require('react');
242+
ReactDOM = require('react-dom');
243+
TestUtils = require('react-dom/test-utils');
244+
TestRenderer = require('react-test-renderer');
245+
});
246+
247+
afterEach(() => {
248+
jest.unmock('scheduler');
249+
});
250+
251+
it('flushes work only outside the outermost act(), even when nested from different renderers', () => {
252+
const log = [];
253+
function Effecty() {
254+
React.useEffect(() => {
255+
log.push('called');
256+
}, []);
257+
return null;
258+
}
259+
// with a mocked scheduler, this tests whether it flushes all work only on the outermost act
227260
TestRenderer.act(() => {
228-
TestRenderer.create(<Effecty />);
261+
TestUtils.act(() => {
262+
TestRenderer.create(<Effecty />);
263+
});
264+
expect(log).toEqual([]);
265+
});
266+
expect(log).toEqual(['called']);
267+
268+
log.splice(0);
269+
// for doublechecking, we flip it inside out, and assert on the outermost
270+
TestUtils.act(() => {
271+
TestRenderer.act(() => {
272+
TestRenderer.create(<Effecty />);
273+
});
229274
});
275+
expect(log).toEqual(['called']);
230276
});
231-
expect(log).toEqual(['called']);
232277
});
233-
}
278+
});

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

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,93 @@
77
* @emails react-core
88
*/
99

10+
let React;
11+
let ReactDOM;
1012
let ReactFeatureFlags;
11-
let act;
12-
describe('mocked scheduler', () => {
13-
beforeEach(() => {
14-
jest.resetModules();
15-
ReactFeatureFlags = require('shared/ReactFeatureFlags');
16-
ReactFeatureFlags.warnAboutMissingMockScheduler = true;
17-
jest.unmock('scheduler');
18-
act = require('react-dom/test-utils').act;
13+
14+
jest.unmock('scheduler');
15+
16+
function App() {
17+
return null;
18+
}
19+
20+
describe('unmocked scheduler warning', () => {
21+
describe('warns on mount', () => {
22+
beforeEach(() => {
23+
jest.resetModules();
24+
React = require('react');
25+
ReactDOM = require('react-dom');
26+
});
27+
28+
it('does not warn when rendering in sync mode', () => {
29+
expect(() => {
30+
ReactDOM.render(<App />, document.createElement('div'));
31+
}).toWarnDev([]);
32+
});
33+
34+
it('should warn when rendering in concurrent mode', () => {
35+
expect(() => {
36+
ReactDOM.unstable_createRoot(document.createElement('div')).render(
37+
<App />,
38+
);
39+
}).toWarnDev(
40+
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
41+
'to guarantee consistent behaviour across tests and browsers.',
42+
{withoutStack: true},
43+
);
44+
// does not warn twice
45+
expect(() => {
46+
ReactDOM.unstable_createRoot(document.createElement('div')).render(
47+
<App />,
48+
);
49+
}).toWarnDev([]);
50+
});
51+
52+
it('should warn when rendering in batched mode', () => {
53+
expect(() => {
54+
ReactDOM.unstable_createSyncRoot(document.createElement('div')).render(
55+
<App />,
56+
);
57+
}).toWarnDev(
58+
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
59+
'to guarantee consistent behaviour across tests and browsers.',
60+
{withoutStack: true},
61+
);
62+
// does not warn twice
63+
expect(() => {
64+
ReactDOM.unstable_createSyncRoot(document.createElement('div')).render(
65+
<App />,
66+
);
67+
}).toWarnDev([]);
68+
});
1969
});
20-
it("should warn when the scheduler isn't mocked", () => {
21-
expect(() => act(() => {})).toWarnDev(
22-
[
23-
'Starting from React v17, the "scheduler" module will need to be mocked',
24-
],
25-
{withoutStack: true},
26-
);
70+
71+
describe('warns with the feature flag', () => {
72+
beforeEach(() => {
73+
jest.resetModules();
74+
React = require('react');
75+
ReactDOM = require('react-dom');
76+
ReactFeatureFlags = require('shared/ReactFeatureFlags');
77+
ReactFeatureFlags.warnAboutMissingMockScheduler = true;
78+
});
79+
80+
afterEach(() => {
81+
ReactFeatureFlags.warnAboutMissingMockScheduler = false;
82+
});
83+
84+
it('should warn in sync mode when the feature flag is enabled', () => {
85+
expect(() => {
86+
ReactDOM.render(<App />, document.createElement('div'));
87+
}).toWarnDev(
88+
[
89+
'Starting from React v17, the "scheduler" module will need to be mocked',
90+
],
91+
{withoutStack: true},
92+
);
93+
// does not warn twice
94+
expect(() => {
95+
ReactDOM.render(<App />, document.createElement('div'));
96+
}).toWarnDev([]);
97+
});
2798
});
2899
});

packages/react-dom/src/test-utils/ReactTestUtilsAct.js

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type {Thenable} from 'react-reconciler/src/ReactFiberWorkLoop';
1212
import warningWithoutStack from 'shared/warningWithoutStack';
1313
import ReactDOM from 'react-dom';
1414
import ReactSharedInternals from 'shared/ReactSharedInternals';
15-
import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags';
1615
import enqueueTask from 'shared/enqueueTask';
1716
import * as Scheduler from 'scheduler';
1817

@@ -43,26 +42,11 @@ const {IsSomeRendererActing} = ReactSharedInternals;
4342
// this implementation should be exactly the same in
4443
// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
4544

46-
let hasWarnedAboutMissingMockScheduler = false;
4745
const isSchedulerMocked =
4846
typeof Scheduler.unstable_flushAllWithoutAsserting === 'function';
4947
const flushWork =
5048
Scheduler.unstable_flushAllWithoutAsserting ||
5149
function() {
52-
if (warnAboutMissingMockScheduler === true) {
53-
if (hasWarnedAboutMissingMockScheduler === false) {
54-
warningWithoutStack(
55-
null,
56-
'Starting from React v17, the "scheduler" module will need to be mocked ' +
57-
'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' +
58-
"to the top of your tests, or in your framework's global config file -\n\n" +
59-
'As an example, for jest - \n' +
60-
"jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" +
61-
'For more info, visit https://fb.me/react-mock-scheduler',
62-
);
63-
hasWarnedAboutMissingMockScheduler = true;
64-
}
65-
}
6650
while (flushPassiveEffects()) {}
6751
};
6852

packages/react-noop-renderer/src/createReactNoop.js

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
2727
import enqueueTask from 'shared/enqueueTask';
2828
import ReactSharedInternals from 'shared/ReactSharedInternals';
2929
import warningWithoutStack from 'shared/warningWithoutStack';
30-
import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags';
3130
import {ConcurrentRoot, BatchedRoot, LegacyRoot} from 'shared/ReactRootTags';
3231

3332
type Container = {
@@ -599,26 +598,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
599598
// this act() implementation should be exactly the same in
600599
// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
601600

602-
let hasWarnedAboutMissingMockScheduler = false;
603601
const isSchedulerMocked =
604602
typeof Scheduler.unstable_flushAllWithoutAsserting === 'function';
605603
const flushWork =
606604
Scheduler.unstable_flushAllWithoutAsserting ||
607605
function() {
608-
if (warnAboutMissingMockScheduler === true) {
609-
if (hasWarnedAboutMissingMockScheduler === false) {
610-
warningWithoutStack(
611-
null,
612-
'Starting from React v17, the "scheduler" module will need to be mocked ' +
613-
'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' +
614-
"to the top of your tests, or in your framework's global config file -\n\n" +
615-
'As an example, for jest - \n' +
616-
"jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" +
617-
'For more info, visit https://fb.me/react-mock-scheduler',
618-
);
619-
hasWarnedAboutMissingMockScheduler = true;
620-
}
621-
}
622606
while (flushPassiveEffects()) {}
623607
};
624608

packages/react-reconciler/src/ReactFiberReconciler.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
flushDiscreteUpdates,
5959
flushPassiveEffects,
6060
warnIfNotScopedWithMatchingAct,
61+
warnIfNotMockedSchedulerInConcurrentOrBatchedMode,
6162
IsThisRendererActing,
6263
} from './ReactFiberWorkLoop';
6364
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue';
@@ -314,6 +315,7 @@ export function updateContainer(
314315
if (__DEV__) {
315316
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
316317
if ('undefined' !== typeof jest) {
318+
warnIfNotMockedSchedulerInConcurrentOrBatchedMode(current);
317319
warnIfNotScopedWithMatchingAct(current);
318320
}
319321
}

0 commit comments

Comments
 (0)