diff --git a/packages/jest-mock-scheduler/npm/index.js b/packages/jest-mock-scheduler/npm/index.js
index d7a102c971..9a1d6ca4d1 100644
--- a/packages/jest-mock-scheduler/npm/index.js
+++ b/packages/jest-mock-scheduler/npm/index.js
@@ -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');
diff --git a/packages/jest-mock-scheduler/src/JestMockScheduler.js b/packages/jest-mock-scheduler/src/JestMockScheduler.js
deleted file mode 100644
index 609ece2c21..0000000000
--- a/packages/jest-mock-scheduler/src/JestMockScheduler.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
-// Math.pow(2, 30) - 1
-// 0b111111111111111111111111111111
-const maxSigned31BitInt = 1073741823;
-
-export function mockRestore() {
- delete global._schedMock;
-}
-
-let callback = null;
-let currentTime = -1;
-
-function flushCallback(didTimeout, ms) {
- if (callback !== null) {
- let cb = callback;
- callback = null;
- try {
- currentTime = ms;
- cb(didTimeout);
- } finally {
- currentTime = -1;
- }
- }
-}
-
-function requestHostCallback(cb, ms) {
- if (currentTime !== -1) {
- // Protect against re-entrancy.
- setTimeout(requestHostCallback, 0, cb, ms);
- } else {
- callback = cb;
- setTimeout(flushCallback, ms, true, ms);
- setTimeout(flushCallback, maxSigned31BitInt, false, maxSigned31BitInt);
- }
-}
-
-function cancelHostCallback() {
- callback = null;
-}
-
-function shouldYieldToHost() {
- return false;
-}
-
-function getCurrentTime() {
- return currentTime === -1 ? 0 : currentTime;
-}
-
-global._schedMock = [
- requestHostCallback,
- cancelHostCallback,
- shouldYieldToHost,
- getCurrentTime,
-];
diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js
index d49a6218cb..69013757ae 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js
@@ -13,6 +13,7 @@ const React = require('react');
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
let ReactDOM;
+let Scheduler;
const ConcurrentMode = React.unstable_ConcurrentMode;
@@ -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);
});
@@ -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');
});
@@ -133,6 +112,7 @@ describe('ReactDOMFiberAsync', () => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactDOM = require('react-dom');
+ Scheduler = require('scheduler');
});
it('renders synchronously', () => {
@@ -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(
Hi
);
expect(container.textContent).toEqual('');
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
root.render(Bye
);
expect(container.textContent).toEqual('Hi');
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('Bye');
});
@@ -188,12 +169,12 @@ describe('ReactDOMFiberAsync', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render();
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');
});
@@ -213,11 +194,11 @@ describe('ReactDOMFiberAsync', () => {
,
container,
);
- jest.runAllTimers();
+ Scheduler.flushAll();
instance.setState({step: 1});
expect(container.textContent).toEqual('0');
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('1');
});
@@ -239,11 +220,11 @@ describe('ReactDOMFiberAsync', () => {
,
container,
);
- jest.runAllTimers();
+ Scheduler.flushAll();
instance.setState({step: 1});
expect(container.textContent).toEqual('0');
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('1');
});
@@ -369,7 +350,7 @@ describe('ReactDOMFiberAsync', () => {
,
container,
);
- jest.runAllTimers();
+ Scheduler.flushAll();
// Updates are async by default
instance.push('A');
@@ -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']);
});
@@ -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 = [];
@@ -525,7 +506,7 @@ describe('ReactDOMFiberAsync', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render();
// Flush
- jest.runAllTimers();
+ Scheduler.flushAll();
let disableButton = disableButtonRef.current;
expect(disableButton.tagName).toBe('BUTTON');
@@ -592,7 +573,7 @@ describe('ReactDOMFiberAsync', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render();
// Flush
- jest.runAllTimers();
+ Scheduler.flushAll();
let disableButton = disableButtonRef.current;
expect(disableButton.tagName).toBe('BUTTON');
@@ -652,7 +633,7 @@ describe('ReactDOMFiberAsync', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render();
// Flush
- jest.runAllTimers();
+ Scheduler.flushAll();
let enableButton = enableButtonRef.current;
expect(enableButton.tagName).toBe('BUTTON');
diff --git a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js
index 84ad3454a1..5dff31ad17 100644
--- a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js
@@ -11,6 +11,7 @@
let React;
let ReactDOM;
+let Scheduler;
describe('ReactDOMHooks', () => {
let container;
@@ -20,6 +21,7 @@ describe('ReactDOMHooks', () => {
React = require('react');
ReactDOM = require('react-dom');
+ Scheduler = require('scheduler');
container = document.createElement('div');
document.body.appendChild(container);
@@ -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');
@@ -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');
@@ -166,14 +168,14 @@ describe('ReactDOMHooks', () => {
,
);
- 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');
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
index 3ae0414d3c..670f45e28b 100644
--- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
@@ -12,74 +12,36 @@
let React = require('react');
let ReactDOM = require('react-dom');
let ReactDOMServer = require('react-dom/server');
+let Scheduler = require('scheduler');
let ConcurrentMode = React.unstable_ConcurrentMode;
describe('ReactDOMRoot', () => {
let container;
- let advanceCurrentTime;
-
beforeEach(() => {
- container = document.createElement('div');
- // 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
- // - must allow artificially changing time returned by Date.now
- // Performance.now is not supported in the test environment
- const originalDateNow = Date.now;
- let advancedTime = null;
- global.Date.now = function() {
- if (advancedTime) {
- return originalDateNow() + advancedTime;
- }
- return originalDateNow();
- };
- advanceCurrentTime = function(amount) {
- advancedTime = amount;
- };
- 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');
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
+ Scheduler = require('scheduler');
ConcurrentMode = React.unstable_ConcurrentMode;
});
it('renders children', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
});
it('unmounts children', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
root.unmount();
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('');
});
@@ -91,7 +53,7 @@ describe('ReactDOMRoot', () => {
ops.push('inside callback: ' + container.textContent);
});
ops.push('before committing: ' + container.textContent);
- jest.runAllTimers();
+ Scheduler.flushAll();
ops.push('after committing: ' + container.textContent);
expect(ops).toEqual([
'before committing: ',
@@ -104,7 +66,7 @@ describe('ReactDOMRoot', () => {
it('resolves `work.then` callback synchronously if the work already committed', () => {
const root = ReactDOM.unstable_createRoot(container);
const work = root.render(Hi);
- jest.runAllTimers();
+ Scheduler.flushAll();
let ops = [];
work.then(() => {
ops.push('inside callback');
@@ -132,7 +94,7 @@ describe('ReactDOMRoot', () => {
,
);
- jest.runAllTimers();
+ Scheduler.flushAll();
// Accepts `hydrate` option
const container2 = document.createElement('div');
@@ -143,7 +105,7 @@ describe('ReactDOMRoot', () => {
,
);
- expect(jest.runAllTimers).toWarnDev('Extra attributes', {
+ expect(() => Scheduler.flushAll()).toWarnDev('Extra attributes', {
withoutStack: true,
});
});
@@ -157,7 +119,7 @@ describe('ReactDOMRoot', () => {
d
,
);
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('abcd');
root.render(
@@ -165,7 +127,7 @@ describe('ReactDOMRoot', () => {
c
,
);
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('abdc');
});
@@ -201,7 +163,7 @@ describe('ReactDOMRoot', () => {
,
);
- jest.runAllTimers();
+ Scheduler.flushAll();
// Hasn't updated yet
expect(container.textContent).toEqual('');
@@ -230,7 +192,7 @@ describe('ReactDOMRoot', () => {
const batch = root.createBatch();
batch.render(Hi);
// Flush all async work.
- jest.runAllTimers();
+ Scheduler.flushAll();
// Root should complete without committing.
expect(ops).toEqual(['Foo']);
expect(container.textContent).toEqual('');
@@ -248,7 +210,7 @@ describe('ReactDOMRoot', () => {
const batch = root.createBatch();
batch.render(Foo);
- jest.runAllTimers();
+ Scheduler.flushAll();
// Hasn't updated yet
expect(container.textContent).toEqual('');
@@ -288,7 +250,7 @@ describe('ReactDOMRoot', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(1);
- advanceCurrentTime(2000);
+ Scheduler.advanceTime(2000);
// This batch has a later expiration time than the earlier update.
const batch = root.createBatch();
@@ -296,7 +258,7 @@ describe('ReactDOMRoot', () => {
batch.commit();
expect(container.textContent).toEqual('');
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('1');
});
@@ -323,7 +285,7 @@ describe('ReactDOMRoot', () => {
batch1.render(1);
// This batch has a later expiration time
- advanceCurrentTime(2000);
+ Scheduler.advanceTime(2000);
const batch2 = root.createBatch();
batch2.render(2);
@@ -342,7 +304,7 @@ describe('ReactDOMRoot', () => {
batch1.render(1);
// This batch has a later expiration time
- advanceCurrentTime(2000);
+ Scheduler.advanceTime(2000);
const batch2 = root.createBatch();
batch2.render(2);
@@ -352,7 +314,7 @@ describe('ReactDOMRoot', () => {
expect(container.textContent).toEqual('2');
batch1.commit();
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('1');
});
@@ -378,7 +340,7 @@ describe('ReactDOMRoot', () => {
it('warns when rendering with legacy API into createRoot() container', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
expect(() => {
ReactDOM.render(Bye
, container);
@@ -393,7 +355,7 @@ describe('ReactDOMRoot', () => {
],
{withoutStack: true},
);
- jest.runAllTimers();
+ Scheduler.flushAll();
// This works now but we could disallow it:
expect(container.textContent).toEqual('Bye');
});
@@ -401,7 +363,7 @@ describe('ReactDOMRoot', () => {
it('warns when hydrating with legacy API into createRoot() container', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
expect(() => {
ReactDOM.hydrate(Hi
, container);
@@ -421,7 +383,7 @@ describe('ReactDOMRoot', () => {
it('warns when unmounting with legacy API (no previous content)', () => {
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
let unmounted = false;
expect(() => {
@@ -437,10 +399,10 @@ describe('ReactDOMRoot', () => {
{withoutStack: true},
);
expect(unmounted).toBe(false);
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
root.unmount();
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('');
});
@@ -450,17 +412,17 @@ describe('ReactDOMRoot', () => {
// The rest is the same as test above.
const root = ReactDOM.unstable_createRoot(container);
root.render(Hi
);
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
let unmounted = false;
expect(() => {
unmounted = ReactDOM.unmountComponentAtNode(container);
}).toWarnDev('Did you mean to call root.unmount()?', {withoutStack: true});
expect(unmounted).toBe(false);
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('Hi');
root.unmount();
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(container.textContent).toEqual('');
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index c0d1828749..22e5f30f00 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -12,6 +12,7 @@
let React;
let ReactDOM;
let ReactDOMServer;
+let Scheduler;
let ReactFeatureFlags;
let Suspense;
let act;
@@ -27,6 +28,7 @@ describe('ReactDOMServerPartialHydration', () => {
ReactDOM = require('react-dom');
act = require('react-dom/test-utils').act;
ReactDOMServer = require('react-dom/server');
+ Scheduler = require('scheduler');
Suspense = React.Suspense;
});
@@ -72,6 +74,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render();
+ Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
@@ -80,6 +83,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = false;
resolve();
await promise;
+ Scheduler.flushAll();
jest.runAllTimers();
// We should now have hydrated with a ref on the existing span.
@@ -238,6 +242,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render();
+ Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
@@ -253,6 +258,7 @@ describe('ReactDOMServerPartialHydration', () => {
// Flushing both of these in the same batch won't be able to hydrate so we'll
// probably throw away the existing subtree.
+ Scheduler.flushAll();
jest.runAllTimers();
// Pick up the new span. In an ideal implementation this might be the same span
@@ -305,15 +311,17 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render();
+ Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
// Render an update, but leave it still suspended.
root.render();
+ Scheduler.flushAll();
+ jest.runAllTimers();
// Flushing now should delete the existing content and show the fallback.
- jest.runAllTimers();
expect(container.getElementsByTagName('span').length).toBe(0);
expect(ref.current).toBe(null);
@@ -324,6 +332,7 @@ describe('ReactDOMServerPartialHydration', () => {
resolve();
await promise;
+ Scheduler.flushAll();
jest.runAllTimers();
let span = container.getElementsByTagName('span')[0];
@@ -375,6 +384,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render();
+ Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
@@ -383,6 +393,7 @@ describe('ReactDOMServerPartialHydration', () => {
root.render();
// Flushing now should delete the existing content and show the fallback.
+ Scheduler.flushAll();
jest.runAllTimers();
expect(container.getElementsByTagName('span').length).toBe(0);
@@ -394,6 +405,7 @@ describe('ReactDOMServerPartialHydration', () => {
resolve();
await promise;
+ Scheduler.flushAll();
jest.runAllTimers();
let span = container.getElementsByTagName('span')[0];
@@ -444,6 +456,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render();
+ Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
@@ -452,6 +465,7 @@ describe('ReactDOMServerPartialHydration', () => {
root.render();
// Flushing now should delete the existing content and show the fallback.
+ Scheduler.flushAll();
jest.runAllTimers();
expect(container.getElementsByTagName('span').length).toBe(0);
@@ -463,6 +477,7 @@ describe('ReactDOMServerPartialHydration', () => {
resolve();
await promise;
+ Scheduler.flushAll();
jest.runAllTimers();
let span = container.getElementsByTagName('span')[0];
@@ -522,6 +537,7 @@ describe('ReactDOMServerPartialHydration', () => {
,
);
+ Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
@@ -541,6 +557,7 @@ describe('ReactDOMServerPartialHydration', () => {
// Flushing both of these in the same batch won't be able to hydrate so we'll
// probably throw away the existing subtree.
+ Scheduler.flushAll();
jest.runAllTimers();
// Pick up the new span. In an ideal implementation this might be the same span
@@ -603,6 +620,7 @@ describe('ReactDOMServerPartialHydration', () => {
,
);
+ Scheduler.flushAll();
jest.runAllTimers();
expect(ref.current).toBe(null);
@@ -615,6 +633,7 @@ describe('ReactDOMServerPartialHydration', () => {
);
// Flushing now should delete the existing content and show the fallback.
+ Scheduler.flushAll();
jest.runAllTimers();
expect(container.getElementsByTagName('span').length).toBe(0);
@@ -626,6 +645,7 @@ describe('ReactDOMServerPartialHydration', () => {
resolve();
await promise;
+ Scheduler.flushAll();
jest.runAllTimers();
let span = container.getElementsByTagName('span')[0];
@@ -674,6 +694,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = false;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render();
+ Scheduler.flushAll();
jest.runAllTimers();
expect(container.textContent).toBe('Hello');
@@ -746,6 +767,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = false;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render();
+ Scheduler.flushAll();
jest.runAllTimers();
// We're still loading because we're waiting for the server to stream more content.
@@ -761,6 +783,7 @@ describe('ReactDOMServerPartialHydration', () => {
// But it is not yet hydrated.
expect(ref.current).toBe(null);
+ Scheduler.flushAll();
jest.runAllTimers();
// Now it's hydrated.
@@ -837,6 +860,7 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = false;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render();
+ Scheduler.flushAll();
jest.runAllTimers();
// We're still loading because we're waiting for the server to stream more content.
@@ -850,6 +874,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.textContent).toBe('Loading...');
expect(ref.current).toBe(null);
+ Scheduler.flushAll();
jest.runAllTimers();
// Hydrating should've generated an error and replaced the suspense boundary.
diff --git a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
index d6ef0a1e4d..c982c787ee 100644
--- a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
@@ -13,6 +13,7 @@ let PropTypes;
let React;
let ReactDOM;
let ReactFeatureFlags;
+let Scheduler;
describe('ReactErrorBoundaries', () => {
let log;
@@ -44,6 +45,7 @@ describe('ReactErrorBoundaries', () => {
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
ReactDOM = require('react-dom');
React = require('react');
+ Scheduler = require('scheduler');
log = [];
@@ -1839,9 +1841,8 @@ describe('ReactErrorBoundaries', () => {
expect(container.firstChild.textContent).toBe('Initial value');
log.length = 0;
- jest.runAllTimers();
-
// Flush passive effects and handle the error
+ Scheduler.flushAll();
expect(log).toEqual([
'BrokenUseEffect useEffect [!]',
// Handle the error
diff --git a/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js b/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js
index 03d2cddeaa..5b0317d447 100644
--- a/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js
+++ b/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js
@@ -12,6 +12,7 @@
const React = require('react');
let ReactDOM = require('react-dom');
let ReactFeatureFlags;
+let Scheduler;
const setUntrackedChecked = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
@@ -484,6 +485,7 @@ describe('ChangeEventPlugin', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
+ Scheduler = require('scheduler');
});
it('text input', () => {
const root = ReactDOM.unstable_createRoot(container);
@@ -515,7 +517,7 @@ describe('ChangeEventPlugin', () => {
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
@@ -565,7 +567,7 @@ describe('ChangeEventPlugin', () => {
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(ops).toEqual(['render: false']);
expect(input.checked).toBe(false);
@@ -581,7 +583,7 @@ describe('ChangeEventPlugin', () => {
// Now let's make sure we're using the controlled value.
root.render();
- jest.runAllTimers();
+ Scheduler.flushAll();
ops = [];
@@ -624,7 +626,7 @@ describe('ChangeEventPlugin', () => {
expect(ops).toEqual([]);
expect(textarea).toBe(undefined);
// Flush callbacks.
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(ops).toEqual(['render: initial']);
expect(textarea.value).toBe('initial');
@@ -675,7 +677,7 @@ describe('ChangeEventPlugin', () => {
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
@@ -726,7 +728,7 @@ describe('ChangeEventPlugin', () => {
expect(ops).toEqual([]);
expect(input).toBe(undefined);
// Flush callbacks.
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(ops).toEqual(['render: initial']);
expect(input.value).toBe('initial');
@@ -741,7 +743,7 @@ describe('ChangeEventPlugin', () => {
expect(input.value).toBe('initial');
// Flush callbacks.
- jest.runAllTimers();
+ Scheduler.flushAll();
// Now the click update has flushed.
expect(ops).toEqual(['render: ']);
expect(input.value).toBe('');
diff --git a/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js b/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js
index 3f11c71371..32f66e8e0c 100644
--- a/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js
+++ b/packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js
@@ -13,6 +13,7 @@ describe('SimpleEventPlugin', function() {
let React;
let ReactDOM;
let ReactFeatureFlags;
+ let Scheduler;
let onClick;
let container;
@@ -35,33 +36,10 @@ describe('SimpleEventPlugin', function() {
}
beforeEach(function() {
- // 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();
React = require('react');
ReactDOM = require('react-dom');
+ Scheduler = require('scheduler');
onClick = jest.fn();
});
@@ -258,6 +236,7 @@ describe('SimpleEventPlugin', function() {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
+ Scheduler = require('scheduler');
});
it('flushes pending interactive work before extracting event handler', () => {
@@ -296,7 +275,7 @@ describe('SimpleEventPlugin', function() {
expect(ops).toEqual([]);
expect(button).toBe(undefined);
// Flush async work
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(ops).toEqual(['render button: enabled']);
ops = [];
@@ -336,7 +315,7 @@ describe('SimpleEventPlugin', function() {
click();
click();
click();
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(ops).toEqual([]);
});
@@ -367,7 +346,7 @@ describe('SimpleEventPlugin', function() {
// Should not have flushed yet because it's async
expect(button).toBe(undefined);
// Flush async work
- jest.runAllTimers();
+ Scheduler.flushAll();
expect(button.textContent).toEqual('Count: 0');
function click() {
@@ -390,7 +369,7 @@ describe('SimpleEventPlugin', function() {
click();
// Flush the remaining work
- jest.runAllTimers();
+ Scheduler.flushAll();
// The counter should equal the total number of clicks
expect(button.textContent).toEqual('Count: 7');
});
@@ -459,7 +438,7 @@ describe('SimpleEventPlugin', function() {
click();
// Flush the remaining work
- jest.runAllTimers();
+ Scheduler.flushAll();
// Both counters should equal the total number of clicks
expect(button.textContent).toEqual('High-pri count: 7, Low-pri count: 7');
});
diff --git a/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js b/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js
index 354ccf25b8..21b691c4ff 100644
--- a/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js
+++ b/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js
@@ -13,34 +13,9 @@ let React;
let ReactFeatureFlags;
let ReactDOM;
let SchedulerTracing;
+let Scheduler;
let ReactCache;
-function initEnvForAsyncTesting() {
- // Boilerplate copied from ReactDOMRoot-test
- // TODO pull this into helper method, reduce repetition.
- // TODO remove `requestAnimationFrame` when upgrading to Jest 24 with Lolex
- 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);
- }
- };
-}
-
function loadModules() {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffects = false;
@@ -51,6 +26,7 @@ function loadModules() {
React = require('react');
SchedulerTracing = require('scheduler/tracing');
ReactDOM = require('react-dom');
+ Scheduler = require('scheduler');
ReactCache = require('react-cache');
}
@@ -61,7 +37,6 @@ describe('ProfilerDOM', () => {
let onInteractionTraced;
beforeEach(() => {
- initEnvForAsyncTesting();
loadModules();
onInteractionScheduledWorkCompleted = jest.fn();
@@ -114,7 +89,7 @@ describe('ProfilerDOM', () => {
batch = root.createBatch();
batch.render(
}>
-
+
,
);
batch.then(
@@ -125,9 +100,12 @@ describe('ProfilerDOM', () => {
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
+ jest.runAllTimers();
+
resourcePromise.then(
SchedulerTracing.unstable_wrap(() => {
jest.runAllTimers();
+ Scheduler.flushAll();
expect(element.textContent).toBe('Text');
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
@@ -154,6 +132,8 @@ describe('ProfilerDOM', () => {
);
}),
);
+
+ Scheduler.flushAll();
});
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
@@ -161,7 +141,7 @@ describe('ProfilerDOM', () => {
interaction,
);
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
-
- jest.runAllTimers();
+ Scheduler.flushAll();
+ jest.advanceTimersByTime(500);
});
});
diff --git a/packages/scheduler/npm/unstable_mock.js b/packages/scheduler/npm/unstable_mock.js
new file mode 100644
index 0000000000..e72ea3186f
--- /dev/null
+++ b/packages/scheduler/npm/unstable_mock.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/scheduler-unstable_mock.production.min.js');
+} else {
+ module.exports = require('./cjs/scheduler-unstable_mock.development.js');
+}
diff --git a/packages/scheduler/package.json b/packages/scheduler/package.json
index a5b88e3a38..dde926df05 100644
--- a/packages/scheduler/package.json
+++ b/packages/scheduler/package.json
@@ -27,6 +27,7 @@
"index.js",
"tracing.js",
"tracing-profiling.js",
+ "unstable_mock.js",
"cjs/",
"umd/"
],
diff --git a/packages/scheduler/src/Scheduler.js b/packages/scheduler/src/Scheduler.js
index 6669699cb2..ab533e9cd8 100644
--- a/packages/scheduler/src/Scheduler.js
+++ b/packages/scheduler/src/Scheduler.js
@@ -9,6 +9,12 @@
/* eslint-disable no-var */
import {enableSchedulerDebugging} from './SchedulerFeatureFlags';
+import {
+ requestHostCallback,
+ cancelHostCallback,
+ shouldYieldToHost,
+ getCurrentTime,
+} from './SchedulerHostConfig';
// TODO: Use symbols?
var ImmediatePriority = 1;
@@ -47,9 +53,6 @@ var isExecutingCallback = false;
var isHostCallbackScheduled = false;
-var hasNativePerformanceNow =
- typeof performance === 'object' && typeof performance.now === 'function';
-
function ensureHostCallbackIsScheduled() {
if (isExecutingCallback) {
// Don't schedule work yet; wait until the next time we yield.
@@ -443,275 +446,6 @@ function unstable_shouldYield() {
);
}
-// The remaining code is essentially a polyfill for requestIdleCallback. It
-// works by scheduling a requestAnimationFrame, storing the time for the start
-// of the frame, then scheduling a postMessage which gets scheduled after paint.
-// Within the postMessage handler do as much work as possible until time + frame
-// rate. By separating the idle call into a separate event tick we ensure that
-// layout, paint and other browser work is counted against the available time.
-// The frame rate is dynamically adjusted.
-
-// We capture a local reference to any global, in case it gets polyfilled after
-// this module is initially evaluated. We want to be using a
-// consistent implementation.
-var localDate = Date;
-
-// This initialization code may run even on server environments if a component
-// just imports ReactDOM (e.g. for findDOMNode). Some environments might not
-// have setTimeout or clearTimeout. However, we always expect them to be defined
-// on the client. https://github.com/facebook/react/pull/13088
-var localSetTimeout = typeof setTimeout === 'function' ? setTimeout : undefined;
-var localClearTimeout =
- typeof clearTimeout === 'function' ? clearTimeout : undefined;
-
-// We don't expect either of these to necessarily be defined, but we will error
-// later if they are missing on the client.
-var localRequestAnimationFrame =
- typeof requestAnimationFrame === 'function'
- ? requestAnimationFrame
- : undefined;
-var localCancelAnimationFrame =
- typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
-
-var getCurrentTime;
-
-// requestAnimationFrame does not run when the tab is in the background. If
-// we're backgrounded we prefer for that work to happen so that the page
-// continues to load in the background. So we also schedule a 'setTimeout' as
-// a fallback.
-// TODO: Need a better heuristic for backgrounded work.
-var ANIMATION_FRAME_TIMEOUT = 100;
-var rAFID;
-var rAFTimeoutID;
-var requestAnimationFrameWithTimeout = function(callback) {
- // schedule rAF and also a setTimeout
- rAFID = localRequestAnimationFrame(function(timestamp) {
- // cancel the setTimeout
- localClearTimeout(rAFTimeoutID);
- callback(timestamp);
- });
- rAFTimeoutID = localSetTimeout(function() {
- // cancel the requestAnimationFrame
- localCancelAnimationFrame(rAFID);
- callback(getCurrentTime());
- }, ANIMATION_FRAME_TIMEOUT);
-};
-
-if (hasNativePerformanceNow) {
- var Performance = performance;
- getCurrentTime = function() {
- return Performance.now();
- };
-} else {
- getCurrentTime = function() {
- return localDate.now();
- };
-}
-
-var requestHostCallback;
-var cancelHostCallback;
-var shouldYieldToHost;
-
-var globalValue = null;
-if (typeof window !== 'undefined') {
- globalValue = window;
-} else if (typeof global !== 'undefined') {
- globalValue = global;
-}
-
-if (globalValue && globalValue._schedMock) {
- // Dynamic injection, only for testing purposes.
- var globalImpl = globalValue._schedMock;
- requestHostCallback = globalImpl[0];
- cancelHostCallback = globalImpl[1];
- shouldYieldToHost = globalImpl[2];
- getCurrentTime = globalImpl[3];
-} else if (
- // If Scheduler runs in a non-DOM environment, it falls back to a naive
- // implementation using setTimeout.
- typeof window === 'undefined' ||
- // Check if MessageChannel is supported, too.
- typeof MessageChannel !== 'function'
-) {
- // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
- // fallback to a naive implementation.
- var _callback = null;
- var _flushCallback = function(didTimeout) {
- if (_callback !== null) {
- try {
- _callback(didTimeout);
- } finally {
- _callback = null;
- }
- }
- };
- requestHostCallback = function(cb, ms) {
- if (_callback !== null) {
- // Protect against re-entrancy.
- setTimeout(requestHostCallback, 0, cb);
- } else {
- _callback = cb;
- setTimeout(_flushCallback, 0, false);
- }
- };
- cancelHostCallback = function() {
- _callback = null;
- };
- shouldYieldToHost = function() {
- return false;
- };
-} else {
- if (typeof console !== 'undefined') {
- // TODO: Remove fb.me link
- if (typeof localRequestAnimationFrame !== 'function') {
- console.error(
- "This browser doesn't support requestAnimationFrame. " +
- 'Make sure that you load a ' +
- 'polyfill in older browsers. https://fb.me/react-polyfills',
- );
- }
- if (typeof localCancelAnimationFrame !== 'function') {
- console.error(
- "This browser doesn't support cancelAnimationFrame. " +
- 'Make sure that you load a ' +
- 'polyfill in older browsers. https://fb.me/react-polyfills',
- );
- }
- }
-
- var scheduledHostCallback = null;
- var isMessageEventScheduled = false;
- var timeoutTime = -1;
-
- var isAnimationFrameScheduled = false;
-
- var isFlushingHostCallback = false;
-
- var frameDeadline = 0;
- // We start out assuming that we run at 30fps but then the heuristic tracking
- // will adjust this value to a faster fps if we get more frequent animation
- // frames.
- var previousFrameTime = 33;
- var activeFrameTime = 33;
-
- shouldYieldToHost = function() {
- return frameDeadline <= getCurrentTime();
- };
-
- // We use the postMessage trick to defer idle work until after the repaint.
- var channel = new MessageChannel();
- var port = channel.port2;
- channel.port1.onmessage = function(event) {
- isMessageEventScheduled = false;
-
- var prevScheduledCallback = scheduledHostCallback;
- var prevTimeoutTime = timeoutTime;
- scheduledHostCallback = null;
- timeoutTime = -1;
-
- var currentTime = getCurrentTime();
-
- var didTimeout = false;
- if (frameDeadline - currentTime <= 0) {
- // There's no time left in this idle period. Check if the callback has
- // a timeout and whether it's been exceeded.
- if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
- // Exceeded the timeout. Invoke the callback even though there's no
- // time left.
- didTimeout = true;
- } else {
- // No timeout.
- if (!isAnimationFrameScheduled) {
- // Schedule another animation callback so we retry later.
- isAnimationFrameScheduled = true;
- requestAnimationFrameWithTimeout(animationTick);
- }
- // Exit without invoking the callback.
- scheduledHostCallback = prevScheduledCallback;
- timeoutTime = prevTimeoutTime;
- return;
- }
- }
-
- if (prevScheduledCallback !== null) {
- isFlushingHostCallback = true;
- try {
- prevScheduledCallback(didTimeout);
- } finally {
- isFlushingHostCallback = false;
- }
- }
- };
-
- var animationTick = function(rafTime) {
- if (scheduledHostCallback !== null) {
- // Eagerly schedule the next animation callback at the beginning of the
- // frame. If the scheduler queue is not empty at the end of the frame, it
- // will continue flushing inside that callback. If the queue *is* empty,
- // then it will exit immediately. Posting the callback at the start of the
- // frame ensures it's fired within the earliest possible frame. If we
- // waited until the end of the frame to post the callback, we risk the
- // browser skipping a frame and not firing the callback until the frame
- // after that.
- requestAnimationFrameWithTimeout(animationTick);
- } else {
- // No pending work. Exit.
- isAnimationFrameScheduled = false;
- return;
- }
-
- var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
- if (
- nextFrameTime < activeFrameTime &&
- previousFrameTime < activeFrameTime
- ) {
- if (nextFrameTime < 8) {
- // Defensive coding. We don't support higher frame rates than 120hz.
- // If the calculated frame time gets lower than 8, it is probably a bug.
- nextFrameTime = 8;
- }
- // If one frame goes long, then the next one can be short to catch up.
- // If two frames are short in a row, then that's an indication that we
- // actually have a higher frame rate than what we're currently optimizing.
- // We adjust our heuristic dynamically accordingly. For example, if we're
- // running on 120hz display or 90hz VR display.
- // Take the max of the two in case one of them was an anomaly due to
- // missed frame deadlines.
- activeFrameTime =
- nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
- } else {
- previousFrameTime = nextFrameTime;
- }
- frameDeadline = rafTime + activeFrameTime;
- if (!isMessageEventScheduled) {
- isMessageEventScheduled = true;
- port.postMessage(undefined);
- }
- };
-
- requestHostCallback = function(callback, absoluteTimeout) {
- scheduledHostCallback = callback;
- timeoutTime = absoluteTimeout;
- if (isFlushingHostCallback || absoluteTimeout < 0) {
- // Don't wait for the next frame. Continue working ASAP, in a new event.
- port.postMessage(undefined);
- } else if (!isAnimationFrameScheduled) {
- // If rAF didn't already schedule one, we need to schedule a frame.
- // TODO: If this rAF doesn't materialize because the browser throttles, we
- // might want to still have setTimeout trigger rIC as a backup to ensure
- // that we keep performing work.
- isAnimationFrameScheduled = true;
- requestAnimationFrameWithTimeout(animationTick);
- }
- };
-
- cancelHostCallback = function() {
- scheduledHostCallback = null;
- isMessageEventScheduled = false;
- timeoutTime = -1;
- };
-}
-
export {
ImmediatePriority as unstable_ImmediatePriority,
UserBlockingPriority as unstable_UserBlockingPriority,
diff --git a/packages/jest-mock-scheduler/index.js b/packages/scheduler/src/SchedulerHostConfig.js
similarity index 70%
rename from packages/jest-mock-scheduler/index.js
rename to packages/scheduler/src/SchedulerHostConfig.js
index c1311b7e23..a4af848702 100644
--- a/packages/jest-mock-scheduler/index.js
+++ b/packages/scheduler/src/SchedulerHostConfig.js
@@ -3,6 +3,8 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
+ *
+ * @flow
*/
-export * from './src/JestMockScheduler';
+throw new Error('This module must be shimmed by a specific build.');
diff --git a/packages/scheduler/src/__tests__/Scheduler-test.js b/packages/scheduler/src/__tests__/Scheduler-test.js
index 8f9b7708de..117daf6916 100644
--- a/packages/scheduler/src/__tests__/Scheduler-test.js
+++ b/packages/scheduler/src/__tests__/Scheduler-test.js
@@ -9,6 +9,7 @@
'use strict';
+let Scheduler;
let runWithPriority;
let ImmediatePriority;
let UserBlockingPriority;
@@ -18,211 +19,44 @@ let cancelCallback;
let wrapCallback;
let getCurrentPriorityLevel;
let shouldYield;
-let flushWork;
-let advanceTime;
-let doWork;
-let yieldedValues;
-let yieldValue;
-let clearYieldedValues;
describe('Scheduler', () => {
beforeEach(() => {
- jest.useFakeTimers();
jest.resetModules();
+ jest.mock('scheduler', () => require('scheduler/unstable_mock'));
- const JestMockScheduler = require('jest-mock-scheduler');
- JestMockScheduler.mockRestore();
+ Scheduler = require('scheduler');
- let _flushWork = null;
- let isFlushing = false;
- let timeoutID = -1;
- let endOfFrame = -1;
- let hasMicrotask = false;
-
- let currentTime = 0;
-
- flushWork = frameSize => {
- if (isFlushing) {
- throw new Error('Already flushing work.');
- }
- if (frameSize === null || frameSize === undefined) {
- frameSize = Infinity;
- }
- if (_flushWork === null) {
- throw new Error('No work is scheduled.');
- }
- timeoutID = -1;
- endOfFrame = currentTime + frameSize;
- try {
- isFlushing = true;
- _flushWork(false);
- } finally {
- isFlushing = false;
- endOfFrame = -1;
- if (hasMicrotask) {
- onTimeout();
- }
- }
- const yields = yieldedValues;
- yieldedValues = [];
- return yields;
- };
-
- advanceTime = ms => {
- currentTime += ms;
- jest.advanceTimersByTime(ms);
- };
-
- doWork = (label, timeCost) => {
- if (typeof timeCost !== 'number') {
- throw new Error('Second arg must be a number.');
- }
- advanceTime(timeCost);
- yieldValue(label);
- };
-
- yieldedValues = [];
- yieldValue = value => {
- yieldedValues.push(value);
- };
-
- clearYieldedValues = () => {
- const yields = yieldedValues;
- yieldedValues = [];
- return yields;
- };
-
- function onTimeout() {
- if (_flushWork === null) {
- return;
- }
- if (isFlushing) {
- hasMicrotask = true;
- } else {
- try {
- isFlushing = true;
- _flushWork(true);
- } finally {
- hasMicrotask = false;
- isFlushing = false;
- }
- }
- }
-
- function requestHostCallback(fw, absoluteTimeout) {
- if (_flushWork !== null) {
- throw new Error('Work is already scheduled.');
- }
- _flushWork = fw;
- timeoutID = setTimeout(onTimeout, absoluteTimeout - currentTime);
- }
- function cancelHostCallback() {
- if (_flushWork === null) {
- throw new Error('No work is scheduled.');
- }
- _flushWork = null;
- clearTimeout(timeoutID);
- }
- function shouldYieldToHost() {
- return endOfFrame <= currentTime;
- }
- function getCurrentTime() {
- return currentTime;
- }
-
- // Override host implementation
- delete global.performance;
- global.Date.now = () => {
- return currentTime;
- };
-
- window._schedMock = [
- requestHostCallback,
- cancelHostCallback,
- shouldYieldToHost,
- getCurrentTime,
- ];
-
- const Schedule = require('scheduler');
- runWithPriority = Schedule.unstable_runWithPriority;
- ImmediatePriority = Schedule.unstable_ImmediatePriority;
- UserBlockingPriority = Schedule.unstable_UserBlockingPriority;
- NormalPriority = Schedule.unstable_NormalPriority;
- scheduleCallback = Schedule.unstable_scheduleCallback;
- cancelCallback = Schedule.unstable_cancelCallback;
- wrapCallback = Schedule.unstable_wrapCallback;
- getCurrentPriorityLevel = Schedule.unstable_getCurrentPriorityLevel;
- shouldYield = Schedule.unstable_shouldYield;
+ runWithPriority = Scheduler.unstable_runWithPriority;
+ ImmediatePriority = Scheduler.unstable_ImmediatePriority;
+ UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
+ NormalPriority = Scheduler.unstable_NormalPriority;
+ scheduleCallback = Scheduler.unstable_scheduleCallback;
+ cancelCallback = Scheduler.unstable_cancelCallback;
+ wrapCallback = Scheduler.unstable_wrapCallback;
+ getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
+ shouldYield = Scheduler.unstable_shouldYield;
});
it('flushes work incrementally', () => {
- scheduleCallback(() => doWork('A', 100));
- scheduleCallback(() => doWork('B', 200));
- scheduleCallback(() => doWork('C', 300));
- scheduleCallback(() => doWork('D', 400));
+ scheduleCallback(() => Scheduler.yieldValue('A'));
+ scheduleCallback(() => Scheduler.yieldValue('B'));
+ scheduleCallback(() => Scheduler.yieldValue('C'));
+ scheduleCallback(() => Scheduler.yieldValue('D'));
- expect(flushWork(300)).toEqual(['A', 'B']);
- expect(flushWork(300)).toEqual(['C']);
- expect(flushWork(400)).toEqual(['D']);
- });
-
- it('flushes work until framesize reached', () => {
- scheduleCallback(() => doWork('A1_100', 100));
- scheduleCallback(() => doWork('A2_200', 200));
- scheduleCallback(() => doWork('B1_100', 100));
- scheduleCallback(() => doWork('B2_200', 200));
- scheduleCallback(() => doWork('C1_300', 300));
- scheduleCallback(() => doWork('C2_300', 300));
- scheduleCallback(() => doWork('D_3000', 3000));
- scheduleCallback(() => doWork('E1_300', 300));
- scheduleCallback(() => doWork('E2_200', 200));
- scheduleCallback(() => doWork('F1_200', 200));
- scheduleCallback(() => doWork('F2_200', 200));
- scheduleCallback(() => doWork('F3_300', 300));
- scheduleCallback(() => doWork('F4_500', 500));
- scheduleCallback(() => doWork('F5_200', 200));
- scheduleCallback(() => doWork('F6_20', 20));
-
- expect(Date.now()).toEqual(0);
- // No time left after A1_100 and A2_200 are run
- expect(flushWork(300)).toEqual(['A1_100', 'A2_200']);
- expect(Date.now()).toEqual(300);
- // B2_200 is started as there is still time left after B1_100
- expect(flushWork(101)).toEqual(['B1_100', 'B2_200']);
- expect(Date.now()).toEqual(600);
- // C1_300 is started as there is even a little frame time
- expect(flushWork(1)).toEqual(['C1_300']);
- expect(Date.now()).toEqual(900);
- // C2_300 is started even though there is no frame time
- expect(flushWork(0)).toEqual(['C2_300']);
- expect(Date.now()).toEqual(1200);
- // D_3000 is very slow, but won't affect next flushes (if no
- // timeouts happen)
- expect(flushWork(100)).toEqual(['D_3000']);
- expect(Date.now()).toEqual(4200);
- expect(flushWork(400)).toEqual(['E1_300', 'E2_200']);
- expect(Date.now()).toEqual(4700);
- // Default timeout is 5000, so during F2_200, work will timeout and are done
- // in reverse, including F2_200
- expect(flushWork(1000)).toEqual([
- 'F1_200',
- 'F2_200',
- 'F3_300',
- 'F4_500',
- 'F5_200',
- 'F6_20',
- ]);
- expect(Date.now()).toEqual(6120);
+ expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
+ expect(Scheduler).toFlushAndYieldThrough(['C']);
+ expect(Scheduler).toFlushAndYield(['D']);
});
it('cancels work', () => {
- scheduleCallback(() => doWork('A', 100));
- const callbackHandleB = scheduleCallback(() => doWork('B', 200));
- scheduleCallback(() => doWork('C', 300));
+ scheduleCallback(() => Scheduler.yieldValue('A'));
+ const callbackHandleB = scheduleCallback(() => Scheduler.yieldValue('B'));
+ scheduleCallback(() => Scheduler.yieldValue('C'));
cancelCallback(callbackHandleB);
- expect(flushWork()).toEqual([
+ expect(Scheduler).toFlushAndYield([
'A',
// B should have been cancelled
'C',
@@ -230,86 +64,100 @@ describe('Scheduler', () => {
});
it('executes the highest priority callbacks first', () => {
- scheduleCallback(() => doWork('A', 100));
- scheduleCallback(() => doWork('B', 100));
+ scheduleCallback(() => Scheduler.yieldValue('A'));
+ scheduleCallback(() => Scheduler.yieldValue('B'));
// Yield before B is flushed
- expect(flushWork(100)).toEqual(['A']);
+ expect(Scheduler).toFlushAndYieldThrough(['A']);
runWithPriority(UserBlockingPriority, () => {
- scheduleCallback(() => doWork('C', 100));
- scheduleCallback(() => doWork('D', 100));
+ scheduleCallback(() => Scheduler.yieldValue('C'));
+ scheduleCallback(() => Scheduler.yieldValue('D'));
});
// C and D should come first, because they are higher priority
- expect(flushWork()).toEqual(['C', 'D', 'B']);
+ expect(Scheduler).toFlushAndYield(['C', 'D', 'B']);
});
it('expires work', () => {
- scheduleCallback(didTimeout =>
- doWork(`A (did timeout: ${didTimeout})`, 100),
- );
- runWithPriority(UserBlockingPriority, () => {
- scheduleCallback(didTimeout =>
- doWork(`B (did timeout: ${didTimeout})`, 100),
- );
+ scheduleCallback(didTimeout => {
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue(`A (did timeout: ${didTimeout})`);
});
runWithPriority(UserBlockingPriority, () => {
- scheduleCallback(didTimeout =>
- doWork(`C (did timeout: ${didTimeout})`, 100),
- );
+ scheduleCallback(didTimeout => {
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue(`B (did timeout: ${didTimeout})`);
+ });
+ });
+ runWithPriority(UserBlockingPriority, () => {
+ scheduleCallback(didTimeout => {
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue(`C (did timeout: ${didTimeout})`);
+ });
});
// Advance time, but not by enough to expire any work
- advanceTime(249);
- expect(clearYieldedValues()).toEqual([]);
+ Scheduler.advanceTime(249);
+ expect(Scheduler).toHaveYielded([]);
// Schedule a few more callbacks
- scheduleCallback(didTimeout =>
- doWork(`D (did timeout: ${didTimeout})`, 100),
- );
- scheduleCallback(didTimeout =>
- doWork(`E (did timeout: ${didTimeout})`, 100),
- );
+ scheduleCallback(didTimeout => {
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue(`D (did timeout: ${didTimeout})`);
+ });
+ scheduleCallback(didTimeout => {
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue(`E (did timeout: ${didTimeout})`);
+ });
// Advance by just a bit more to expire the user blocking callbacks
- advanceTime(1);
- expect(clearYieldedValues()).toEqual([
+ Scheduler.advanceTime(1);
+ expect(Scheduler).toHaveYielded([
'B (did timeout: true)',
'C (did timeout: true)',
]);
// Expire A
- advanceTime(4600);
- expect(clearYieldedValues()).toEqual(['A (did timeout: true)']);
+ Scheduler.advanceTime(4600);
+ expect(Scheduler).toHaveYielded(['A (did timeout: true)']);
// Flush the rest without expiring
- expect(flushWork()).toEqual([
+ expect(Scheduler).toFlushAndYield([
'D (did timeout: false)',
- 'E (did timeout: false)',
+ 'E (did timeout: true)',
]);
});
it('has a default expiration of ~5 seconds', () => {
- scheduleCallback(() => doWork('A', 100));
+ scheduleCallback(() => Scheduler.yieldValue('A'));
- advanceTime(4999);
- expect(clearYieldedValues()).toEqual([]);
+ Scheduler.advanceTime(4999);
+ expect(Scheduler).toHaveYielded([]);
- advanceTime(1);
- expect(clearYieldedValues()).toEqual(['A']);
+ Scheduler.advanceTime(1);
+ expect(Scheduler).toHaveYielded(['A']);
});
it('continues working on same task after yielding', () => {
- scheduleCallback(() => doWork('A', 100));
- scheduleCallback(() => doWork('B', 100));
+ scheduleCallback(() => {
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue('A');
+ });
+ scheduleCallback(() => {
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue('B');
+ });
+ let didYield = false;
const tasks = [['C1', 100], ['C2', 100], ['C3', 100]];
const C = () => {
while (tasks.length > 0) {
- doWork(...tasks.shift());
+ const [label, ms] = tasks.shift();
+ Scheduler.advanceTime(ms);
+ Scheduler.yieldValue(label);
if (shouldYield()) {
- yieldValue('Yield!');
+ didYield = true;
return C;
}
}
@@ -317,21 +165,32 @@ describe('Scheduler', () => {
scheduleCallback(C);
- scheduleCallback(() => doWork('D', 100));
- scheduleCallback(() => doWork('E', 100));
+ scheduleCallback(() => {
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue('D');
+ });
+ scheduleCallback(() => {
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue('E');
+ });
- expect(flushWork(300)).toEqual(['A', 'B', 'C1', 'Yield!']);
+ // Flush, then yield while in the middle of C.
+ expect(didYield).toBe(false);
+ expect(Scheduler).toFlushAndYieldThrough(['A', 'B', 'C1']);
+ expect(didYield).toBe(true);
- expect(flushWork()).toEqual(['C2', 'C3', 'D', 'E']);
+ // When we resume, we should continue working on C.
+ expect(Scheduler).toFlushAndYield(['C2', 'C3', 'D', 'E']);
});
it('continuation callbacks inherit the expiration of the previous callback', () => {
const tasks = [['A', 125], ['B', 124], ['C', 100], ['D', 100]];
const work = () => {
while (tasks.length > 0) {
- doWork(...tasks.shift());
+ const [label, ms] = tasks.shift();
+ Scheduler.advanceTime(ms);
+ Scheduler.yieldValue(label);
if (shouldYield()) {
- yieldValue('Yield!');
return work;
}
}
@@ -341,50 +200,56 @@ describe('Scheduler', () => {
runWithPriority(UserBlockingPriority, () => scheduleCallback(work));
// Flush until just before the expiration time
- expect(flushWork(249)).toEqual(['A', 'B', 'Yield!']);
+ expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
// Advance time by just a bit more. This should expire all the remaining work.
- advanceTime(1);
- expect(clearYieldedValues()).toEqual(['C', 'D']);
+ Scheduler.advanceTime(1);
+ expect(Scheduler).toHaveYielded(['C', 'D']);
});
it('nested callbacks inherit the priority of the currently executing callback', () => {
runWithPriority(UserBlockingPriority, () => {
scheduleCallback(() => {
- doWork('Parent callback', 100);
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue('Parent callback');
scheduleCallback(() => {
- doWork('Nested callback', 100);
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue('Nested callback');
});
});
});
- expect(flushWork(100)).toEqual(['Parent callback']);
+ expect(Scheduler).toFlushAndYieldThrough(['Parent callback']);
// The nested callback has user-blocking priority, so it should
// expire quickly.
- advanceTime(250 + 100);
- expect(clearYieldedValues()).toEqual(['Nested callback']);
+ Scheduler.advanceTime(250 + 100);
+ expect(Scheduler).toHaveYielded(['Nested callback']);
});
it('continuations are interrupted by higher priority work', () => {
const tasks = [['A', 100], ['B', 100], ['C', 100], ['D', 100]];
const work = () => {
while (tasks.length > 0) {
- doWork(...tasks.shift());
+ const [label, ms] = tasks.shift();
+ Scheduler.advanceTime(ms);
+ Scheduler.yieldValue(label);
if (tasks.length > 0 && shouldYield()) {
- yieldValue('Yield!');
return work;
}
}
};
scheduleCallback(work);
- expect(flushWork(100)).toEqual(['A', 'Yield!']);
+ expect(Scheduler).toFlushAndYieldThrough(['A']);
runWithPriority(UserBlockingPriority, () => {
- scheduleCallback(() => doWork('High pri', 100));
+ scheduleCallback(() => {
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue('High pri');
+ });
});
- expect(flushWork()).toEqual(['High pri', 'B', 'C', 'D']);
+ expect(Scheduler).toFlushAndYield(['High pri', 'B', 'C', 'D']);
});
it(
@@ -395,22 +260,27 @@ describe('Scheduler', () => {
const work = () => {
while (tasks.length > 0) {
const task = tasks.shift();
- doWork(...task);
+ const [label, ms] = task;
+ Scheduler.advanceTime(ms);
+ Scheduler.yieldValue(label);
if (task[0] === 'B') {
// Schedule high pri work from inside another callback
- yieldValue('Schedule high pri');
+ Scheduler.yieldValue('Schedule high pri');
runWithPriority(UserBlockingPriority, () =>
- scheduleCallback(() => doWork('High pri', 100)),
+ scheduleCallback(() => {
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue('High pri');
+ }),
);
}
if (tasks.length > 0 && shouldYield()) {
- yieldValue('Yield!');
+ Scheduler.yieldValue('Yield!');
return work;
}
}
};
scheduleCallback(work);
- expect(flushWork()).toEqual([
+ expect(Scheduler).toFlushAndYield([
'A',
'B',
'Schedule high pri',
@@ -427,19 +297,19 @@ describe('Scheduler', () => {
it('immediate callbacks fire at the end of outermost event', () => {
runWithPriority(ImmediatePriority, () => {
- scheduleCallback(() => yieldValue('A'));
- scheduleCallback(() => yieldValue('B'));
+ scheduleCallback(() => Scheduler.yieldValue('A'));
+ scheduleCallback(() => Scheduler.yieldValue('B'));
// Nested event
runWithPriority(ImmediatePriority, () => {
- scheduleCallback(() => yieldValue('C'));
+ scheduleCallback(() => Scheduler.yieldValue('C'));
// Nothing should have fired yet
- expect(clearYieldedValues()).toEqual([]);
+ expect(Scheduler).toHaveYielded([]);
});
// Nothing should have fired yet
- expect(clearYieldedValues()).toEqual([]);
+ expect(Scheduler).toHaveYielded([]);
});
// The callbacks were called at the end of the outer event
- expect(clearYieldedValues()).toEqual(['A', 'B', 'C']);
+ expect(Scheduler).toHaveYielded(['A', 'B', 'C']);
});
it('wrapped callbacks have same signature as original callback', () => {
@@ -450,7 +320,8 @@ describe('Scheduler', () => {
it('wrapped callbacks inherit the current priority', () => {
const wrappedCallback = wrapCallback(() => {
scheduleCallback(() => {
- doWork('Normal', 100);
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue('Normal');
});
});
const wrappedInteractiveCallback = runWithPriority(
@@ -458,7 +329,8 @@ describe('Scheduler', () => {
() =>
wrapCallback(() => {
scheduleCallback(() => {
- doWork('User-blocking', 100);
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue('User-blocking');
});
}),
);
@@ -468,19 +340,20 @@ describe('Scheduler', () => {
// This should schedule an user-blocking callback
wrappedInteractiveCallback();
- advanceTime(249);
- expect(clearYieldedValues()).toEqual([]);
- advanceTime(1);
- expect(clearYieldedValues()).toEqual(['User-blocking']);
+ Scheduler.advanceTime(249);
+ expect(Scheduler).toHaveYielded([]);
+ Scheduler.advanceTime(1);
+ expect(Scheduler).toHaveYielded(['User-blocking']);
- advanceTime(10000);
- expect(clearYieldedValues()).toEqual(['Normal']);
+ Scheduler.advanceTime(10000);
+ expect(Scheduler).toHaveYielded(['Normal']);
});
it('wrapped callbacks inherit the current priority even when nested', () => {
const wrappedCallback = wrapCallback(() => {
scheduleCallback(() => {
- doWork('Normal', 100);
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue('Normal');
});
});
const wrappedInteractiveCallback = runWithPriority(
@@ -488,7 +361,8 @@ describe('Scheduler', () => {
() =>
wrapCallback(() => {
scheduleCallback(() => {
- doWork('User-blocking', 100);
+ Scheduler.advanceTime(100);
+ Scheduler.yieldValue('User-blocking');
});
}),
);
@@ -500,66 +374,66 @@ describe('Scheduler', () => {
wrappedInteractiveCallback();
});
- advanceTime(249);
- expect(clearYieldedValues()).toEqual([]);
- advanceTime(1);
- expect(clearYieldedValues()).toEqual(['User-blocking']);
+ Scheduler.advanceTime(249);
+ expect(Scheduler).toHaveYielded([]);
+ Scheduler.advanceTime(1);
+ expect(Scheduler).toHaveYielded(['User-blocking']);
- advanceTime(10000);
- expect(clearYieldedValues()).toEqual(['Normal']);
+ Scheduler.advanceTime(10000);
+ expect(Scheduler).toHaveYielded(['Normal']);
});
it('immediate callbacks fire at the end of callback', () => {
const immediateCallback = runWithPriority(ImmediatePriority, () =>
wrapCallback(() => {
- scheduleCallback(() => yieldValue('callback'));
+ scheduleCallback(() => Scheduler.yieldValue('callback'));
}),
);
immediateCallback();
// The callback was called at the end of the outer event
- expect(clearYieldedValues()).toEqual(['callback']);
+ expect(Scheduler).toHaveYielded(['callback']);
});
it("immediate callbacks fire even if there's an error", () => {
expect(() => {
runWithPriority(ImmediatePriority, () => {
scheduleCallback(() => {
- yieldValue('A');
+ Scheduler.yieldValue('A');
throw new Error('Oops A');
});
scheduleCallback(() => {
- yieldValue('B');
+ Scheduler.yieldValue('B');
});
scheduleCallback(() => {
- yieldValue('C');
+ Scheduler.yieldValue('C');
throw new Error('Oops C');
});
});
}).toThrow('Oops A');
- expect(clearYieldedValues()).toEqual(['A']);
+ expect(Scheduler).toHaveYielded(['A']);
// B and C flush in a subsequent event. That way, the second error is not
// swallowed.
- expect(() => flushWork(0)).toThrow('Oops C');
- expect(clearYieldedValues()).toEqual(['B', 'C']);
+ expect(() => Scheduler.unstable_flushExpired()).toThrow('Oops C');
+ expect(Scheduler).toHaveYielded(['B', 'C']);
});
it('exposes the current priority level', () => {
- yieldValue(getCurrentPriorityLevel());
+ Scheduler.yieldValue(getCurrentPriorityLevel());
runWithPriority(ImmediatePriority, () => {
- yieldValue(getCurrentPriorityLevel());
+ Scheduler.yieldValue(getCurrentPriorityLevel());
runWithPriority(NormalPriority, () => {
- yieldValue(getCurrentPriorityLevel());
+ Scheduler.yieldValue(getCurrentPriorityLevel());
runWithPriority(UserBlockingPriority, () => {
- yieldValue(getCurrentPriorityLevel());
+ Scheduler.yieldValue(getCurrentPriorityLevel());
});
});
- yieldValue(getCurrentPriorityLevel());
+ Scheduler.yieldValue(getCurrentPriorityLevel());
});
- expect(clearYieldedValues()).toEqual([
+ expect(Scheduler).toHaveYielded([
NormalPriority,
ImmediatePriority,
NormalPriority,
diff --git a/packages/scheduler/src/__tests__/SchedulerDOM-test.js b/packages/scheduler/src/__tests__/SchedulerDOM-test.js
index 77fac5ffe6..7565e84454 100644
--- a/packages/scheduler/src/__tests__/SchedulerDOM-test.js
+++ b/packages/scheduler/src/__tests__/SchedulerDOM-test.js
@@ -89,8 +89,13 @@ describe('SchedulerDOM', () => {
};
jest.resetModules();
- const JestMockScheduler = require('jest-mock-scheduler');
- JestMockScheduler.mockRestore();
+ // Un-mock scheduler
+ jest.mock('scheduler', () => require.requireActual('scheduler'));
+ jest.mock('scheduler/src/SchedulerHostConfig', () =>
+ require.requireActual(
+ 'scheduler/src/forks/SchedulerHostConfig.default.js',
+ ),
+ );
Scheduler = require('scheduler');
});
diff --git a/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js b/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js
index 5a6aa12861..55d47037e0 100644
--- a/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js
+++ b/packages/scheduler/src/__tests__/SchedulerNoDOM-test.js
@@ -19,10 +19,17 @@ describe('SchedulerNoDOM', () => {
// implementation using setTimeout. This only meant to be used for testing
// purposes, like with jest's fake timer API.
beforeEach(() => {
- jest.useFakeTimers();
jest.resetModules();
- // Delete addEventListener to force us into the fallback mode.
- window.addEventListener = undefined;
+ jest.useFakeTimers();
+
+ // Un-mock scheduler
+ jest.mock('scheduler', () => require.requireActual('scheduler'));
+ jest.mock('scheduler/src/SchedulerHostConfig', () =>
+ require.requireActual(
+ 'scheduler/src/forks/SchedulerHostConfig.default.js',
+ ),
+ );
+
const Scheduler = require('scheduler');
scheduleCallback = Scheduler.unstable_scheduleCallback;
runWithPriority = Scheduler.unstable_runWithPriority;
@@ -69,36 +76,6 @@ describe('SchedulerNoDOM', () => {
expect(log).toEqual(['C', 'D', 'A', 'B']);
});
- it('advanceTimersByTime expires callbacks incrementally', () => {
- let log = [];
-
- scheduleCallback(() => {
- log.push('A');
- });
- scheduleCallback(() => {
- log.push('B');
- });
- runWithPriority(UserBlockingPriority, () => {
- scheduleCallback(() => {
- log.push('C');
- });
- scheduleCallback(() => {
- log.push('D');
- });
- });
-
- expect(log).toEqual([]);
- jest.advanceTimersByTime(249);
- expect(log).toEqual([]);
- jest.advanceTimersByTime(1);
- expect(log).toEqual(['C', 'D']);
-
- log = [];
-
- jest.runAllTimers();
- expect(log).toEqual(['A', 'B']);
- });
-
it('calls immediate callbacks immediately', () => {
let log = [];
diff --git a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js
index b22d2c2770..9812ac55b9 100644
--- a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js
+++ b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js
@@ -14,6 +14,13 @@ describe('Scheduling UMD bundle', () => {
global.__UMD__ = true;
jest.resetModules();
+
+ jest.mock('scheduler', () => require.requireActual('scheduler'));
+ jest.mock('scheduler/src/SchedulerHostConfig', () =>
+ require.requireActual(
+ 'scheduler/src/forks/SchedulerHostConfig.default.js',
+ ),
+ );
});
function filterPrivateKeys(name) {
diff --git a/packages/scheduler/src/forks/SchedulerHostConfig.default.js b/packages/scheduler/src/forks/SchedulerHostConfig.default.js
new file mode 100644
index 0000000000..fd6bdea743
--- /dev/null
+++ b/packages/scheduler/src/forks/SchedulerHostConfig.default.js
@@ -0,0 +1,264 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+// The DOM Scheduler implementation is similar to requestIdleCallback. It
+// works by scheduling a requestAnimationFrame, storing the time for the start
+// of the frame, then scheduling a postMessage which gets scheduled after paint.
+// Within the postMessage handler do as much work as possible until time + frame
+// rate. By separating the idle call into a separate event tick we ensure that
+// layout, paint and other browser work is counted against the available time.
+// The frame rate is dynamically adjusted.
+
+export let requestHostCallback;
+export let cancelHostCallback;
+export let shouldYieldToHost;
+export let getCurrentTime;
+
+const hasNativePerformanceNow =
+ typeof performance === 'object' && typeof performance.now === 'function';
+
+// We capture a local reference to any global, in case it gets polyfilled after
+// this module is initially evaluated. We want to be using a
+// consistent implementation.
+const localDate = Date;
+
+// This initialization code may run even on server environments if a component
+// just imports ReactDOM (e.g. for findDOMNode). Some environments might not
+// have setTimeout or clearTimeout. However, we always expect them to be defined
+// on the client. https://github.com/facebook/react/pull/13088
+const localSetTimeout =
+ typeof setTimeout === 'function' ? setTimeout : undefined;
+const localClearTimeout =
+ typeof clearTimeout === 'function' ? clearTimeout : undefined;
+
+// We don't expect either of these to necessarily be defined, but we will error
+// later if they are missing on the client.
+const localRequestAnimationFrame =
+ typeof requestAnimationFrame === 'function'
+ ? requestAnimationFrame
+ : undefined;
+const localCancelAnimationFrame =
+ typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
+
+// requestAnimationFrame does not run when the tab is in the background. If
+// we're backgrounded we prefer for that work to happen so that the page
+// continues to load in the background. So we also schedule a 'setTimeout' as
+// a fallback.
+// TODO: Need a better heuristic for backgrounded work.
+const ANIMATION_FRAME_TIMEOUT = 100;
+let rAFID;
+let rAFTimeoutID;
+const requestAnimationFrameWithTimeout = function(callback) {
+ // schedule rAF and also a setTimeout
+ rAFID = localRequestAnimationFrame(function(timestamp) {
+ // cancel the setTimeout
+ localClearTimeout(rAFTimeoutID);
+ callback(timestamp);
+ });
+ rAFTimeoutID = localSetTimeout(function() {
+ // cancel the requestAnimationFrame
+ localCancelAnimationFrame(rAFID);
+ callback(getCurrentTime());
+ }, ANIMATION_FRAME_TIMEOUT);
+};
+
+if (hasNativePerformanceNow) {
+ const Performance = performance;
+ getCurrentTime = function() {
+ return Performance.now();
+ };
+} else {
+ getCurrentTime = function() {
+ return localDate.now();
+ };
+}
+
+if (
+ // If Scheduler runs in a non-DOM environment, it falls back to a naive
+ // implementation using setTimeout.
+ typeof window === 'undefined' ||
+ // Check if MessageChannel is supported, too.
+ typeof MessageChannel !== 'function'
+) {
+ // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
+ // fallback to a naive implementation.
+ let _callback = null;
+ const _flushCallback = function(didTimeout) {
+ if (_callback !== null) {
+ try {
+ _callback(didTimeout);
+ } finally {
+ _callback = null;
+ }
+ }
+ };
+ requestHostCallback = function(cb, ms) {
+ if (_callback !== null) {
+ // Protect against re-entrancy.
+ setTimeout(requestHostCallback, 0, cb);
+ } else {
+ _callback = cb;
+ setTimeout(_flushCallback, 0, false);
+ }
+ };
+ cancelHostCallback = function() {
+ _callback = null;
+ };
+ shouldYieldToHost = function() {
+ return false;
+ };
+} else {
+ if (typeof console !== 'undefined') {
+ // TODO: Remove fb.me link
+ if (typeof localRequestAnimationFrame !== 'function') {
+ console.error(
+ "This browser doesn't support requestAnimationFrame. " +
+ 'Make sure that you load a ' +
+ 'polyfill in older browsers. https://fb.me/react-polyfills',
+ );
+ }
+ if (typeof localCancelAnimationFrame !== 'function') {
+ console.error(
+ "This browser doesn't support cancelAnimationFrame. " +
+ 'Make sure that you load a ' +
+ 'polyfill in older browsers. https://fb.me/react-polyfills',
+ );
+ }
+ }
+
+ let scheduledHostCallback = null;
+ let isMessageEventScheduled = false;
+ let timeoutTime = -1;
+
+ let isAnimationFrameScheduled = false;
+
+ let isFlushingHostCallback = false;
+
+ let frameDeadline = 0;
+ // We start out assuming that we run at 30fps but then the heuristic tracking
+ // will adjust this value to a faster fps if we get more frequent animation
+ // frames.
+ let previousFrameTime = 33;
+ let activeFrameTime = 33;
+
+ shouldYieldToHost = function() {
+ return frameDeadline <= getCurrentTime();
+ };
+
+ // We use the postMessage trick to defer idle work until after the repaint.
+ const channel = new MessageChannel();
+ const port = channel.port2;
+ channel.port1.onmessage = function(event) {
+ isMessageEventScheduled = false;
+
+ const prevScheduledCallback = scheduledHostCallback;
+ const prevTimeoutTime = timeoutTime;
+ scheduledHostCallback = null;
+ timeoutTime = -1;
+
+ const currentTime = getCurrentTime();
+
+ let didTimeout = false;
+ if (frameDeadline - currentTime <= 0) {
+ // There's no time left in this idle period. Check if the callback has
+ // a timeout and whether it's been exceeded.
+ if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
+ // Exceeded the timeout. Invoke the callback even though there's no
+ // time left.
+ didTimeout = true;
+ } else {
+ // No timeout.
+ if (!isAnimationFrameScheduled) {
+ // Schedule another animation callback so we retry later.
+ isAnimationFrameScheduled = true;
+ requestAnimationFrameWithTimeout(animationTick);
+ }
+ // Exit without invoking the callback.
+ scheduledHostCallback = prevScheduledCallback;
+ timeoutTime = prevTimeoutTime;
+ return;
+ }
+ }
+
+ if (prevScheduledCallback !== null) {
+ isFlushingHostCallback = true;
+ try {
+ prevScheduledCallback(didTimeout);
+ } finally {
+ isFlushingHostCallback = false;
+ }
+ }
+ };
+
+ const animationTick = function(rafTime) {
+ if (scheduledHostCallback !== null) {
+ // Eagerly schedule the next animation callback at the beginning of the
+ // frame. If the scheduler queue is not empty at the end of the frame, it
+ // will continue flushing inside that callback. If the queue *is* empty,
+ // then it will exit immediately. Posting the callback at the start of the
+ // frame ensures it's fired within the earliest possible frame. If we
+ // waited until the end of the frame to post the callback, we risk the
+ // browser skipping a frame and not firing the callback until the frame
+ // after that.
+ requestAnimationFrameWithTimeout(animationTick);
+ } else {
+ // No pending work. Exit.
+ isAnimationFrameScheduled = false;
+ return;
+ }
+
+ let nextFrameTime = rafTime - frameDeadline + activeFrameTime;
+ if (
+ nextFrameTime < activeFrameTime &&
+ previousFrameTime < activeFrameTime
+ ) {
+ if (nextFrameTime < 8) {
+ // Defensive coding. We don't support higher frame rates than 120hz.
+ // If the calculated frame time gets lower than 8, it is probably a bug.
+ nextFrameTime = 8;
+ }
+ // If one frame goes long, then the next one can be short to catch up.
+ // If two frames are short in a row, then that's an indication that we
+ // actually have a higher frame rate than what we're currently optimizing.
+ // We adjust our heuristic dynamically accordingly. For example, if we're
+ // running on 120hz display or 90hz VR display.
+ // Take the max of the two in case one of them was an anomaly due to
+ // missed frame deadlines.
+ activeFrameTime =
+ nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
+ } else {
+ previousFrameTime = nextFrameTime;
+ }
+ frameDeadline = rafTime + activeFrameTime;
+ if (!isMessageEventScheduled) {
+ isMessageEventScheduled = true;
+ port.postMessage(undefined);
+ }
+ };
+
+ requestHostCallback = function(callback, absoluteTimeout) {
+ scheduledHostCallback = callback;
+ timeoutTime = absoluteTimeout;
+ if (isFlushingHostCallback || absoluteTimeout < 0) {
+ // Don't wait for the next frame. Continue working ASAP, in a new event.
+ port.postMessage(undefined);
+ } else if (!isAnimationFrameScheduled) {
+ // If rAF didn't already schedule one, we need to schedule a frame.
+ // TODO: If this rAF doesn't materialize because the browser throttles, we
+ // might want to still have setTimeout trigger rIC as a backup to ensure
+ // that we keep performing work.
+ isAnimationFrameScheduled = true;
+ requestAnimationFrameWithTimeout(animationTick);
+ }
+ };
+
+ cancelHostCallback = function() {
+ scheduledHostCallback = null;
+ isMessageEventScheduled = false;
+ timeoutTime = -1;
+ };
+}
diff --git a/packages/scheduler/src/forks/SchedulerHostConfig.mock.js b/packages/scheduler/src/forks/SchedulerHostConfig.mock.js
new file mode 100644
index 0000000000..1fda4d2ca7
--- /dev/null
+++ b/packages/scheduler/src/forks/SchedulerHostConfig.mock.js
@@ -0,0 +1,167 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+let currentTime: number = 0;
+let scheduledCallback: (boolean => void) | null = null;
+let scheduledCallbackExpiration: number = -1;
+let yieldedValues: Array | null = null;
+let expectedNumberOfYields: number = -1;
+let didStop: boolean = false;
+let isFlushing: boolean = false;
+
+export function requestHostCallback(
+ callback: boolean => void,
+ expiration: number,
+) {
+ scheduledCallback = callback;
+ scheduledCallbackExpiration = expiration;
+}
+
+export function cancelHostCallback(): void {
+ scheduledCallback = null;
+ scheduledCallbackExpiration = -1;
+}
+
+export function shouldYieldToHost(): boolean {
+ if (
+ (expectedNumberOfYields !== -1 &&
+ yieldedValues !== null &&
+ yieldedValues.length >= expectedNumberOfYields) ||
+ (scheduledCallbackExpiration !== -1 &&
+ scheduledCallbackExpiration <= currentTime)
+ ) {
+ // We yielded at least as many values as expected. Stop flushing.
+ didStop = true;
+ return true;
+ }
+ return false;
+}
+
+export function getCurrentTime(): number {
+ return currentTime;
+}
+
+export function reset() {
+ if (isFlushing) {
+ throw new Error('Cannot reset while already flushing work.');
+ }
+ currentTime = 0;
+ scheduledCallback = null;
+ scheduledCallbackExpiration = -1;
+ yieldedValues = null;
+ expectedNumberOfYields = -1;
+ didStop = false;
+ isFlushing = false;
+}
+
+// Should only be used via an assertion helper that inspects the yielded values.
+export function unstable_flushNumberOfYields(count: number): void {
+ if (isFlushing) {
+ throw new Error('Already flushing work.');
+ }
+ expectedNumberOfYields = count;
+ isFlushing = true;
+ try {
+ while (scheduledCallback !== null && !didStop) {
+ const cb = scheduledCallback;
+ scheduledCallback = null;
+ const didTimeout =
+ scheduledCallbackExpiration !== -1 &&
+ scheduledCallbackExpiration <= currentTime;
+ cb(didTimeout);
+ }
+ } finally {
+ expectedNumberOfYields = -1;
+ didStop = false;
+ isFlushing = false;
+ }
+}
+
+export function unstable_flushExpired() {
+ if (isFlushing) {
+ throw new Error('Already flushing work.');
+ }
+ if (scheduledCallback !== null) {
+ const cb = scheduledCallback;
+ scheduledCallback = null;
+ isFlushing = true;
+ try {
+ cb(true);
+ } finally {
+ isFlushing = false;
+ }
+ }
+}
+
+export function unstable_flushWithoutYielding(): void {
+ if (isFlushing) {
+ throw new Error('Already flushing work.');
+ }
+ isFlushing = true;
+ try {
+ while (scheduledCallback !== null) {
+ const cb = scheduledCallback;
+ scheduledCallback = null;
+ const didTimeout =
+ scheduledCallbackExpiration !== -1 &&
+ scheduledCallbackExpiration <= currentTime;
+ cb(didTimeout);
+ }
+ } finally {
+ expectedNumberOfYields = -1;
+ didStop = false;
+ isFlushing = false;
+ }
+}
+
+export function unstable_clearYields(): Array {
+ if (yieldedValues === null) {
+ return [];
+ }
+ const values = yieldedValues;
+ yieldedValues = null;
+ return values;
+}
+
+export function flushAll(): void {
+ if (yieldedValues !== null) {
+ throw new Error(
+ 'Log is not empty. Assert on the log of yielded values before ' +
+ 'flushing additional work.',
+ );
+ }
+ unstable_flushWithoutYielding();
+ if (yieldedValues !== null) {
+ throw new Error(
+ 'While flushing work, something yielded a value. Use an ' +
+ 'assertion helper to assert on the log of yielded values, e.g. ' +
+ 'expect(Scheduler).toFlushAndYield([...])',
+ );
+ }
+}
+
+export function yieldValue(value: mixed): void {
+ if (yieldedValues === null) {
+ yieldedValues = [value];
+ } else {
+ yieldedValues.push(value);
+ }
+}
+
+export function advanceTime(ms: number) {
+ currentTime += ms;
+ // If the host callback timed out, flush the expired work.
+ if (
+ !isFlushing &&
+ scheduledCallbackExpiration !== -1 &&
+ scheduledCallbackExpiration <= currentTime
+ ) {
+ unstable_flushExpired();
+ }
+}
diff --git a/packages/scheduler/unstable_mock.js b/packages/scheduler/unstable_mock.js
new file mode 100644
index 0000000000..8ab48d336b
--- /dev/null
+++ b/packages/scheduler/unstable_mock.js
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+'use strict';
+
+export * from './src/Scheduler';
+
+export {
+ unstable_flushWithoutYielding,
+ unstable_flushNumberOfYields,
+ unstable_flushExpired,
+ unstable_clearYields,
+ flushAll,
+ yieldValue,
+ advanceTime,
+} from './src/SchedulerHostConfig.js';
diff --git a/packages/shared/__tests__/ReactDOMFrameScheduling-test.js b/packages/shared/__tests__/ReactDOMFrameScheduling-test.js
index b23ce3a7fd..fa3d145e6c 100644
--- a/packages/shared/__tests__/ReactDOMFrameScheduling-test.js
+++ b/packages/shared/__tests__/ReactDOMFrameScheduling-test.js
@@ -10,6 +10,18 @@
'use strict';
describe('ReactDOMFrameScheduling', () => {
+ beforeEach(() => {
+ jest.resetModules();
+
+ // Un-mock scheduler
+ jest.mock('scheduler', () => require.requireActual('scheduler'));
+ jest.mock('scheduler/src/SchedulerHostConfig', () =>
+ require.requireActual(
+ 'scheduler/src/forks/SchedulerHostConfig.default.js',
+ ),
+ );
+ });
+
it('warns when requestAnimationFrame is not polyfilled in the browser', () => {
const previousRAF = global.requestAnimationFrame;
const previousMessageChannel = global.MessageChannel;
@@ -21,11 +33,6 @@ describe('ReactDOMFrameScheduling', () => {
port2: {},
};
};
- jest.resetModules();
-
- const JestMockScheduler = require('jest-mock-scheduler');
- JestMockScheduler.mockRestore();
-
spyOnDevAndProd(console, 'error');
require('react-dom');
expect(console.error.calls.count()).toEqual(1);
diff --git a/scripts/jest/config.build.js b/scripts/jest/config.build.js
index cdb3750357..1fcc1d314e 100644
--- a/scripts/jest/config.build.js
+++ b/scripts/jest/config.build.js
@@ -35,7 +35,7 @@ packages.forEach(name => {
moduleNameMapper[`^${name}$`] = `/build/node_modules/${name}`;
// Named entry points
moduleNameMapper[
- `^${name}/(.*)$`
+ `^${name}\/([^\/]+)$`
] = `/build/node_modules/${name}/$1`;
});
@@ -46,4 +46,8 @@ module.exports = Object.assign({}, baseConfig, {
testPathIgnorePatterns: ['/node_modules/', '-test.internal.js$'],
// Exclude the build output from transforms
transformIgnorePatterns: ['/node_modules/', '/build/'],
+ setupFiles: [
+ ...baseConfig.setupFiles,
+ require.resolve('./setupTests.build.js'),
+ ],
});
diff --git a/scripts/jest/matchers/reactTestMatchers.js b/scripts/jest/matchers/reactTestMatchers.js
index a143e7211c..fb3d01f549 100644
--- a/scripts/jest/matchers/reactTestMatchers.js
+++ b/scripts/jest/matchers/reactTestMatchers.js
@@ -1,6 +1,7 @@
'use strict';
const JestReact = require('jest-react');
+const SchedulerMatchers = require('./schedulerTestMatchers');
function captureAssertion(fn) {
// Trick to use a Jest matcher inside another Jest matcher. `fn` contains an
@@ -18,6 +19,10 @@ function captureAssertion(fn) {
return {pass: true};
}
+function isScheduler(obj) {
+ return typeof obj.unstable_scheduleCallback === 'function';
+}
+
function isReactNoop(obj) {
return typeof obj.hasScheduledCallback === 'function';
}
@@ -33,6 +38,9 @@ function assertYieldsWereCleared(ReactNoop) {
}
function toFlushAndYield(ReactNoop, expectedYields) {
+ if (isScheduler(ReactNoop)) {
+ return SchedulerMatchers.toFlushAndYield(ReactNoop, expectedYields);
+ }
if (!isReactNoop(ReactNoop)) {
return JestReact.unstable_toFlushAndYield(ReactNoop, expectedYields);
}
@@ -44,6 +52,9 @@ function toFlushAndYield(ReactNoop, expectedYields) {
}
function toFlushAndYieldThrough(ReactNoop, expectedYields) {
+ if (isScheduler(ReactNoop)) {
+ return SchedulerMatchers.toFlushAndYieldThrough(ReactNoop, expectedYields);
+ }
if (!isReactNoop(ReactNoop)) {
return JestReact.unstable_toFlushAndYieldThrough(ReactNoop, expectedYields);
}
@@ -57,6 +68,9 @@ function toFlushAndYieldThrough(ReactNoop, expectedYields) {
}
function toFlushWithoutYielding(ReactNoop) {
+ if (isScheduler(ReactNoop)) {
+ return SchedulerMatchers.toFlushWithoutYielding(ReactNoop);
+ }
if (!isReactNoop(ReactNoop)) {
return JestReact.unstable_toFlushWithoutYielding(ReactNoop);
}
@@ -64,6 +78,9 @@ function toFlushWithoutYielding(ReactNoop) {
}
function toHaveYielded(ReactNoop, expectedYields) {
+ if (isScheduler(ReactNoop)) {
+ return SchedulerMatchers.toHaveYielded(ReactNoop, expectedYields);
+ }
if (!isReactNoop(ReactNoop)) {
return JestReact.unstable_toHaveYielded(ReactNoop, expectedYields);
}
@@ -74,6 +91,9 @@ function toHaveYielded(ReactNoop, expectedYields) {
}
function toFlushAndThrow(ReactNoop, ...rest) {
+ if (isScheduler(ReactNoop)) {
+ return SchedulerMatchers.toFlushAndThrow(ReactNoop, ...rest);
+ }
if (!isReactNoop(ReactNoop)) {
return JestReact.unstable_toFlushAndThrow(ReactNoop, ...rest);
}
diff --git a/scripts/jest/matchers/schedulerTestMatchers.js b/scripts/jest/matchers/schedulerTestMatchers.js
new file mode 100644
index 0000000000..4984ea42b5
--- /dev/null
+++ b/scripts/jest/matchers/schedulerTestMatchers.js
@@ -0,0 +1,73 @@
+'use strict';
+
+function captureAssertion(fn) {
+ // Trick to use a Jest matcher inside another Jest matcher. `fn` contains an
+ // assertion; if it throws, we capture the error and return it, so the stack
+ // trace presented to the user points to the original assertion in the
+ // test file.
+ try {
+ fn();
+ } catch (error) {
+ return {
+ pass: false,
+ message: () => error.message,
+ };
+ }
+ return {pass: true};
+}
+
+function assertYieldsWereCleared(Scheduler) {
+ const actualYields = Scheduler.unstable_clearYields();
+ if (actualYields.length !== 0) {
+ throw new Error(
+ 'Log of yielded values is not empty. ' +
+ 'Call expect(Scheduler).toHaveYielded(...) first.'
+ );
+ }
+}
+
+function toFlushAndYield(Scheduler, expectedYields) {
+ assertYieldsWereCleared(Scheduler);
+ Scheduler.unstable_flushWithoutYielding();
+ const actualYields = Scheduler.unstable_clearYields();
+ return captureAssertion(() => {
+ expect(actualYields).toEqual(expectedYields);
+ });
+}
+
+function toFlushAndYieldThrough(Scheduler, expectedYields) {
+ assertYieldsWereCleared(Scheduler);
+ Scheduler.unstable_flushNumberOfYields(expectedYields.length);
+ const actualYields = Scheduler.unstable_clearYields();
+ return captureAssertion(() => {
+ expect(actualYields).toEqual(expectedYields);
+ });
+}
+
+function toFlushWithoutYielding(Scheduler) {
+ return toFlushAndYield(Scheduler, []);
+}
+
+function toHaveYielded(Scheduler, expectedYields) {
+ return captureAssertion(() => {
+ const actualYields = Scheduler.unstable_clearYields();
+ expect(actualYields).toEqual(expectedYields);
+ });
+}
+
+function toFlushAndThrow(Scheduler, ...rest) {
+ assertYieldsWereCleared(Scheduler);
+ return captureAssertion(() => {
+ expect(() => {
+ Scheduler.unstable_flushWithoutYielding();
+ }).toThrow(...rest);
+ });
+}
+
+module.exports = {
+ toFlushAndYield,
+ toFlushAndYieldThrough,
+ toFlushWithoutYielding,
+ toHaveYielded,
+ toFlushAndThrow,
+};
diff --git a/scripts/jest/setupHostConfigs.js b/scripts/jest/setupHostConfigs.js
index d7f4aab367..b4036f52c9 100644
--- a/scripts/jest/setupHostConfigs.js
+++ b/scripts/jest/setupHostConfigs.js
@@ -111,3 +111,8 @@ inlinedHostConfigs.forEach(rendererInfo => {
jest.mock('shared/ReactSharedInternals', () =>
require.requireActual('react/src/ReactSharedInternals')
);
+
+jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));
+jest.mock('scheduler/src/SchedulerHostConfig', () =>
+ require.requireActual('scheduler/src/forks/SchedulerHostConfig.mock.js')
+);
diff --git a/scripts/jest/setupTests.build.js b/scripts/jest/setupTests.build.js
new file mode 100644
index 0000000000..62db0fc007
--- /dev/null
+++ b/scripts/jest/setupTests.build.js
@@ -0,0 +1,6 @@
+'use strict';
+
+jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));
+jest.mock('scheduler/src/SchedulerHostConfig', () =>
+ require.requireActual('scheduler/src/forks/SchedulerHostConfig.mock.js')
+);
diff --git a/scripts/jest/setupTests.js b/scripts/jest/setupTests.js
index c6e467d96a..838e0fd6f6 100644
--- a/scripts/jest/setupTests.js
+++ b/scripts/jest/setupTests.js
@@ -49,8 +49,6 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
...require('./matchers/reactTestMatchers'),
});
- require('jest-mock-scheduler');
-
// We have a Babel transform that inserts guards against infinite loops.
// If a loop runs for too many iterations, we throw an error and set this
// global variable. The global lets us detect an infinite loop even if
diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js
index 3e0e501fa2..4aad55e33d 100644
--- a/scripts/rollup/bundles.js
+++ b/scripts/rollup/bundles.js
@@ -453,6 +453,15 @@ const bundles = [
externals: [],
},
+ /******* React Scheduler Mock (experimental) *******/
+ {
+ bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
+ moduleType: ISOMORPHIC,
+ entry: 'scheduler/unstable_mock',
+ global: 'SchedulerMock',
+ externals: [],
+ },
+
/******* Jest React (experimental) *******/
{
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
@@ -462,15 +471,6 @@ const bundles = [
externals: [],
},
- /******* Jest Scheduler (experimental) *******/
- {
- bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
- moduleType: ISOMORPHIC,
- entry: 'jest-mock-scheduler',
- global: 'JestMockScheduler',
- externals: [],
- },
-
/******* ESLint Plugin for Hooks (proposal) *******/
{
// TODO: it's awkward to create a bundle for this
diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js
index b5044a1f64..98c1b4a00f 100644
--- a/scripts/rollup/forks.js
+++ b/scripts/rollup/forks.js
@@ -159,16 +159,22 @@ const forks = Object.freeze({
'scheduler/src/SchedulerFeatureFlags': (bundleType, entry, dependencies) => {
if (
- entry === 'scheduler' &&
- (bundleType === FB_WWW_DEV ||
- bundleType === FB_WWW_PROD ||
- bundleType === FB_WWW_PROFILING)
+ bundleType === FB_WWW_DEV ||
+ bundleType === FB_WWW_PROD ||
+ bundleType === FB_WWW_PROFILING
) {
return 'scheduler/src/forks/SchedulerFeatureFlags.www.js';
}
return 'scheduler/src/SchedulerFeatureFlags';
},
+ 'scheduler/src/SchedulerHostConfig': (bundleType, entry, dependencies) => {
+ if (entry === 'scheduler/unstable_mock') {
+ return 'scheduler/src/forks/SchedulerHostConfig.mock';
+ }
+ return 'scheduler/src/forks/SchedulerHostConfig.default';
+ },
+
// This logic is forked on www to fork the formatting function.
'shared/invariant': (bundleType, entry) => {
switch (bundleType) {
diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json
index c84299de78..a75771086e 100644
--- a/scripts/rollup/results.json
+++ b/scripts/rollup/results.json
@@ -4,22 +4,22 @@
"filename": "react.development.js",
"bundleType": "UMD_DEV",
"packageName": "react",
- "size": 101989,
- "gzip": 26428
+ "size": 102032,
+ "gzip": 26457
},
{
"filename": "react.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react",
- "size": 12548,
- "gzip": 4823
+ "size": 12564,
+ "gzip": 4826
},
{
"filename": "react.development.js",
"bundleType": "NODE_DEV",
"packageName": "react",
- "size": 63522,
- "gzip": 17094
+ "size": 63704,
+ "gzip": 17181
},
{
"filename": "react.production.min.js",
@@ -46,29 +46,29 @@
"filename": "react-dom.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
- "size": 783692,
- "gzip": 178609
+ "size": 791682,
+ "gzip": 179962
},
{
"filename": "react-dom.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
- "size": 107842,
- "gzip": 34729
+ "size": 107808,
+ "gzip": 34704
},
{
"filename": "react-dom.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
- "size": 778167,
- "gzip": 177083
+ "size": 786187,
+ "gzip": 178415
},
{
"filename": "react-dom.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
- "size": 108009,
- "gzip": 34209
+ "size": 108031,
+ "gzip": 34186
},
{
"filename": "ReactDOM-dev.js",
@@ -165,29 +165,29 @@
"filename": "react-dom-server.browser.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
- "size": 129798,
- "gzip": 34602
+ "size": 130425,
+ "gzip": 34746
},
{
"filename": "react-dom-server.browser.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
- "size": 19117,
- "gzip": 7343
+ "size": 19319,
+ "gzip": 7379
},
{
"filename": "react-dom-server.browser.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
- "size": 125836,
- "gzip": 33642
+ "size": 126463,
+ "gzip": 33795
},
{
"filename": "react-dom-server.browser.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
- "size": 19037,
- "gzip": 7325
+ "size": 19239,
+ "gzip": 7368
},
{
"filename": "ReactDOMServer-dev.js",
@@ -207,43 +207,43 @@
"filename": "react-dom-server.node.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
- "size": 127943,
- "gzip": 34197
+ "size": 128570,
+ "gzip": 34348
},
{
"filename": "react-dom-server.node.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
- "size": 19930,
- "gzip": 7641
+ "size": 20132,
+ "gzip": 7685
},
{
"filename": "react-art.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-art",
- "size": 554932,
- "gzip": 120585
+ "size": 562387,
+ "gzip": 121860
},
{
"filename": "react-art.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-art",
- "size": 99655,
- "gzip": 30575
+ "size": 99616,
+ "gzip": 30538
},
{
"filename": "react-art.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-art",
- "size": 484327,
- "gzip": 102945
+ "size": 491818,
+ "gzip": 104175
},
{
"filename": "react-art.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-art",
- "size": 63856,
- "gzip": 19480
+ "size": 63873,
+ "gzip": 19446
},
{
"filename": "ReactART-dev.js",
@@ -291,29 +291,29 @@
"filename": "react-test-renderer.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-test-renderer",
- "size": 496691,
- "gzip": 105367
+ "size": 503952,
+ "gzip": 106548
},
{
"filename": "react-test-renderer.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-test-renderer",
- "size": 65252,
- "gzip": 19980
+ "size": 65222,
+ "gzip": 19962
},
{
"filename": "react-test-renderer.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-test-renderer",
- "size": 490997,
- "gzip": 104031
+ "size": 498258,
+ "gzip": 105207
},
{
"filename": "react-test-renderer.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-test-renderer",
- "size": 64908,
- "gzip": 19647
+ "size": 64873,
+ "gzip": 19614
},
{
"filename": "ReactTestRenderer-dev.js",
@@ -361,43 +361,43 @@
"filename": "react-noop-renderer.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-noop-renderer",
- "size": 32858,
- "gzip": 7514
+ "size": 28240,
+ "gzip": 6740
},
{
"filename": "react-noop-renderer.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-noop-renderer",
- "size": 11486,
- "gzip": 3766
+ "size": 10081,
+ "gzip": 3367
},
{
"filename": "react-reconciler.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
- "size": 481582,
- "gzip": 101249
+ "size": 489120,
+ "gzip": 102507
},
{
"filename": "react-reconciler.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-reconciler",
- "size": 65118,
- "gzip": 19259
+ "size": 65080,
+ "gzip": 19234
},
{
"filename": "react-reconciler-persistent.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
- "size": 479920,
- "gzip": 100594
+ "size": 487276,
+ "gzip": 101780
},
{
"filename": "react-reconciler-persistent.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-reconciler",
- "size": 65129,
- "gzip": 19264
+ "size": 65091,
+ "gzip": 19239
},
{
"filename": "react-reconciler-reflection.development.js",
@@ -501,8 +501,8 @@
"filename": "React-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react",
- "size": 60700,
- "gzip": 16126
+ "size": 60786,
+ "gzip": 16140
},
{
"filename": "React-prod.js",
@@ -515,15 +515,15 @@
"filename": "ReactDOM-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
- "size": 801709,
- "gzip": 178348
+ "size": 809740,
+ "gzip": 179616
},
{
"filename": "ReactDOM-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
- "size": 329235,
- "gzip": 60001
+ "size": 329960,
+ "gzip": 60112
},
{
"filename": "ReactTestUtils-dev.js",
@@ -550,92 +550,92 @@
"filename": "ReactDOMServer-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
- "size": 126805,
- "gzip": 33138
+ "size": 127337,
+ "gzip": 33235
},
{
"filename": "ReactDOMServer-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
- "size": 46040,
- "gzip": 10601
+ "size": 46343,
+ "gzip": 10662
},
{
"filename": "ReactART-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-art",
- "size": 493866,
- "gzip": 102238
+ "size": 501297,
+ "gzip": 103384
},
{
"filename": "ReactART-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-art",
- "size": 199704,
- "gzip": 33787
+ "size": 200132,
+ "gzip": 33817
},
{
"filename": "ReactNativeRenderer-dev.js",
"bundleType": "RN_FB_DEV",
"packageName": "react-native-renderer",
- "size": 621398,
- "gzip": 133323
+ "size": 631255,
+ "gzip": 134845
},
{
"filename": "ReactNativeRenderer-prod.js",
"bundleType": "RN_FB_PROD",
"packageName": "react-native-renderer",
- "size": 252107,
- "gzip": 44030
+ "size": 252735,
+ "gzip": 44084
},
{
"filename": "ReactNativeRenderer-dev.js",
"bundleType": "RN_OSS_DEV",
"packageName": "react-native-renderer",
- "size": 621309,
- "gzip": 133286
+ "size": 631168,
+ "gzip": 134810
},
{
"filename": "ReactNativeRenderer-prod.js",
"bundleType": "RN_OSS_PROD",
"packageName": "react-native-renderer",
- "size": 252121,
- "gzip": 44026
+ "size": 252749,
+ "gzip": 44079
},
{
"filename": "ReactFabric-dev.js",
"bundleType": "RN_FB_DEV",
"packageName": "react-native-renderer",
- "size": 612032,
- "gzip": 130990
+ "size": 621889,
+ "gzip": 132504
},
{
"filename": "ReactFabric-prod.js",
"bundleType": "RN_FB_PROD",
"packageName": "react-native-renderer",
- "size": 244266,
- "gzip": 42532
+ "size": 244896,
+ "gzip": 42571
},
{
"filename": "ReactFabric-dev.js",
"bundleType": "RN_OSS_DEV",
"packageName": "react-native-renderer",
- "size": 611935,
- "gzip": 130940
+ "size": 621794,
+ "gzip": 132455
},
{
"filename": "ReactFabric-prod.js",
"bundleType": "RN_OSS_PROD",
"packageName": "react-native-renderer",
- "size": 244272,
- "gzip": 42522
+ "size": 244902,
+ "gzip": 42561
},
{
"filename": "ReactTestRenderer-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-test-renderer",
- "size": 501300,
- "gzip": 103689
+ "size": 508573,
+ "gzip": 104823
},
{
"filename": "ReactShallowRenderer-dev.js",
@@ -676,15 +676,15 @@
"filename": "scheduler.development.js",
"bundleType": "NODE_DEV",
"packageName": "scheduler",
- "size": 23870,
- "gzip": 6174
+ "size": 23505,
+ "gzip": 6019
},
{
"filename": "scheduler.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "scheduler",
- "size": 5000,
- "gzip": 1883
+ "size": 4888,
+ "gzip": 1819
},
{
"filename": "SimpleCacheProvider-dev.js",
@@ -704,50 +704,50 @@
"filename": "react-noop-renderer-persistent.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-noop-renderer",
- "size": 32977,
- "gzip": 7525
+ "size": 28359,
+ "gzip": 6753
},
{
"filename": "react-noop-renderer-persistent.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-noop-renderer",
- "size": 11508,
- "gzip": 3772
+ "size": 10103,
+ "gzip": 3373
},
{
"filename": "react-dom.profiling.min.js",
"bundleType": "NODE_PROFILING",
"packageName": "react-dom",
- "size": 111156,
- "gzip": 35048
+ "size": 111178,
+ "gzip": 35023
},
{
"filename": "ReactNativeRenderer-profiling.js",
"bundleType": "RN_OSS_PROFILING",
"packageName": "react-native-renderer",
- "size": 258626,
- "gzip": 45614
+ "size": 259254,
+ "gzip": 45666
},
{
"filename": "ReactFabric-profiling.js",
"bundleType": "RN_OSS_PROFILING",
"packageName": "react-native-renderer",
- "size": 250654,
- "gzip": 44086
+ "size": 251284,
+ "gzip": 44137
},
{
"filename": "Scheduler-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "scheduler",
- "size": 24123,
- "gzip": 6223
+ "size": 23758,
+ "gzip": 6067
},
{
"filename": "Scheduler-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "scheduler",
- "size": 14327,
- "gzip": 2953
+ "size": 14025,
+ "gzip": 2841
},
{
"filename": "react.profiling.min.js",
@@ -767,43 +767,43 @@
"filename": "ReactDOM-profiling.js",
"bundleType": "FB_WWW_PROFILING",
"packageName": "react-dom",
- "size": 335969,
- "gzip": 61519
+ "size": 336694,
+ "gzip": 61627
},
{
"filename": "ReactNativeRenderer-profiling.js",
"bundleType": "RN_FB_PROFILING",
"packageName": "react-native-renderer",
- "size": 258607,
- "gzip": 45621
+ "size": 259235,
+ "gzip": 45675
},
{
"filename": "ReactFabric-profiling.js",
"bundleType": "RN_FB_PROFILING",
"packageName": "react-native-renderer",
- "size": 250643,
- "gzip": 44092
+ "size": 251273,
+ "gzip": 44141
},
{
"filename": "react.profiling.min.js",
"bundleType": "UMD_PROFILING",
"packageName": "react",
- "size": 14756,
- "gzip": 5369
+ "size": 14773,
+ "gzip": 5356
},
{
"filename": "react-dom.profiling.min.js",
"bundleType": "UMD_PROFILING",
"packageName": "react-dom",
- "size": 110830,
- "gzip": 35633
+ "size": 110796,
+ "gzip": 35607
},
{
"filename": "scheduler-tracing.development.js",
"bundleType": "NODE_DEV",
"packageName": "scheduler",
- "size": 10554,
- "gzip": 2432
+ "size": 10737,
+ "gzip": 2535
},
{
"filename": "scheduler-tracing.production.min.js",
@@ -823,8 +823,8 @@
"filename": "SchedulerTracing-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "scheduler",
- "size": 10121,
- "gzip": 2117
+ "size": 10207,
+ "gzip": 2134
},
{
"filename": "SchedulerTracing-prod.js",
@@ -928,15 +928,15 @@
"filename": "eslint-plugin-react-hooks.development.js",
"bundleType": "NODE_DEV",
"packageName": "eslint-plugin-react-hooks",
- "size": 26115,
- "gzip": 6005
+ "size": 50458,
+ "gzip": 11887
},
{
"filename": "eslint-plugin-react-hooks.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "eslint-plugin-react-hooks",
- "size": 5080,
- "gzip": 1872
+ "size": 12598,
+ "gzip": 4566
},
{
"filename": "ReactDOMFizzServer-dev.js",
@@ -1026,71 +1026,71 @@
"filename": "ESLintPluginReactHooks-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "eslint-plugin-react-hooks",
- "size": 27788,
- "gzip": 6145
+ "size": 54073,
+ "gzip": 12239
},
{
"filename": "react-dom-unstable-fire.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
- "size": 784046,
- "gzip": 178758
+ "size": 792036,
+ "gzip": 180102
},
{
"filename": "react-dom-unstable-fire.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
- "size": 107857,
- "gzip": 34738
+ "size": 107823,
+ "gzip": 34713
},
{
"filename": "react-dom-unstable-fire.profiling.min.js",
"bundleType": "UMD_PROFILING",
"packageName": "react-dom",
- "size": 110845,
- "gzip": 35642
+ "size": 110811,
+ "gzip": 35616
},
{
"filename": "react-dom-unstable-fire.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
- "size": 778520,
- "gzip": 177227
+ "size": 786540,
+ "gzip": 178557
},
{
"filename": "react-dom-unstable-fire.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
- "size": 108023,
- "gzip": 34220
+ "size": 108045,
+ "gzip": 34197
},
{
"filename": "react-dom-unstable-fire.profiling.min.js",
"bundleType": "NODE_PROFILING",
"packageName": "react-dom",
- "size": 111170,
- "gzip": 35058
+ "size": 111192,
+ "gzip": 35033
},
{
"filename": "ReactFire-dev.js",
"bundleType": "FB_WWW_DEV",
"packageName": "react-dom",
- "size": 800900,
- "gzip": 178268
+ "size": 808931,
+ "gzip": 179569
},
{
"filename": "ReactFire-prod.js",
"bundleType": "FB_WWW_PROD",
"packageName": "react-dom",
- "size": 317385,
- "gzip": 57621
+ "size": 318110,
+ "gzip": 57721
},
{
"filename": "ReactFire-profiling.js",
"bundleType": "FB_WWW_PROFILING",
"packageName": "react-dom",
- "size": 324156,
- "gzip": 59066
+ "size": 324881,
+ "gzip": 59172
},
{
"filename": "jest-mock-scheduler.development.js",
@@ -1119,6 +1119,34 @@
"packageName": "jest-mock-scheduler",
"size": 1085,
"gzip": 532
+ },
+ {
+ "filename": "scheduler-unstable_mock.development.js",
+ "bundleType": "NODE_DEV",
+ "packageName": "scheduler",
+ "size": 17926,
+ "gzip": 4128
+ },
+ {
+ "filename": "scheduler-unstable_mock.production.min.js",
+ "bundleType": "NODE_PROD",
+ "packageName": "scheduler",
+ "size": 4173,
+ "gzip": 1606
+ },
+ {
+ "filename": "SchedulerMock-dev.js",
+ "bundleType": "FB_WWW_DEV",
+ "packageName": "scheduler",
+ "size": 18170,
+ "gzip": 4175
+ },
+ {
+ "filename": "SchedulerMock-prod.js",
+ "bundleType": "FB_WWW_PROD",
+ "packageName": "scheduler",
+ "size": 12088,
+ "gzip": 2473
}
]
}
\ No newline at end of file