Skip to content

Commit 00748c5

Browse files
authored
Add new mock build of Scheduler with flush, yield API (#14964)
* Add new mock build of Scheduler with flush, yield API Test environments need a way to take control of the Scheduler queue and incrementally flush work. Our current tests accomplish this either using dynamic injection, or by using Jest's fake timers feature. Both of these options are fragile and rely too much on implementation details. In this new approach, we have a separate build of Scheduler that is specifically designed for test environments. We mock the default implementation like we would any other module; in our case, via Jest. This special build has methods like `flushAll` and `yieldValue` that control when work is flushed. These methods are based on equivalent methods we've been using to write incremental React tests. Eventually we may want to migrate the React tests to interact with the mock Scheduler directly, instead of going through the host config like we currently do. For now, I'm using our custom static injection infrastructure to create the two builds of Scheduler — a default build for DOM (which falls back to a naive timer based implementation), and the new mock build. I did it this way because it allows me to share most of the implementation, which isn't specific to a host environment — e.g. everything related to the priority queue. It may be better to duplicate the shared code instead, especially considering that future environments (like React Native) may have entirely forked implementations. I'd prefer to wait until the implementation stabilizes before worrying about that, but I'm open to changing this now if we decide it's important enough. * Mock Scheduler in bundle tests, too * Remove special case by making regex more restrictive
1 parent 4186952 commit 00748c5

31 files changed

+1074
-1002
lines changed
+1-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
'use strict';
22

3-
if (process.env.NODE_ENV === 'production') {
4-
module.exports = require('./cjs/jest-mock-scheduler.production.min.js');
5-
} else {
6-
module.exports = require('./cjs/jest-mock-scheduler.development.js');
7-
}
3+
module.exports = require('scheduler/unstable_mock');

packages/jest-mock-scheduler/src/JestMockScheduler.js

-61
This file was deleted.

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

+19-38
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const React = require('react');
1313
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
1414

1515
let ReactDOM;
16+
let Scheduler;
1617

1718
const ConcurrentMode = React.unstable_ConcurrentMode;
1819

@@ -25,33 +26,10 @@ describe('ReactDOMFiberAsync', () => {
2526
let container;
2627

2728
beforeEach(() => {
28-
// TODO pull this into helper method, reduce repetition.
29-
// mock the browser APIs which are used in schedule:
30-
// - requestAnimationFrame should pass the DOMHighResTimeStamp argument
31-
// - calling 'window.postMessage' should actually fire postmessage handlers
32-
global.requestAnimationFrame = function(cb) {
33-
return setTimeout(() => {
34-
cb(Date.now());
35-
});
36-
};
37-
const originalAddEventListener = global.addEventListener;
38-
let postMessageCallback;
39-
global.addEventListener = function(eventName, callback, useCapture) {
40-
if (eventName === 'message') {
41-
postMessageCallback = callback;
42-
} else {
43-
originalAddEventListener(eventName, callback, useCapture);
44-
}
45-
};
46-
global.postMessage = function(messageKey, targetOrigin) {
47-
const postMessageEvent = {source: window, data: messageKey};
48-
if (postMessageCallback) {
49-
postMessageCallback(postMessageEvent);
50-
}
51-
};
5229
jest.resetModules();
5330
container = document.createElement('div');
5431
ReactDOM = require('react-dom');
32+
Scheduler = require('scheduler');
5533

5634
document.body.appendChild(container);
5735
});
@@ -124,6 +102,7 @@ describe('ReactDOMFiberAsync', () => {
124102

125103
// Should flush both updates now.
126104
jest.runAllTimers();
105+
Scheduler.flushAll();
127106
expect(asyncValueRef.current.textContent).toBe('hello');
128107
expect(syncValueRef.current.textContent).toBe('hello');
129108
});
@@ -133,6 +112,7 @@ describe('ReactDOMFiberAsync', () => {
133112
jest.resetModules();
134113
ReactFeatureFlags = require('shared/ReactFeatureFlags');
135114
ReactDOM = require('react-dom');
115+
Scheduler = require('scheduler');
136116
});
137117

138118
it('renders synchronously', () => {
@@ -160,18 +140,19 @@ describe('ReactDOMFiberAsync', () => {
160140
ReactFeatureFlags = require('shared/ReactFeatureFlags');
161141
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
162142
ReactDOM = require('react-dom');
143+
Scheduler = require('scheduler');
163144
});
164145

165146
it('createRoot makes the entire tree async', () => {
166147
const root = ReactDOM.unstable_createRoot(container);
167148
root.render(<div>Hi</div>);
168149
expect(container.textContent).toEqual('');
169-
jest.runAllTimers();
150+
Scheduler.flushAll();
170151
expect(container.textContent).toEqual('Hi');
171152

172153
root.render(<div>Bye</div>);
173154
expect(container.textContent).toEqual('Hi');
174-
jest.runAllTimers();
155+
Scheduler.flushAll();
175156
expect(container.textContent).toEqual('Bye');
176157
});
177158

@@ -188,12 +169,12 @@ describe('ReactDOMFiberAsync', () => {
188169
const root = ReactDOM.unstable_createRoot(container);
189170
root.render(<Component />);
190171
expect(container.textContent).toEqual('');
191-
jest.runAllTimers();
172+
Scheduler.flushAll();
192173
expect(container.textContent).toEqual('0');
193174

194175
instance.setState({step: 1});
195176
expect(container.textContent).toEqual('0');
196-
jest.runAllTimers();
177+
Scheduler.flushAll();
197178
expect(container.textContent).toEqual('1');
198179
});
199180

@@ -213,11 +194,11 @@ describe('ReactDOMFiberAsync', () => {
213194
</ConcurrentMode>,
214195
container,
215196
);
216-
jest.runAllTimers();
197+
Scheduler.flushAll();
217198

218199
instance.setState({step: 1});
219200
expect(container.textContent).toEqual('0');
220-
jest.runAllTimers();
201+
Scheduler.flushAll();
221202
expect(container.textContent).toEqual('1');
222203
});
223204

@@ -239,11 +220,11 @@ describe('ReactDOMFiberAsync', () => {
239220
</div>,
240221
container,
241222
);
242-
jest.runAllTimers();
223+
Scheduler.flushAll();
243224

244225
instance.setState({step: 1});
245226
expect(container.textContent).toEqual('0');
246-
jest.runAllTimers();
227+
Scheduler.flushAll();
247228
expect(container.textContent).toEqual('1');
248229
});
249230

@@ -369,7 +350,7 @@ describe('ReactDOMFiberAsync', () => {
369350
</ConcurrentMode>,
370351
container,
371352
);
372-
jest.runAllTimers();
353+
Scheduler.flushAll();
373354

374355
// Updates are async by default
375356
instance.push('A');
@@ -392,7 +373,7 @@ describe('ReactDOMFiberAsync', () => {
392373
expect(ops).toEqual(['BC']);
393374

394375
// Flush the async updates
395-
jest.runAllTimers();
376+
Scheduler.flushAll();
396377
expect(container.textContent).toEqual('ABCD');
397378
expect(ops).toEqual(['BC', 'ABCD']);
398379
});
@@ -419,7 +400,7 @@ describe('ReactDOMFiberAsync', () => {
419400
// Test that a normal update is async
420401
inst.increment();
421402
expect(container.textContent).toEqual('0');
422-
jest.runAllTimers();
403+
Scheduler.flushAll();
423404
expect(container.textContent).toEqual('1');
424405

425406
let ops = [];
@@ -525,7 +506,7 @@ describe('ReactDOMFiberAsync', () => {
525506
const root = ReactDOM.unstable_createRoot(container);
526507
root.render(<Form />);
527508
// Flush
528-
jest.runAllTimers();
509+
Scheduler.flushAll();
529510

530511
let disableButton = disableButtonRef.current;
531512
expect(disableButton.tagName).toBe('BUTTON');
@@ -592,7 +573,7 @@ describe('ReactDOMFiberAsync', () => {
592573
const root = ReactDOM.unstable_createRoot(container);
593574
root.render(<Form />);
594575
// Flush
595-
jest.runAllTimers();
576+
Scheduler.flushAll();
596577

597578
let disableButton = disableButtonRef.current;
598579
expect(disableButton.tagName).toBe('BUTTON');
@@ -652,7 +633,7 @@ describe('ReactDOMFiberAsync', () => {
652633
const root = ReactDOM.unstable_createRoot(container);
653634
root.render(<Form />);
654635
// Flush
655-
jest.runAllTimers();
636+
Scheduler.flushAll();
656637

657638
let enableButton = enableButtonRef.current;
658639
expect(enableButton.tagName).toBe('BUTTON');

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
let React;
1313
let ReactDOM;
14+
let Scheduler;
1415

1516
describe('ReactDOMHooks', () => {
1617
let container;
@@ -20,6 +21,7 @@ describe('ReactDOMHooks', () => {
2021

2122
React = require('react');
2223
ReactDOM = require('react-dom');
24+
Scheduler = require('scheduler');
2325

2426
container = document.createElement('div');
2527
document.body.appendChild(container);
@@ -55,7 +57,7 @@ describe('ReactDOMHooks', () => {
5557
expect(container.textContent).toBe('1');
5658
expect(container2.textContent).toBe('');
5759
expect(container3.textContent).toBe('');
58-
jest.runAllTimers();
60+
Scheduler.flushAll();
5961
expect(container.textContent).toBe('1');
6062
expect(container2.textContent).toBe('2');
6163
expect(container3.textContent).toBe('3');
@@ -64,7 +66,7 @@ describe('ReactDOMHooks', () => {
6466
expect(container.textContent).toBe('2');
6567
expect(container2.textContent).toBe('2'); // Not flushed yet
6668
expect(container3.textContent).toBe('3'); // Not flushed yet
67-
jest.runAllTimers();
69+
Scheduler.flushAll();
6870
expect(container.textContent).toBe('2');
6971
expect(container2.textContent).toBe('4');
7072
expect(container3.textContent).toBe('6');
@@ -166,14 +168,14 @@ describe('ReactDOMHooks', () => {
166168
</React.unstable_ConcurrentMode>,
167169
);
168170

169-
jest.runAllTimers();
171+
Scheduler.flushAll();
170172

171173
inputRef.current.value = 'abc';
172174
inputRef.current.dispatchEvent(
173175
new Event('input', {bubbles: true, cancelable: true}),
174176
);
175177

176-
jest.runAllTimers();
178+
Scheduler.flushAll();
177179

178180
expect(labelRef.current.innerHTML).toBe('abc');
179181
});

0 commit comments

Comments
 (0)