diff --git a/packages/jest-mock-scheduler/README.md b/packages/jest-mock-scheduler/README.md new file mode 100644 index 0000000000000..387f015246617 --- /dev/null +++ b/packages/jest-mock-scheduler/README.md @@ -0,0 +1,5 @@ +# `jest-mock-scheduler` + +Jest matchers and utilities for testing the `scheduler` package. + +This package is experimental. APIs may change between releases. \ No newline at end of file diff --git a/packages/jest-mock-scheduler/index.js b/packages/jest-mock-scheduler/index.js new file mode 100644 index 0000000000000..c1311b7e234ba --- /dev/null +++ b/packages/jest-mock-scheduler/index.js @@ -0,0 +1,8 @@ +/** + * 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. + */ + +export * from './src/JestMockScheduler'; diff --git a/packages/jest-mock-scheduler/npm/index.js b/packages/jest-mock-scheduler/npm/index.js new file mode 100644 index 0000000000000..d7a102c971275 --- /dev/null +++ b/packages/jest-mock-scheduler/npm/index.js @@ -0,0 +1,7 @@ +'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'); +} diff --git a/packages/jest-mock-scheduler/package.json b/packages/jest-mock-scheduler/package.json new file mode 100644 index 0000000000000..5c1cfacca989e --- /dev/null +++ b/packages/jest-mock-scheduler/package.json @@ -0,0 +1,28 @@ +{ + "name": "jest-mock-scheduler", + "private": true, + "version": "0.1.0", + "description": "Jest matchers and utilities for testing the scheduler package.", + "main": "index.js", + "repository": "facebook/react", + "keywords": [ + "jest", + "scheduler" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/facebook/react/issues" + }, + "homepage": "https://reactjs.org/", + "peerDependencies": { + "jest": "^23.0.1", + "scheduler": "^0.11.0" + }, + "files": [ + "LICENSE", + "README.md", + "build-info.json", + "index.js", + "cjs/" + ] +} diff --git a/packages/jest-mock-scheduler/src/JestMockScheduler.js b/packages/jest-mock-scheduler/src/JestMockScheduler.js new file mode 100644 index 0000000000000..609ece2c21906 --- /dev/null +++ b/packages/jest-mock-scheduler/src/JestMockScheduler.js @@ -0,0 +1,61 @@ +/** + * 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/scheduler/src/Scheduler.js b/packages/scheduler/src/Scheduler.js index c815d0071bd41..90e33a70dceb0 100644 --- a/packages/scheduler/src/Scheduler.js +++ b/packages/scheduler/src/Scheduler.js @@ -447,12 +447,20 @@ var requestHostCallback; var cancelHostCallback; var shouldYieldToHost; -if (typeof window !== 'undefined' && window._schedMock) { +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 impl = window._schedMock; - requestHostCallback = impl[0]; - cancelHostCallback = impl[1]; - shouldYieldToHost = impl[2]; + 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. @@ -460,28 +468,23 @@ if (typeof window !== 'undefined' && window._schedMock) { // 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 _currentTime = -1; - var _flushCallback = function(didTimeout, ms) { + var _flushCallback = function(didTimeout) { if (_callback !== null) { var cb = _callback; _callback = null; - try { - _currentTime = ms; - cb(didTimeout); - } finally { - _currentTime = -1; - } + cb(didTimeout); } }; requestHostCallback = function(cb, ms) { - if (_currentTime !== -1) { + if (_callback !== null) { // Protect against re-entrancy. - setTimeout(requestHostCallback, 0, cb, ms); + setTimeout(requestHostCallback, 0, cb); } else { _callback = cb; - setTimeout(_flushCallback, ms, true, ms); - setTimeout(_flushCallback, maxSigned31BitInt, false, maxSigned31BitInt); + setTimeout(_flushCallback, 0, false); } }; cancelHostCallback = function() { @@ -490,9 +493,6 @@ if (typeof window !== 'undefined' && window._schedMock) { shouldYieldToHost = function() { return false; }; - getCurrentTime = function() { - return _currentTime === -1 ? 0 : _currentTime; - }; } else { if (typeof console !== 'undefined') { // TODO: Remove fb.me link diff --git a/packages/scheduler/src/__tests__/Scheduler-test.js b/packages/scheduler/src/__tests__/Scheduler-test.js index d6db53db8dcef..4674a9b16a5f4 100644 --- a/packages/scheduler/src/__tests__/Scheduler-test.js +++ b/packages/scheduler/src/__tests__/Scheduler-test.js @@ -30,6 +30,9 @@ describe('Scheduler', () => { jest.useFakeTimers(); jest.resetModules(); + const JestMockScheduler = require('jest-mock-scheduler'); + JestMockScheduler.mockRestore(); + let _flushWork = null; let isFlushing = false; let timeoutID = -1; @@ -123,16 +126,21 @@ describe('Scheduler', () => { 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'); diff --git a/packages/scheduler/src/__tests__/SchedulerDOM-test.js b/packages/scheduler/src/__tests__/SchedulerDOM-test.js index 562113f65bbda..9cdec37285afc 100644 --- a/packages/scheduler/src/__tests__/SchedulerDOM-test.js +++ b/packages/scheduler/src/__tests__/SchedulerDOM-test.js @@ -88,6 +88,10 @@ describe('SchedulerDOM', () => { return currentTime; }; jest.resetModules(); + + const JestMockScheduler = require('jest-mock-scheduler'); + JestMockScheduler.mockRestore(); + Scheduler = require('scheduler'); }); diff --git a/packages/shared/__tests__/ReactDOMFrameScheduling-test.js b/packages/shared/__tests__/ReactDOMFrameScheduling-test.js index 7d8a1652f06c0..b23ce3a7fd7db 100644 --- a/packages/shared/__tests__/ReactDOMFrameScheduling-test.js +++ b/packages/shared/__tests__/ReactDOMFrameScheduling-test.js @@ -22,6 +22,10 @@ describe('ReactDOMFrameScheduling', () => { }; }; 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/setupTests.js b/scripts/jest/setupTests.js index 285f2adc898c1..8a5a522f312e6 100644 --- a/scripts/jest/setupTests.js +++ b/scripts/jest/setupTests.js @@ -56,6 +56,8 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) { toMatchRenderedOutput: JestReact.unstable_toMatchRenderedOutput, }); + 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 f8ec115438834..958c84f97d768 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -396,6 +396,15 @@ const bundles = [ externals: ['jest-diff'], }, + /******* Jest Scheduler (experimental) *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD], + moduleType: ISOMORPHIC, + entry: 'jest-mock-scheduler', + global: 'JestMockScheduler', + externals: ['jest-diff'], + }, + /******* ESLint Plugin for Hooks (proposal) *******/ { // TODO: it's awkward to create a bundle for this