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