Spaces:
Sleeping
Sleeping
; | |
const globalObject = require("@sinonjs/commons").global; | |
/** | |
* @typedef {object} IdleDeadline | |
* @property {boolean} didTimeout - whether or not the callback was called before reaching the optional timeout | |
* @property {function():number} timeRemaining - a floating-point value providing an estimate of the number of milliseconds remaining in the current idle period | |
*/ | |
/** | |
* Queues a function to be called during a browser's idle periods | |
* | |
* @callback RequestIdleCallback | |
* @param {function(IdleDeadline)} callback | |
* @param {{timeout: number}} options - an options object | |
* @returns {number} the id | |
*/ | |
/** | |
* @callback NextTick | |
* @param {VoidVarArgsFunc} callback - the callback to run | |
* @param {...*} arguments - optional arguments to call the callback with | |
* @returns {void} | |
*/ | |
/** | |
* @callback SetImmediate | |
* @param {VoidVarArgsFunc} callback - the callback to run | |
* @param {...*} arguments - optional arguments to call the callback with | |
* @returns {NodeImmediate} | |
*/ | |
/** | |
* @callback VoidVarArgsFunc | |
* @param {...*} callback - the callback to run | |
* @returns {void} | |
*/ | |
/** | |
* @typedef RequestAnimationFrame | |
* @property {function(number):void} requestAnimationFrame | |
* @returns {number} - the id | |
*/ | |
/** | |
* @typedef Performance | |
* @property {function(): number} now | |
*/ | |
/* eslint-disable jsdoc/require-property-description */ | |
/** | |
* @typedef {object} Clock | |
* @property {number} now - the current time | |
* @property {Date} Date - the Date constructor | |
* @property {number} loopLimit - the maximum number of timers before assuming an infinite loop | |
* @property {RequestIdleCallback} requestIdleCallback | |
* @property {function(number):void} cancelIdleCallback | |
* @property {setTimeout} setTimeout | |
* @property {clearTimeout} clearTimeout | |
* @property {NextTick} nextTick | |
* @property {queueMicrotask} queueMicrotask | |
* @property {setInterval} setInterval | |
* @property {clearInterval} clearInterval | |
* @property {SetImmediate} setImmediate | |
* @property {function(NodeImmediate):void} clearImmediate | |
* @property {function():number} countTimers | |
* @property {RequestAnimationFrame} requestAnimationFrame | |
* @property {function(number):void} cancelAnimationFrame | |
* @property {function():void} runMicrotasks | |
* @property {function(string | number): number} tick | |
* @property {function(string | number): Promise<number>} tickAsync | |
* @property {function(): number} next | |
* @property {function(): Promise<number>} nextAsync | |
* @property {function(): number} runAll | |
* @property {function(): number} runToFrame | |
* @property {function(): Promise<number>} runAllAsync | |
* @property {function(): number} runToLast | |
* @property {function(): Promise<number>} runToLastAsync | |
* @property {function(): void} reset | |
* @property {function(number | Date): void} setSystemTime | |
* @property {Performance} performance | |
* @property {function(number[]): number[]} hrtime - process.hrtime (legacy) | |
* @property {function(): void} uninstall Uninstall the clock. | |
* @property {Function[]} methods - the methods that are faked | |
* @property {boolean} [shouldClearNativeTimers] inherited from config | |
*/ | |
/* eslint-enable jsdoc/require-property-description */ | |
/** | |
* Configuration object for the `install` method. | |
* | |
* @typedef {object} Config | |
* @property {number|Date} [now] a number (in milliseconds) or a Date object (default epoch) | |
* @property {string[]} [toFake] names of the methods that should be faked. | |
* @property {number} [loopLimit] the maximum number of timers that will be run when calling runAll() | |
* @property {boolean} [shouldAdvanceTime] tells FakeTimers to increment mocked time automatically (default false) | |
* @property {number} [advanceTimeDelta] increment mocked time every <<advanceTimeDelta>> ms (default: 20ms) | |
* @property {boolean} [shouldClearNativeTimers] forwards clear timer calls to native functions if they are not fakes (default: false) | |
*/ | |
/* eslint-disable jsdoc/require-property-description */ | |
/** | |
* The internal structure to describe a scheduled fake timer | |
* | |
* @typedef {object} Timer | |
* @property {Function} func | |
* @property {*[]} args | |
* @property {number} delay | |
* @property {number} callAt | |
* @property {number} createdAt | |
* @property {boolean} immediate | |
* @property {number} id | |
* @property {Error} [error] | |
*/ | |
/** | |
* A Node timer | |
* | |
* @typedef {object} NodeImmediate | |
* @property {function(): boolean} hasRef | |
* @property {function(): NodeImmediate} ref | |
* @property {function(): NodeImmediate} unref | |
*/ | |
/* eslint-enable jsdoc/require-property-description */ | |
/* eslint-disable complexity */ | |
/** | |
* Mocks available features in the specified global namespace. | |
* | |
* @param {*} _global Namespace to mock (e.g. `window`) | |
* @returns {FakeTimers} | |
*/ | |
function withGlobal(_global) { | |
const userAgent = _global.navigator && _global.navigator.userAgent; | |
const isRunningInIE = userAgent && userAgent.indexOf("MSIE ") > -1; | |
const maxTimeout = Math.pow(2, 31) - 1; //see https://heycam.github.io/webidl/#abstract-opdef-converttoint | |
const idCounterStart = 1e12; // arbitrarily large number to avoid collisions with native timer IDs | |
const NOOP = function () { | |
return undefined; | |
}; | |
const NOOP_ARRAY = function () { | |
return []; | |
}; | |
const timeoutResult = _global.setTimeout(NOOP, 0); | |
const addTimerReturnsObject = typeof timeoutResult === "object"; | |
const hrtimePresent = | |
_global.process && typeof _global.process.hrtime === "function"; | |
const hrtimeBigintPresent = | |
hrtimePresent && typeof _global.process.hrtime.bigint === "function"; | |
const nextTickPresent = | |
_global.process && typeof _global.process.nextTick === "function"; | |
const utilPromisify = _global.process && require("util").promisify; | |
const performancePresent = | |
_global.performance && typeof _global.performance.now === "function"; | |
const hasPerformancePrototype = | |
_global.Performance && | |
(typeof _global.Performance).match(/^(function|object)$/); | |
const queueMicrotaskPresent = _global.hasOwnProperty("queueMicrotask"); | |
const requestAnimationFramePresent = | |
_global.requestAnimationFrame && | |
typeof _global.requestAnimationFrame === "function"; | |
const cancelAnimationFramePresent = | |
_global.cancelAnimationFrame && | |
typeof _global.cancelAnimationFrame === "function"; | |
const requestIdleCallbackPresent = | |
_global.requestIdleCallback && | |
typeof _global.requestIdleCallback === "function"; | |
const cancelIdleCallbackPresent = | |
_global.cancelIdleCallback && | |
typeof _global.cancelIdleCallback === "function"; | |
const setImmediatePresent = | |
_global.setImmediate && typeof _global.setImmediate === "function"; | |
// Make properties writable in IE, as per | |
// https://www.adequatelygood.com/Replacing-setTimeout-Globally.html | |
/* eslint-disable no-self-assign */ | |
if (isRunningInIE) { | |
_global.setTimeout = _global.setTimeout; | |
_global.clearTimeout = _global.clearTimeout; | |
_global.setInterval = _global.setInterval; | |
_global.clearInterval = _global.clearInterval; | |
_global.Date = _global.Date; | |
} | |
// setImmediate is not a standard function | |
// avoid adding the prop to the window object if not present | |
if (setImmediatePresent) { | |
_global.setImmediate = _global.setImmediate; | |
_global.clearImmediate = _global.clearImmediate; | |
} | |
/* eslint-enable no-self-assign */ | |
_global.clearTimeout(timeoutResult); | |
const NativeDate = _global.Date; | |
let uniqueTimerId = idCounterStart; | |
/** | |
* @param {number} num | |
* @returns {boolean} | |
*/ | |
function isNumberFinite(num) { | |
if (Number.isFinite) { | |
return Number.isFinite(num); | |
} | |
return isFinite(num); | |
} | |
let isNearInfiniteLimit = false; | |
/** | |
* @param {Clock} clock | |
* @param {number} i | |
*/ | |
function checkIsNearInfiniteLimit(clock, i) { | |
if (clock.loopLimit && i === clock.loopLimit - 1) { | |
isNearInfiniteLimit = true; | |
} | |
} | |
/** | |
* | |
*/ | |
function resetIsNearInfiniteLimit() { | |
isNearInfiniteLimit = false; | |
} | |
/** | |
* Parse strings like "01:10:00" (meaning 1 hour, 10 minutes, 0 seconds) into | |
* number of milliseconds. This is used to support human-readable strings passed | |
* to clock.tick() | |
* | |
* @param {string} str | |
* @returns {number} | |
*/ | |
function parseTime(str) { | |
if (!str) { | |
return 0; | |
} | |
const strings = str.split(":"); | |
const l = strings.length; | |
let i = l; | |
let ms = 0; | |
let parsed; | |
if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { | |
throw new Error( | |
"tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits" | |
); | |
} | |
while (i--) { | |
parsed = parseInt(strings[i], 10); | |
if (parsed >= 60) { | |
throw new Error(`Invalid time ${str}`); | |
} | |
ms += parsed * Math.pow(60, l - i - 1); | |
} | |
return ms * 1000; | |
} | |
/** | |
* Get the decimal part of the millisecond value as nanoseconds | |
* | |
* @param {number} msFloat the number of milliseconds | |
* @returns {number} an integer number of nanoseconds in the range [0,1e6) | |
* | |
* Example: nanoRemainer(123.456789) -> 456789 | |
*/ | |
function nanoRemainder(msFloat) { | |
const modulo = 1e6; | |
const remainder = (msFloat * 1e6) % modulo; | |
const positiveRemainder = | |
remainder < 0 ? remainder + modulo : remainder; | |
return Math.floor(positiveRemainder); | |
} | |
/** | |
* Used to grok the `now` parameter to createClock. | |
* | |
* @param {Date|number} epoch the system time | |
* @returns {number} | |
*/ | |
function getEpoch(epoch) { | |
if (!epoch) { | |
return 0; | |
} | |
if (typeof epoch.getTime === "function") { | |
return epoch.getTime(); | |
} | |
if (typeof epoch === "number") { | |
return epoch; | |
} | |
throw new TypeError("now should be milliseconds since UNIX epoch"); | |
} | |
/** | |
* @param {number} from | |
* @param {number} to | |
* @param {Timer} timer | |
* @returns {boolean} | |
*/ | |
function inRange(from, to, timer) { | |
return timer && timer.callAt >= from && timer.callAt <= to; | |
} | |
/** | |
* @param {Clock} clock | |
* @param {Timer} job | |
*/ | |
function getInfiniteLoopError(clock, job) { | |
const infiniteLoopError = new Error( | |
`Aborting after running ${clock.loopLimit} timers, assuming an infinite loop!` | |
); | |
if (!job.error) { | |
return infiniteLoopError; | |
} | |
// pattern never matched in Node | |
const computedTargetPattern = /target\.*[<|(|[].*?[>|\]|)]\s*/; | |
let clockMethodPattern = new RegExp( | |
String(Object.keys(clock).join("|")) | |
); | |
if (addTimerReturnsObject) { | |
// node.js environment | |
clockMethodPattern = new RegExp( | |
`\\s+at (Object\\.)?(?:${Object.keys(clock).join("|")})\\s+` | |
); | |
} | |
let matchedLineIndex = -1; | |
job.error.stack.split("\n").some(function (line, i) { | |
// If we've matched a computed target line (e.g. setTimeout) then we | |
// don't need to look any further. Return true to stop iterating. | |
const matchedComputedTarget = line.match(computedTargetPattern); | |
/* istanbul ignore if */ | |
if (matchedComputedTarget) { | |
matchedLineIndex = i; | |
return true; | |
} | |
// If we've matched a clock method line, then there may still be | |
// others further down the trace. Return false to keep iterating. | |
const matchedClockMethod = line.match(clockMethodPattern); | |
if (matchedClockMethod) { | |
matchedLineIndex = i; | |
return false; | |
} | |
// If we haven't matched anything on this line, but we matched | |
// previously and set the matched line index, then we can stop. | |
// If we haven't matched previously, then we should keep iterating. | |
return matchedLineIndex >= 0; | |
}); | |
const stack = `${infiniteLoopError}\n${job.type || "Microtask"} - ${ | |
job.func.name || "anonymous" | |
}\n${job.error.stack | |
.split("\n") | |
.slice(matchedLineIndex + 1) | |
.join("\n")}`; | |
try { | |
Object.defineProperty(infiniteLoopError, "stack", { | |
value: stack, | |
}); | |
} catch (e) { | |
// noop | |
} | |
return infiniteLoopError; | |
} | |
/** | |
* @param {Date} target | |
* @param {Date} source | |
* @returns {Date} the target after modifications | |
*/ | |
function mirrorDateProperties(target, source) { | |
let prop; | |
for (prop in source) { | |
if (source.hasOwnProperty(prop)) { | |
target[prop] = source[prop]; | |
} | |
} | |
// set special now implementation | |
if (source.now) { | |
target.now = function now() { | |
return target.clock.now; | |
}; | |
} else { | |
delete target.now; | |
} | |
// set special toSource implementation | |
if (source.toSource) { | |
target.toSource = function toSource() { | |
return source.toSource(); | |
}; | |
} else { | |
delete target.toSource; | |
} | |
// set special toString implementation | |
target.toString = function toString() { | |
return source.toString(); | |
}; | |
target.prototype = source.prototype; | |
target.parse = source.parse; | |
target.UTC = source.UTC; | |
target.prototype.toUTCString = source.prototype.toUTCString; | |
return target; | |
} | |
//eslint-disable-next-line jsdoc/require-jsdoc | |
function createDate() { | |
/** | |
* @param {number} year | |
* @param {number} month | |
* @param {number} date | |
* @param {number} hour | |
* @param {number} minute | |
* @param {number} second | |
* @param {number} ms | |
* | |
* @returns {Date} | |
*/ | |
function ClockDate(year, month, date, hour, minute, second, ms) { | |
// the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2. | |
// This remains so in the 10th edition of 2019 as well. | |
if (!(this instanceof ClockDate)) { | |
return new NativeDate(ClockDate.clock.now).toString(); | |
} | |
// if Date is called as a constructor with 'new' keyword | |
// Defensive and verbose to avoid potential harm in passing | |
// explicit undefined when user does not pass argument | |
switch (arguments.length) { | |
case 0: | |
return new NativeDate(ClockDate.clock.now); | |
case 1: | |
return new NativeDate(year); | |
case 2: | |
return new NativeDate(year, month); | |
case 3: | |
return new NativeDate(year, month, date); | |
case 4: | |
return new NativeDate(year, month, date, hour); | |
case 5: | |
return new NativeDate(year, month, date, hour, minute); | |
case 6: | |
return new NativeDate( | |
year, | |
month, | |
date, | |
hour, | |
minute, | |
second | |
); | |
default: | |
return new NativeDate( | |
year, | |
month, | |
date, | |
hour, | |
minute, | |
second, | |
ms | |
); | |
} | |
} | |
return mirrorDateProperties(ClockDate, NativeDate); | |
} | |
//eslint-disable-next-line jsdoc/require-jsdoc | |
function enqueueJob(clock, job) { | |
// enqueues a microtick-deferred task - ecma262/#sec-enqueuejob | |
if (!clock.jobs) { | |
clock.jobs = []; | |
} | |
clock.jobs.push(job); | |
} | |
//eslint-disable-next-line jsdoc/require-jsdoc | |
function runJobs(clock) { | |
// runs all microtick-deferred tasks - ecma262/#sec-runjobs | |
if (!clock.jobs) { | |
return; | |
} | |
for (let i = 0; i < clock.jobs.length; i++) { | |
const job = clock.jobs[i]; | |
job.func.apply(null, job.args); | |
checkIsNearInfiniteLimit(clock, i); | |
if (clock.loopLimit && i > clock.loopLimit) { | |
throw getInfiniteLoopError(clock, job); | |
} | |
} | |
resetIsNearInfiniteLimit(); | |
clock.jobs = []; | |
} | |
/** | |
* @param {Clock} clock | |
* @param {Timer} timer | |
* @returns {number} id of the created timer | |
*/ | |
function addTimer(clock, timer) { | |
if (timer.func === undefined) { | |
throw new Error("Callback must be provided to timer calls"); | |
} | |
if (addTimerReturnsObject) { | |
// Node.js environment | |
if (typeof timer.func !== "function") { | |
throw new TypeError( | |
`[ERR_INVALID_CALLBACK]: Callback must be a function. Received ${ | |
timer.func | |
} of type ${typeof timer.func}` | |
); | |
} | |
} | |
if (isNearInfiniteLimit) { | |
timer.error = new Error(); | |
} | |
timer.type = timer.immediate ? "Immediate" : "Timeout"; | |
if (timer.hasOwnProperty("delay")) { | |
if (typeof timer.delay !== "number") { | |
timer.delay = parseInt(timer.delay, 10); | |
} | |
if (!isNumberFinite(timer.delay)) { | |
timer.delay = 0; | |
} | |
timer.delay = timer.delay > maxTimeout ? 1 : timer.delay; | |
timer.delay = Math.max(0, timer.delay); | |
} | |
if (timer.hasOwnProperty("interval")) { | |
timer.type = "Interval"; | |
timer.interval = timer.interval > maxTimeout ? 1 : timer.interval; | |
} | |
if (timer.hasOwnProperty("animation")) { | |
timer.type = "AnimationFrame"; | |
timer.animation = true; | |
} | |
if (timer.hasOwnProperty("idleCallback")) { | |
timer.type = "IdleCallback"; | |
timer.idleCallback = true; | |
} | |
if (!clock.timers) { | |
clock.timers = {}; | |
} | |
timer.id = uniqueTimerId++; | |
timer.createdAt = clock.now; | |
timer.callAt = | |
clock.now + (parseInt(timer.delay) || (clock.duringTick ? 1 : 0)); | |
clock.timers[timer.id] = timer; | |
if (addTimerReturnsObject) { | |
const res = { | |
ref: function () { | |
return res; | |
}, | |
unref: function () { | |
return res; | |
}, | |
refresh: function () { | |
clearTimeout(timer.id); | |
const args = [timer.func, timer.delay].concat(timer.args); | |
return setTimeout.apply(null, args); | |
}, | |
[Symbol.toPrimitive]: function () { | |
return timer.id; | |
}, | |
}; | |
return res; | |
} | |
return timer.id; | |
} | |
/* eslint consistent-return: "off" */ | |
/** | |
* Timer comparitor | |
* | |
* @param {Timer} a | |
* @param {Timer} b | |
* @returns {number} | |
*/ | |
function compareTimers(a, b) { | |
// Sort first by absolute timing | |
if (a.callAt < b.callAt) { | |
return -1; | |
} | |
if (a.callAt > b.callAt) { | |
return 1; | |
} | |
// Sort next by immediate, immediate timers take precedence | |
if (a.immediate && !b.immediate) { | |
return -1; | |
} | |
if (!a.immediate && b.immediate) { | |
return 1; | |
} | |
// Sort next by creation time, earlier-created timers take precedence | |
if (a.createdAt < b.createdAt) { | |
return -1; | |
} | |
if (a.createdAt > b.createdAt) { | |
return 1; | |
} | |
// Sort next by id, lower-id timers take precedence | |
if (a.id < b.id) { | |
return -1; | |
} | |
if (a.id > b.id) { | |
return 1; | |
} | |
// As timer ids are unique, no fallback `0` is necessary | |
} | |
/** | |
* @param {Clock} clock | |
* @param {number} from | |
* @param {number} to | |
* | |
* @returns {Timer} | |
*/ | |
function firstTimerInRange(clock, from, to) { | |
const timers = clock.timers; | |
let timer = null; | |
let id, isInRange; | |
for (id in timers) { | |
if (timers.hasOwnProperty(id)) { | |
isInRange = inRange(from, to, timers[id]); | |
if ( | |
isInRange && | |
(!timer || compareTimers(timer, timers[id]) === 1) | |
) { | |
timer = timers[id]; | |
} | |
} | |
} | |
return timer; | |
} | |
/** | |
* @param {Clock} clock | |
* @returns {Timer} | |
*/ | |
function firstTimer(clock) { | |
const timers = clock.timers; | |
let timer = null; | |
let id; | |
for (id in timers) { | |
if (timers.hasOwnProperty(id)) { | |
if (!timer || compareTimers(timer, timers[id]) === 1) { | |
timer = timers[id]; | |
} | |
} | |
} | |
return timer; | |
} | |
/** | |
* @param {Clock} clock | |
* @returns {Timer} | |
*/ | |
function lastTimer(clock) { | |
const timers = clock.timers; | |
let timer = null; | |
let id; | |
for (id in timers) { | |
if (timers.hasOwnProperty(id)) { | |
if (!timer || compareTimers(timer, timers[id]) === -1) { | |
timer = timers[id]; | |
} | |
} | |
} | |
return timer; | |
} | |
/** | |
* @param {Clock} clock | |
* @param {Timer} timer | |
*/ | |
function callTimer(clock, timer) { | |
if (typeof timer.interval === "number") { | |
clock.timers[timer.id].callAt += timer.interval; | |
} else { | |
delete clock.timers[timer.id]; | |
} | |
if (typeof timer.func === "function") { | |
timer.func.apply(null, timer.args); | |
} else { | |
/* eslint no-eval: "off" */ | |
const eval2 = eval; | |
(function () { | |
eval2(timer.func); | |
})(); | |
} | |
} | |
/** | |
* Gets clear handler name for a given timer type | |
* @param {string} ttype | |
*/ | |
function getClearHandler(ttype) { | |
if (ttype === "IdleCallback" || ttype === "AnimationFrame") { | |
return `cancel${ttype}`; | |
} | |
return `clear${ttype}`; | |
} | |
/** | |
* Gets schedule handler name for a given timer type | |
* @param {string} ttype | |
*/ | |
function getScheduleHandler(ttype) { | |
if (ttype === "IdleCallback" || ttype === "AnimationFrame") { | |
return `request${ttype}`; | |
} | |
return `set${ttype}`; | |
} | |
/** | |
* Creates an anonymous function to warn only once | |
*/ | |
function createWarnOnce() { | |
let calls = 0; | |
return function (msg) { | |
// eslint-disable-next-line | |
!calls++ && console.warn(msg); | |
}; | |
} | |
const warnOnce = createWarnOnce(); | |
/** | |
* @param {Clock} clock | |
* @param {number} timerId | |
* @param {string} ttype | |
*/ | |
function clearTimer(clock, timerId, ttype) { | |
if (!timerId) { | |
// null appears to be allowed in most browsers, and appears to be | |
// relied upon by some libraries, like Bootstrap carousel | |
return; | |
} | |
if (!clock.timers) { | |
clock.timers = {}; | |
} | |
// in Node, the ID is stored as the primitive value for `Timeout` objects | |
// for `Immediate` objects, no ID exists, so it gets coerced to NaN | |
const id = Number(timerId); | |
if (Number.isNaN(id) || id < idCounterStart) { | |
const handlerName = getClearHandler(ttype); | |
if (clock.shouldClearNativeTimers === true) { | |
const nativeHandler = clock[`_${handlerName}`]; | |
return typeof nativeHandler === "function" | |
? nativeHandler(timerId) | |
: undefined; | |
} | |
warnOnce( | |
`FakeTimers: ${handlerName} was invoked to clear a native timer instead of one created by this library.` + | |
"\nTo automatically clean-up native timers, use `shouldClearNativeTimers`." | |
); | |
} | |
if (clock.timers.hasOwnProperty(id)) { | |
// check that the ID matches a timer of the correct type | |
const timer = clock.timers[id]; | |
if ( | |
timer.type === ttype || | |
(timer.type === "Timeout" && ttype === "Interval") || | |
(timer.type === "Interval" && ttype === "Timeout") | |
) { | |
delete clock.timers[id]; | |
} else { | |
const clear = getClearHandler(ttype); | |
const schedule = getScheduleHandler(timer.type); | |
throw new Error( | |
`Cannot clear timer: timer created with ${schedule}() but cleared with ${clear}()` | |
); | |
} | |
} | |
} | |
/** | |
* @param {Clock} clock | |
* @param {Config} config | |
* @returns {Timer[]} | |
*/ | |
function uninstall(clock, config) { | |
let method, i, l; | |
const installedHrTime = "_hrtime"; | |
const installedNextTick = "_nextTick"; | |
for (i = 0, l = clock.methods.length; i < l; i++) { | |
method = clock.methods[i]; | |
if (method === "hrtime" && _global.process) { | |
_global.process.hrtime = clock[installedHrTime]; | |
} else if (method === "nextTick" && _global.process) { | |
_global.process.nextTick = clock[installedNextTick]; | |
} else if (method === "performance") { | |
const originalPerfDescriptor = Object.getOwnPropertyDescriptor( | |
clock, | |
`_${method}` | |
); | |
if ( | |
originalPerfDescriptor && | |
originalPerfDescriptor.get && | |
!originalPerfDescriptor.set | |
) { | |
Object.defineProperty( | |
_global, | |
method, | |
originalPerfDescriptor | |
); | |
} else if (originalPerfDescriptor.configurable) { | |
_global[method] = clock[`_${method}`]; | |
} | |
} else { | |
if (_global[method] && _global[method].hadOwnProperty) { | |
_global[method] = clock[`_${method}`]; | |
} else { | |
try { | |
delete _global[method]; | |
} catch (ignore) { | |
/* eslint no-empty: "off" */ | |
} | |
} | |
} | |
} | |
if (config.shouldAdvanceTime === true) { | |
_global.clearInterval(clock.attachedInterval); | |
} | |
// Prevent multiple executions which will completely remove these props | |
clock.methods = []; | |
// return pending timers, to enable checking what timers remained on uninstall | |
if (!clock.timers) { | |
return []; | |
} | |
return Object.keys(clock.timers).map(function mapper(key) { | |
return clock.timers[key]; | |
}); | |
} | |
/** | |
* @param {object} target the target containing the method to replace | |
* @param {string} method the keyname of the method on the target | |
* @param {Clock} clock | |
*/ | |
function hijackMethod(target, method, clock) { | |
clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call( | |
target, | |
method | |
); | |
clock[`_${method}`] = target[method]; | |
if (method === "Date") { | |
const date = mirrorDateProperties(clock[method], target[method]); | |
target[method] = date; | |
} else if (method === "performance") { | |
const originalPerfDescriptor = Object.getOwnPropertyDescriptor( | |
target, | |
method | |
); | |
// JSDOM has a read only performance field so we have to save/copy it differently | |
if ( | |
originalPerfDescriptor && | |
originalPerfDescriptor.get && | |
!originalPerfDescriptor.set | |
) { | |
Object.defineProperty( | |
clock, | |
`_${method}`, | |
originalPerfDescriptor | |
); | |
const perfDescriptor = Object.getOwnPropertyDescriptor( | |
clock, | |
method | |
); | |
Object.defineProperty(target, method, perfDescriptor); | |
} else { | |
target[method] = clock[method]; | |
} | |
} else { | |
target[method] = function () { | |
return clock[method].apply(clock, arguments); | |
}; | |
Object.defineProperties( | |
target[method], | |
Object.getOwnPropertyDescriptors(clock[method]) | |
); | |
} | |
target[method].clock = clock; | |
} | |
/** | |
* @param {Clock} clock | |
* @param {number} advanceTimeDelta | |
*/ | |
function doIntervalTick(clock, advanceTimeDelta) { | |
clock.tick(advanceTimeDelta); | |
} | |
/** | |
* @typedef {object} Timers | |
* @property {setTimeout} setTimeout | |
* @property {clearTimeout} clearTimeout | |
* @property {setInterval} setInterval | |
* @property {clearInterval} clearInterval | |
* @property {Date} Date | |
* @property {SetImmediate=} setImmediate | |
* @property {function(NodeImmediate): void=} clearImmediate | |
* @property {function(number[]):number[]=} hrtime | |
* @property {NextTick=} nextTick | |
* @property {Performance=} performance | |
* @property {RequestAnimationFrame=} requestAnimationFrame | |
* @property {boolean=} queueMicrotask | |
* @property {function(number): void=} cancelAnimationFrame | |
* @property {RequestIdleCallback=} requestIdleCallback | |
* @property {function(number): void=} cancelIdleCallback | |
*/ | |
/** @type {Timers} */ | |
const timers = { | |
setTimeout: _global.setTimeout, | |
clearTimeout: _global.clearTimeout, | |
setInterval: _global.setInterval, | |
clearInterval: _global.clearInterval, | |
Date: _global.Date, | |
}; | |
if (setImmediatePresent) { | |
timers.setImmediate = _global.setImmediate; | |
timers.clearImmediate = _global.clearImmediate; | |
} | |
if (hrtimePresent) { | |
timers.hrtime = _global.process.hrtime; | |
} | |
if (nextTickPresent) { | |
timers.nextTick = _global.process.nextTick; | |
} | |
if (performancePresent) { | |
timers.performance = _global.performance; | |
} | |
if (requestAnimationFramePresent) { | |
timers.requestAnimationFrame = _global.requestAnimationFrame; | |
} | |
if (queueMicrotaskPresent) { | |
timers.queueMicrotask = true; | |
} | |
if (cancelAnimationFramePresent) { | |
timers.cancelAnimationFrame = _global.cancelAnimationFrame; | |
} | |
if (requestIdleCallbackPresent) { | |
timers.requestIdleCallback = _global.requestIdleCallback; | |
} | |
if (cancelIdleCallbackPresent) { | |
timers.cancelIdleCallback = _global.cancelIdleCallback; | |
} | |
const originalSetTimeout = _global.setImmediate || _global.setTimeout; | |
/** | |
* @param {Date|number} [start] the system time - non-integer values are floored | |
* @param {number} [loopLimit] maximum number of timers that will be run when calling runAll() | |
* @returns {Clock} | |
*/ | |
function createClock(start, loopLimit) { | |
// eslint-disable-next-line no-param-reassign | |
start = Math.floor(getEpoch(start)); | |
// eslint-disable-next-line no-param-reassign | |
loopLimit = loopLimit || 1000; | |
let nanos = 0; | |
const adjustedSystemTime = [0, 0]; // [millis, nanoremainder] | |
if (NativeDate === undefined) { | |
throw new Error( | |
"The global scope doesn't have a `Date` object" + | |
" (see https://github.com/sinonjs/sinon/issues/1852#issuecomment-419622780)" | |
); | |
} | |
const clock = { | |
now: start, | |
Date: createDate(), | |
loopLimit: loopLimit, | |
}; | |
clock.Date.clock = clock; | |
//eslint-disable-next-line jsdoc/require-jsdoc | |
function getTimeToNextFrame() { | |
return 16 - ((clock.now - start) % 16); | |
} | |
//eslint-disable-next-line jsdoc/require-jsdoc | |
function hrtime(prev) { | |
const millisSinceStart = clock.now - adjustedSystemTime[0] - start; | |
const secsSinceStart = Math.floor(millisSinceStart / 1000); | |
const remainderInNanos = | |
(millisSinceStart - secsSinceStart * 1e3) * 1e6 + | |
nanos - | |
adjustedSystemTime[1]; | |
if (Array.isArray(prev)) { | |
if (prev[1] > 1e9) { | |
throw new TypeError( | |
"Number of nanoseconds can't exceed a billion" | |
); | |
} | |
const oldSecs = prev[0]; | |
let nanoDiff = remainderInNanos - prev[1]; | |
let secDiff = secsSinceStart - oldSecs; | |
if (nanoDiff < 0) { | |
nanoDiff += 1e9; | |
secDiff -= 1; | |
} | |
return [secDiff, nanoDiff]; | |
} | |
return [secsSinceStart, remainderInNanos]; | |
} | |
if (hrtimeBigintPresent) { | |
hrtime.bigint = function () { | |
const parts = hrtime(); | |
return BigInt(parts[0]) * BigInt(1e9) + BigInt(parts[1]); // eslint-disable-line | |
}; | |
} | |
clock.requestIdleCallback = function requestIdleCallback( | |
func, | |
timeout | |
) { | |
let timeToNextIdlePeriod = 0; | |
if (clock.countTimers() > 0) { | |
timeToNextIdlePeriod = 50; // const for now | |
} | |
const result = addTimer(clock, { | |
func: func, | |
args: Array.prototype.slice.call(arguments, 2), | |
delay: | |
typeof timeout === "undefined" | |
? timeToNextIdlePeriod | |
: Math.min(timeout, timeToNextIdlePeriod), | |
idleCallback: true, | |
}); | |
return Number(result); | |
}; | |
clock.cancelIdleCallback = function cancelIdleCallback(timerId) { | |
return clearTimer(clock, timerId, "IdleCallback"); | |
}; | |
clock.setTimeout = function setTimeout(func, timeout) { | |
return addTimer(clock, { | |
func: func, | |
args: Array.prototype.slice.call(arguments, 2), | |
delay: timeout, | |
}); | |
}; | |
if (typeof _global.Promise !== "undefined" && utilPromisify) { | |
clock.setTimeout[ | |
utilPromisify.custom | |
] = function promisifiedSetTimeout(timeout, arg) { | |
return new _global.Promise(function setTimeoutExecutor( | |
resolve | |
) { | |
addTimer(clock, { | |
func: resolve, | |
args: [arg], | |
delay: timeout, | |
}); | |
}); | |
}; | |
} | |
clock.clearTimeout = function clearTimeout(timerId) { | |
return clearTimer(clock, timerId, "Timeout"); | |
}; | |
clock.nextTick = function nextTick(func) { | |
return enqueueJob(clock, { | |
func: func, | |
args: Array.prototype.slice.call(arguments, 1), | |
error: isNearInfiniteLimit ? new Error() : null, | |
}); | |
}; | |
clock.queueMicrotask = function queueMicrotask(func) { | |
return clock.nextTick(func); // explicitly drop additional arguments | |
}; | |
clock.setInterval = function setInterval(func, timeout) { | |
// eslint-disable-next-line no-param-reassign | |
timeout = parseInt(timeout, 10); | |
return addTimer(clock, { | |
func: func, | |
args: Array.prototype.slice.call(arguments, 2), | |
delay: timeout, | |
interval: timeout, | |
}); | |
}; | |
clock.clearInterval = function clearInterval(timerId) { | |
return clearTimer(clock, timerId, "Interval"); | |
}; | |
if (setImmediatePresent) { | |
clock.setImmediate = function setImmediate(func) { | |
return addTimer(clock, { | |
func: func, | |
args: Array.prototype.slice.call(arguments, 1), | |
immediate: true, | |
}); | |
}; | |
if (typeof _global.Promise !== "undefined" && utilPromisify) { | |
clock.setImmediate[ | |
utilPromisify.custom | |
] = function promisifiedSetImmediate(arg) { | |
return new _global.Promise(function setImmediateExecutor( | |
resolve | |
) { | |
addTimer(clock, { | |
func: resolve, | |
args: [arg], | |
immediate: true, | |
}); | |
}); | |
}; | |
} | |
clock.clearImmediate = function clearImmediate(timerId) { | |
return clearTimer(clock, timerId, "Immediate"); | |
}; | |
} | |
clock.countTimers = function countTimers() { | |
return ( | |
Object.keys(clock.timers || {}).length + | |
(clock.jobs || []).length | |
); | |
}; | |
clock.requestAnimationFrame = function requestAnimationFrame(func) { | |
const result = addTimer(clock, { | |
func: func, | |
delay: getTimeToNextFrame(), | |
args: [clock.now + getTimeToNextFrame()], | |
animation: true, | |
}); | |
return Number(result); | |
}; | |
clock.cancelAnimationFrame = function cancelAnimationFrame(timerId) { | |
return clearTimer(clock, timerId, "AnimationFrame"); | |
}; | |
clock.runMicrotasks = function runMicrotasks() { | |
runJobs(clock); | |
}; | |
/** | |
* @param {number|string} tickValue milliseconds or a string parseable by parseTime | |
* @param {boolean} isAsync | |
* @param {Function} resolve | |
* @param {Function} reject | |
* @returns {number|undefined} will return the new `now` value or nothing for async | |
*/ | |
function doTick(tickValue, isAsync, resolve, reject) { | |
const msFloat = | |
typeof tickValue === "number" | |
? tickValue | |
: parseTime(tickValue); | |
const ms = Math.floor(msFloat); | |
const remainder = nanoRemainder(msFloat); | |
let nanosTotal = nanos + remainder; | |
let tickTo = clock.now + ms; | |
if (msFloat < 0) { | |
throw new TypeError("Negative ticks are not supported"); | |
} | |
// adjust for positive overflow | |
if (nanosTotal >= 1e6) { | |
tickTo += 1; | |
nanosTotal -= 1e6; | |
} | |
nanos = nanosTotal; | |
let tickFrom = clock.now; | |
let previous = clock.now; | |
// ESLint fails to detect this correctly | |
/* eslint-disable prefer-const */ | |
let timer, | |
firstException, | |
oldNow, | |
nextPromiseTick, | |
compensationCheck, | |
postTimerCall; | |
/* eslint-enable prefer-const */ | |
clock.duringTick = true; | |
// perform microtasks | |
oldNow = clock.now; | |
runJobs(clock); | |
if (oldNow !== clock.now) { | |
// compensate for any setSystemTime() call during microtask callback | |
tickFrom += clock.now - oldNow; | |
tickTo += clock.now - oldNow; | |
} | |
//eslint-disable-next-line jsdoc/require-jsdoc | |
function doTickInner() { | |
// perform each timer in the requested range | |
timer = firstTimerInRange(clock, tickFrom, tickTo); | |
// eslint-disable-next-line no-unmodified-loop-condition | |
while (timer && tickFrom <= tickTo) { | |
if (clock.timers[timer.id]) { | |
tickFrom = timer.callAt; | |
clock.now = timer.callAt; | |
oldNow = clock.now; | |
try { | |
runJobs(clock); | |
callTimer(clock, timer); | |
} catch (e) { | |
firstException = firstException || e; | |
} | |
if (isAsync) { | |
// finish up after native setImmediate callback to allow | |
// all native es6 promises to process their callbacks after | |
// each timer fires. | |
originalSetTimeout(nextPromiseTick); | |
return; | |
} | |
compensationCheck(); | |
} | |
postTimerCall(); | |
} | |
// perform process.nextTick()s again | |
oldNow = clock.now; | |
runJobs(clock); | |
if (oldNow !== clock.now) { | |
// compensate for any setSystemTime() call during process.nextTick() callback | |
tickFrom += clock.now - oldNow; | |
tickTo += clock.now - oldNow; | |
} | |
clock.duringTick = false; | |
// corner case: during runJobs new timers were scheduled which could be in the range [clock.now, tickTo] | |
timer = firstTimerInRange(clock, tickFrom, tickTo); | |
if (timer) { | |
try { | |
clock.tick(tickTo - clock.now); // do it all again - for the remainder of the requested range | |
} catch (e) { | |
firstException = firstException || e; | |
} | |
} else { | |
// no timers remaining in the requested range: move the clock all the way to the end | |
clock.now = tickTo; | |
// update nanos | |
nanos = nanosTotal; | |
} | |
if (firstException) { | |
throw firstException; | |
} | |
if (isAsync) { | |
resolve(clock.now); | |
} else { | |
return clock.now; | |
} | |
} | |
nextPromiseTick = | |
isAsync && | |
function () { | |
try { | |
compensationCheck(); | |
postTimerCall(); | |
doTickInner(); | |
} catch (e) { | |
reject(e); | |
} | |
}; | |
compensationCheck = function () { | |
// compensate for any setSystemTime() call during timer callback | |
if (oldNow !== clock.now) { | |
tickFrom += clock.now - oldNow; | |
tickTo += clock.now - oldNow; | |
previous += clock.now - oldNow; | |
} | |
}; | |
postTimerCall = function () { | |
timer = firstTimerInRange(clock, previous, tickTo); | |
previous = tickFrom; | |
}; | |
return doTickInner(); | |
} | |
/** | |
* @param {string|number} tickValue number of milliseconds or a human-readable value like "01:11:15" | |
* @returns {number} will return the new `now` value | |
*/ | |
clock.tick = function tick(tickValue) { | |
return doTick(tickValue, false); | |
}; | |
if (typeof _global.Promise !== "undefined") { | |
/** | |
* @param {string|number} tickValue number of milliseconds or a human-readable value like "01:11:15" | |
* @returns {Promise} | |
*/ | |
clock.tickAsync = function tickAsync(tickValue) { | |
return new _global.Promise(function (resolve, reject) { | |
originalSetTimeout(function () { | |
try { | |
doTick(tickValue, true, resolve, reject); | |
} catch (e) { | |
reject(e); | |
} | |
}); | |
}); | |
}; | |
} | |
clock.next = function next() { | |
runJobs(clock); | |
const timer = firstTimer(clock); | |
if (!timer) { | |
return clock.now; | |
} | |
clock.duringTick = true; | |
try { | |
clock.now = timer.callAt; | |
callTimer(clock, timer); | |
runJobs(clock); | |
return clock.now; | |
} finally { | |
clock.duringTick = false; | |
} | |
}; | |
if (typeof _global.Promise !== "undefined") { | |
clock.nextAsync = function nextAsync() { | |
return new _global.Promise(function (resolve, reject) { | |
originalSetTimeout(function () { | |
try { | |
const timer = firstTimer(clock); | |
if (!timer) { | |
resolve(clock.now); | |
return; | |
} | |
let err; | |
clock.duringTick = true; | |
clock.now = timer.callAt; | |
try { | |
callTimer(clock, timer); | |
} catch (e) { | |
err = e; | |
} | |
clock.duringTick = false; | |
originalSetTimeout(function () { | |
if (err) { | |
reject(err); | |
} else { | |
resolve(clock.now); | |
} | |
}); | |
} catch (e) { | |
reject(e); | |
} | |
}); | |
}); | |
}; | |
} | |
clock.runAll = function runAll() { | |
let numTimers, i; | |
runJobs(clock); | |
for (i = 0; i < clock.loopLimit; i++) { | |
if (!clock.timers) { | |
resetIsNearInfiniteLimit(); | |
return clock.now; | |
} | |
numTimers = Object.keys(clock.timers).length; | |
if (numTimers === 0) { | |
resetIsNearInfiniteLimit(); | |
return clock.now; | |
} | |
clock.next(); | |
checkIsNearInfiniteLimit(clock, i); | |
} | |
const excessJob = firstTimer(clock); | |
throw getInfiniteLoopError(clock, excessJob); | |
}; | |
clock.runToFrame = function runToFrame() { | |
return clock.tick(getTimeToNextFrame()); | |
}; | |
if (typeof _global.Promise !== "undefined") { | |
clock.runAllAsync = function runAllAsync() { | |
return new _global.Promise(function (resolve, reject) { | |
let i = 0; | |
/** | |
* | |
*/ | |
function doRun() { | |
originalSetTimeout(function () { | |
try { | |
let numTimers; | |
if (i < clock.loopLimit) { | |
if (!clock.timers) { | |
resetIsNearInfiniteLimit(); | |
resolve(clock.now); | |
return; | |
} | |
numTimers = Object.keys(clock.timers) | |
.length; | |
if (numTimers === 0) { | |
resetIsNearInfiniteLimit(); | |
resolve(clock.now); | |
return; | |
} | |
clock.next(); | |
i++; | |
doRun(); | |
checkIsNearInfiniteLimit(clock, i); | |
return; | |
} | |
const excessJob = firstTimer(clock); | |
reject(getInfiniteLoopError(clock, excessJob)); | |
} catch (e) { | |
reject(e); | |
} | |
}); | |
} | |
doRun(); | |
}); | |
}; | |
} | |
clock.runToLast = function runToLast() { | |
const timer = lastTimer(clock); | |
if (!timer) { | |
runJobs(clock); | |
return clock.now; | |
} | |
return clock.tick(timer.callAt - clock.now); | |
}; | |
if (typeof _global.Promise !== "undefined") { | |
clock.runToLastAsync = function runToLastAsync() { | |
return new _global.Promise(function (resolve, reject) { | |
originalSetTimeout(function () { | |
try { | |
const timer = lastTimer(clock); | |
if (!timer) { | |
resolve(clock.now); | |
} | |
resolve(clock.tickAsync(timer.callAt)); | |
} catch (e) { | |
reject(e); | |
} | |
}); | |
}); | |
}; | |
} | |
clock.reset = function reset() { | |
nanos = 0; | |
clock.timers = {}; | |
clock.jobs = []; | |
clock.now = start; | |
}; | |
clock.setSystemTime = function setSystemTime(systemTime) { | |
// determine time difference | |
const newNow = getEpoch(systemTime); | |
const difference = newNow - clock.now; | |
let id, timer; | |
adjustedSystemTime[0] = adjustedSystemTime[0] + difference; | |
adjustedSystemTime[1] = adjustedSystemTime[1] + nanos; | |
// update 'system clock' | |
clock.now = newNow; | |
nanos = 0; | |
// update timers and intervals to keep them stable | |
for (id in clock.timers) { | |
if (clock.timers.hasOwnProperty(id)) { | |
timer = clock.timers[id]; | |
timer.createdAt += difference; | |
timer.callAt += difference; | |
} | |
} | |
}; | |
if (performancePresent) { | |
clock.performance = Object.create(null); | |
if (hasPerformancePrototype) { | |
const proto = _global.Performance.prototype; | |
Object.getOwnPropertyNames(proto).forEach(function (name) { | |
if (name.indexOf("getEntries") === 0) { | |
// match expected return type for getEntries functions | |
clock.performance[name] = NOOP_ARRAY; | |
} else { | |
clock.performance[name] = NOOP; | |
} | |
}); | |
} | |
clock.performance.now = function FakeTimersNow() { | |
const hrt = hrtime(); | |
const millis = hrt[0] * 1000 + hrt[1] / 1e6; | |
return millis; | |
}; | |
} | |
if (hrtimePresent) { | |
clock.hrtime = hrtime; | |
} | |
return clock; | |
} | |
/* eslint-disable complexity */ | |
/** | |
* @param {Config=} [config] Optional config | |
* @returns {Clock} | |
*/ | |
function install(config) { | |
if ( | |
arguments.length > 1 || | |
config instanceof Date || | |
Array.isArray(config) || | |
typeof config === "number" | |
) { | |
throw new TypeError( | |
`FakeTimers.install called with ${String( | |
config | |
)} install requires an object parameter` | |
); | |
} | |
// eslint-disable-next-line no-param-reassign | |
config = typeof config !== "undefined" ? config : {}; | |
config.shouldAdvanceTime = config.shouldAdvanceTime || false; | |
config.advanceTimeDelta = config.advanceTimeDelta || 20; | |
config.shouldClearNativeTimers = | |
config.shouldClearNativeTimers || false; | |
if (config.target) { | |
throw new TypeError( | |
"config.target is no longer supported. Use `withGlobal(target)` instead." | |
); | |
} | |
let i, l; | |
const clock = createClock(config.now, config.loopLimit); | |
clock.shouldClearNativeTimers = config.shouldClearNativeTimers; | |
clock.uninstall = function () { | |
return uninstall(clock, config); | |
}; | |
clock.methods = config.toFake || []; | |
if (clock.methods.length === 0) { | |
// do not fake nextTick by default - GitHub#126 | |
clock.methods = Object.keys(timers).filter(function (key) { | |
return key !== "nextTick" && key !== "queueMicrotask"; | |
}); | |
} | |
if (config.shouldAdvanceTime === true) { | |
const intervalTick = doIntervalTick.bind( | |
null, | |
clock, | |
config.advanceTimeDelta | |
); | |
const intervalId = _global.setInterval( | |
intervalTick, | |
config.advanceTimeDelta | |
); | |
clock.attachedInterval = intervalId; | |
} | |
for (i = 0, l = clock.methods.length; i < l; i++) { | |
const nameOfMethodToReplace = clock.methods[i]; | |
if (nameOfMethodToReplace === "hrtime") { | |
if ( | |
_global.process && | |
typeof _global.process.hrtime === "function" | |
) { | |
hijackMethod(_global.process, nameOfMethodToReplace, clock); | |
} | |
} else if (nameOfMethodToReplace === "nextTick") { | |
if ( | |
_global.process && | |
typeof _global.process.nextTick === "function" | |
) { | |
hijackMethod(_global.process, nameOfMethodToReplace, clock); | |
} | |
} else { | |
hijackMethod(_global, nameOfMethodToReplace, clock); | |
} | |
} | |
return clock; | |
} | |
/* eslint-enable complexity */ | |
return { | |
timers: timers, | |
createClock: createClock, | |
install: install, | |
withGlobal: withGlobal, | |
}; | |
} | |
/** | |
* @typedef {object} FakeTimers | |
* @property {Timers} timers | |
* @property {createClock} createClock | |
* @property {Function} install | |
* @property {withGlobal} withGlobal | |
*/ | |
/* eslint-enable complexity */ | |
/** @type {FakeTimers} */ | |
const defaultImplementation = withGlobal(globalObject); | |
exports.timers = defaultImplementation.timers; | |
exports.createClock = defaultImplementation.createClock; | |
exports.install = defaultImplementation.install; | |
exports.withGlobal = withGlobal; | |