Skip to content

Commit

Permalink
Add new mock build of Scheduler with flush, yield API (#14964)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
acdlite authored Feb 27, 2019
1 parent 4186952 commit 00748c5
Show file tree
Hide file tree
Showing 31 changed files with 1,074 additions and 1,002 deletions.
6 changes: 1 addition & 5 deletions packages/jest-mock-scheduler/npm/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/jest-mock-scheduler.production.min.js');
} else {
module.exports = require('./cjs/jest-mock-scheduler.development.js');
}
module.exports = require('scheduler/unstable_mock');
61 changes: 0 additions & 61 deletions packages/jest-mock-scheduler/src/JestMockScheduler.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const React = require('react');
let ReactFeatureFlags = require('shared/ReactFeatureFlags');

let ReactDOM;
let Scheduler;

const ConcurrentMode = React.unstable_ConcurrentMode;

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

beforeEach(() => {
// TODO pull this into helper method, reduce repetition.
// mock the browser APIs which are used in schedule:
// - requestAnimationFrame should pass the DOMHighResTimeStamp argument
// - calling 'window.postMessage' should actually fire postmessage handlers
global.requestAnimationFrame = function(cb) {
return setTimeout(() => {
cb(Date.now());
});
};
const originalAddEventListener = global.addEventListener;
let postMessageCallback;
global.addEventListener = function(eventName, callback, useCapture) {
if (eventName === 'message') {
postMessageCallback = callback;
} else {
originalAddEventListener(eventName, callback, useCapture);
}
};
global.postMessage = function(messageKey, targetOrigin) {
const postMessageEvent = {source: window, data: messageKey};
if (postMessageCallback) {
postMessageCallback(postMessageEvent);
}
};
jest.resetModules();
container = document.createElement('div');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');

document.body.appendChild(container);
});
Expand Down Expand Up @@ -124,6 +102,7 @@ describe('ReactDOMFiberAsync', () => {

// Should flush both updates now.
jest.runAllTimers();
Scheduler.flushAll();
expect(asyncValueRef.current.textContent).toBe('hello');
expect(syncValueRef.current.textContent).toBe('hello');
});
Expand All @@ -133,6 +112,7 @@ describe('ReactDOMFiberAsync', () => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
});

it('renders synchronously', () => {
Expand Down Expand Up @@ -160,18 +140,19 @@ describe('ReactDOMFiberAsync', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
});

it('createRoot makes the entire tree async', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<div>Hi</div>);
expect(container.textContent).toEqual('');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');

root.render(<div>Bye</div>);
expect(container.textContent).toEqual('Hi');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('Bye');
});

Expand All @@ -188,12 +169,12 @@ describe('ReactDOMFiberAsync', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<Component />);
expect(container.textContent).toEqual('');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('0');

instance.setState({step: 1});
expect(container.textContent).toEqual('0');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('1');
});

Expand All @@ -213,11 +194,11 @@ describe('ReactDOMFiberAsync', () => {
</ConcurrentMode>,
container,
);
jest.runAllTimers();
Scheduler.flushAll();

instance.setState({step: 1});
expect(container.textContent).toEqual('0');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('1');
});

Expand All @@ -239,11 +220,11 @@ describe('ReactDOMFiberAsync', () => {
</div>,
container,
);
jest.runAllTimers();
Scheduler.flushAll();

instance.setState({step: 1});
expect(container.textContent).toEqual('0');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('1');
});

Expand Down Expand Up @@ -369,7 +350,7 @@ describe('ReactDOMFiberAsync', () => {
</ConcurrentMode>,
container,
);
jest.runAllTimers();
Scheduler.flushAll();

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

// Flush the async updates
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('ABCD');
expect(ops).toEqual(['BC', 'ABCD']);
});
Expand All @@ -419,7 +400,7 @@ describe('ReactDOMFiberAsync', () => {
// Test that a normal update is async
inst.increment();
expect(container.textContent).toEqual('0');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toEqual('1');

let ops = [];
Expand Down Expand Up @@ -525,7 +506,7 @@ describe('ReactDOMFiberAsync', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<Form />);
// Flush
jest.runAllTimers();
Scheduler.flushAll();

let disableButton = disableButtonRef.current;
expect(disableButton.tagName).toBe('BUTTON');
Expand Down Expand Up @@ -592,7 +573,7 @@ describe('ReactDOMFiberAsync', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<Form />);
// Flush
jest.runAllTimers();
Scheduler.flushAll();

let disableButton = disableButtonRef.current;
expect(disableButton.tagName).toBe('BUTTON');
Expand Down Expand Up @@ -652,7 +633,7 @@ describe('ReactDOMFiberAsync', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(<Form />);
// Flush
jest.runAllTimers();
Scheduler.flushAll();

let enableButton = enableButtonRef.current;
expect(enableButton.tagName).toBe('BUTTON');
Expand Down
10 changes: 6 additions & 4 deletions packages/react-dom/src/__tests__/ReactDOMHooks-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

let React;
let ReactDOM;
let Scheduler;

describe('ReactDOMHooks', () => {
let container;
Expand All @@ -20,6 +21,7 @@ describe('ReactDOMHooks', () => {

React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');

container = document.createElement('div');
document.body.appendChild(container);
Expand Down Expand Up @@ -55,7 +57,7 @@ describe('ReactDOMHooks', () => {
expect(container.textContent).toBe('1');
expect(container2.textContent).toBe('');
expect(container3.textContent).toBe('');
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toBe('1');
expect(container2.textContent).toBe('2');
expect(container3.textContent).toBe('3');
Expand All @@ -64,7 +66,7 @@ describe('ReactDOMHooks', () => {
expect(container.textContent).toBe('2');
expect(container2.textContent).toBe('2'); // Not flushed yet
expect(container3.textContent).toBe('3'); // Not flushed yet
jest.runAllTimers();
Scheduler.flushAll();
expect(container.textContent).toBe('2');
expect(container2.textContent).toBe('4');
expect(container3.textContent).toBe('6');
Expand Down Expand Up @@ -166,14 +168,14 @@ describe('ReactDOMHooks', () => {
</React.unstable_ConcurrentMode>,
);

jest.runAllTimers();
Scheduler.flushAll();

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

jest.runAllTimers();
Scheduler.flushAll();

expect(labelRef.current.innerHTML).toBe('abc');
});
Expand Down
Loading

0 comments on commit 00748c5

Please sign in to comment.