diff --git a/packages/scheduler/src/Scheduler.js b/packages/scheduler/src/Scheduler.js index 2c692d7dfe7..42a5659beb1 100644 --- a/packages/scheduler/src/Scheduler.js +++ b/packages/scheduler/src/Scheduler.js @@ -468,30 +468,53 @@ var requestHostCallback; var cancelHostCallback; var getFrameDeadline; -// "addEventListener" might not be available on the window object -// if this is a mocked "window" object. So we need to validate that too. -if ( +if (typeof window !== 'undefined' && window._schedMock) { + // Dynamic injection, only for testing purposes. + var impl = window._schedMock; + requestHostCallback = impl[0]; + cancelHostCallback = impl[1]; + getFrameDeadline = impl[2]; +} else if ( + // If Scheduler runs in a non-DOM environment, it falls back to a naive + // implementation using setTimeout. typeof window === 'undefined' || + // "addEventListener" might not be available on the window object + // if this is a mocked "window" object. So we need to validate that too. typeof window.addEventListener !== 'function' ) { - // If this accidentally gets imported in a non-browser environment, fallback - // to a naive implementation. - var timeoutID = -1; - requestHostCallback = function(callback, absoluteTimeout) { - timeoutID = setTimeout(callback, 0, true); + var _callback = null; + var _currentTime = -1; + var _flushCallback = function(didTimeout, ms) { + if (_callback !== null) { + var cb = _callback; + _callback = null; + try { + _currentTime = ms; + cb(didTimeout); + } finally { + _currentTime = -1; + } + } + }; + requestHostCallback = function(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); + } }; cancelHostCallback = function() { - clearTimeout(timeoutID); + _callback = null; }; getFrameDeadline = function() { - return 0; + return Infinity; + }; + getCurrentTime = function() { + return _currentTime === -1 ? 0 : _currentTime; }; -} else if (window._schedMock) { - // Dynamic injection, only for testing purposes. - var impl = window._schedMock; - requestHostCallback = impl[0]; - cancelHostCallback = impl[1]; - getFrameDeadline = impl[2]; } else { if (typeof console !== 'undefined') { // TODO: Remove fb.me link diff --git a/packages/scheduler/src/__tests__/SchedulerNoDOM-test.internal.js b/packages/scheduler/src/__tests__/SchedulerNoDOM-test.internal.js new file mode 100644 index 00000000000..88f4a0eae50 --- /dev/null +++ b/packages/scheduler/src/__tests__/SchedulerNoDOM-test.internal.js @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let scheduleCallback; +let runWithPriority; +let ImmediatePriority; +let InteractivePriority; + +describe('SchedulerNoDOM', () => { + // If Scheduler runs in a non-DOM environment, it falls back to a naive + // 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; + const Scheduler = require('scheduler'); + scheduleCallback = Scheduler.unstable_scheduleCallback; + runWithPriority = Scheduler.unstable_runWithPriority; + ImmediatePriority = Scheduler.unstable_ImmediatePriority; + InteractivePriority = Scheduler.unstable_InteractivePriority; + }); + + it('runAllTimers flushes all scheduled callbacks', () => { + let log = []; + scheduleCallback(() => { + log.push('A'); + }); + scheduleCallback(() => { + log.push('B'); + }); + scheduleCallback(() => { + log.push('C'); + }); + expect(log).toEqual([]); + jest.runAllTimers(); + expect(log).toEqual(['A', 'B', 'C']); + }); + + it('executes callbacks in order of priority', () => { + let log = []; + + scheduleCallback(() => { + log.push('A'); + }); + scheduleCallback(() => { + log.push('B'); + }); + runWithPriority(InteractivePriority, () => { + scheduleCallback(() => { + log.push('C'); + }); + scheduleCallback(() => { + log.push('D'); + }); + }); + + expect(log).toEqual([]); + jest.runAllTimers(); + expect(log).toEqual(['C', 'D', 'A', 'B']); + }); + + it('advanceTimersByTime expires callbacks incrementally', () => { + let log = []; + + scheduleCallback(() => { + log.push('A'); + }); + scheduleCallback(() => { + log.push('B'); + }); + runWithPriority(InteractivePriority, () => { + 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 = []; + + runWithPriority(ImmediatePriority, () => { + scheduleCallback(() => { + log.push('A'); + scheduleCallback(() => { + log.push('B'); + }); + }); + }); + + expect(log).toEqual(['A', 'B']); + }); + + it('handles errors', () => { + let log = []; + + expect(() => { + runWithPriority(ImmediatePriority, () => { + scheduleCallback(() => { + log.push('A'); + throw new Error('Oops A'); + }); + scheduleCallback(() => { + log.push('B'); + }); + scheduleCallback(() => { + log.push('C'); + throw new Error('Oops C'); + }); + }); + }).toThrow('Oops A'); + + expect(log).toEqual(['A']); + + log = []; + + // B and C flush in a subsequent event. That way, the second error is not + // swallowed. + expect(() => jest.runAllTimers()).toThrow('Oops C'); + expect(log).toEqual(['B', 'C']); + }); +});