Skip to content

Commit

Permalink
Fail when a timer is not available in the context
Browse files Browse the repository at this point in the history
This makes FakeTimers stricter by failing when trying to fake
a timer or object that is not present on the (chosen) global object.

To make transitioning easier, we provide a flag to revert to the
behavior where missing timers were (mostly) ignored.
  • Loading branch information
fatso83 committed Feb 10, 2024
1 parent cc58937 commit dd831b1
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 108 deletions.
129 changes: 88 additions & 41 deletions src/fake-timers-src.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,19 @@ if (typeof require === "function" && typeof module === "object") {
* @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)
* @property {boolean} [ignoreMissingTimers] default is false, meaning asking to fake timers that are not present will throw an error
*/

/**
* Map of the potential timers/objects to fake and their presence in the passed in global
*
* @typedef {object} TimerPresenceMap
* @property {boolean} performance whether or not global.performance is there
* @property {boolean} setTimeout is setTimeout present
* @property {boolean} setImmediate is setImmediate present
* @property {boolean} hrtime is 'hrtime' present
* @property {boolean} hrtimeBigint is 'hrtimeBigint' present
* @property {boolean} queueMicrotask is 'queueMicrotask' present
*/

/* eslint-disable jsdoc/require-property-description */
Expand Down Expand Up @@ -151,16 +164,27 @@ function withGlobal(_global) {
const NOOP_ARRAY = function () {
return [];
};
const timeoutResult = _global.setTimeout(NOOP, 0);
const addTimerReturnsObject = typeof timeoutResult === "object";
const hrtimePresent =
/** @type {TimerPresenceMap} */
const isPresent = {};
let timeoutResult,
addTimerReturnsObject = false;

if (_global.setTimeout) {
isPresent.setTimeout = true;
timeoutResult = _global.setTimeout(NOOP, 0);
addTimerReturnsObject = typeof timeoutResult === "object";
}
isPresent.clearTimeout = Boolean(_global.clearTimeout);
isPresent.setInterval = Boolean(_global.setInterval);
isPresent.clearInterval = Boolean(_global.clearInterval);
isPresent.hrtime =
_global.process && typeof _global.process.hrtime === "function";
const hrtimeBigintPresent =
hrtimePresent && typeof _global.process.hrtime.bigint === "function";
const nextTickPresent =
isPresent.hrtimeBigint =
isPresent.hrtime && typeof _global.process.hrtime.bigint === "function";
isPresent.nextTick =
_global.process && typeof _global.process.nextTick === "function";
const utilPromisify = _global.process && require("util").promisify;
const performancePresent =
isPresent.performance =
_global.performance && typeof _global.performance.now === "function";
const hasPerformancePrototype =
_global.Performance &&
Expand All @@ -169,29 +193,41 @@ function withGlobal(_global) {
_global.performance &&
_global.performance.constructor &&
_global.performance.constructor.prototype;
const queueMicrotaskPresent = _global.hasOwnProperty("queueMicrotask");
const requestAnimationFramePresent =
isPresent.queueMicrotask = _global.hasOwnProperty("queueMicrotask");
isPresent.requestAnimationFrame =
_global.requestAnimationFrame &&
typeof _global.requestAnimationFrame === "function";
const cancelAnimationFramePresent =
isPresent.cancelAnimationFrame =
_global.cancelAnimationFrame &&
typeof _global.cancelAnimationFrame === "function";
const requestIdleCallbackPresent =
isPresent.requestIdleCallback =
_global.requestIdleCallback &&
typeof _global.requestIdleCallback === "function";
const cancelIdleCallbackPresent =
isPresent.cancelIdleCallbackPresent =
_global.cancelIdleCallback &&
typeof _global.cancelIdleCallback === "function";
const setImmediatePresent =
isPresent.setImmediate =
_global.setImmediate && typeof _global.setImmediate === "function";
const intlPresent = _global.Intl && typeof _global.Intl === "object";
isPresent.clearImmediate =
_global.clearImmediate && typeof _global.clearImmediate === "function";
isPresent.Intl = _global.Intl && typeof _global.Intl === "object";

_global.clearTimeout(timeoutResult);
if (_global.clearTimeout) {
_global.clearTimeout(timeoutResult);
}

const NativeDate = _global.Date;
const NativeIntl = _global.Intl;
let uniqueTimerId = idCounterStart;

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)",
);
}
isPresent.Date = true;

/**
* @param {number} num
* @returns {boolean}
Expand Down Expand Up @@ -1042,44 +1078,44 @@ function withGlobal(_global) {
Date: _global.Date,
};

if (setImmediatePresent) {
if (isPresent.setImmediate) {
timers.setImmediate = _global.setImmediate;
timers.clearImmediate = _global.clearImmediate;
}

if (hrtimePresent) {
if (isPresent.hrtime) {
timers.hrtime = _global.process.hrtime;
}

if (nextTickPresent) {
if (isPresent.nextTick) {
timers.nextTick = _global.process.nextTick;
}

if (performancePresent) {
if (isPresent.performance) {
timers.performance = _global.performance;
}

if (requestAnimationFramePresent) {
if (isPresent.requestAnimationFrame) {
timers.requestAnimationFrame = _global.requestAnimationFrame;
}

if (queueMicrotaskPresent) {
if (isPresent.queueMicrotask) {
timers.queueMicrotask = true;
}

if (cancelAnimationFramePresent) {
if (isPresent.cancelAnimationFrame) {
timers.cancelAnimationFrame = _global.cancelAnimationFrame;
}

if (requestIdleCallbackPresent) {
if (isPresent.requestIdleCallback) {
timers.requestIdleCallback = _global.requestIdleCallback;
}

if (cancelIdleCallbackPresent) {
if (isPresent.cancelIdleCallback) {
timers.cancelIdleCallback = _global.cancelIdleCallback;
}

if (intlPresent) {
if (isPresent.Intl) {
timers.Intl = _global.Intl;
}

Expand All @@ -1098,13 +1134,6 @@ function withGlobal(_global) {
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(),
Expand Down Expand Up @@ -1165,14 +1194,14 @@ function withGlobal(_global) {
return millis;
}

if (hrtimeBigintPresent) {
if (isPresent.hrtimeBigint) {
hrtime.bigint = function () {
const parts = hrtime();
return BigInt(parts[0]) * BigInt(1e9) + BigInt(parts[1]); // eslint-disable-line
};
}

if (intlPresent) {
if (isPresent.Intl) {
clock.Intl = createIntl();
clock.Intl.clock = clock;
}
Expand Down Expand Up @@ -1257,7 +1286,7 @@ function withGlobal(_global) {
return clearTimer(clock, timerId, "Interval");
};

if (setImmediatePresent) {
if (isPresent.setImmediate) {
clock.setImmediate = function setImmediate(func) {
return addTimer(clock, {
func: func,
Expand Down Expand Up @@ -1696,12 +1725,12 @@ function withGlobal(_global) {
clock.tick(ms);
};

if (performancePresent) {
if (isPresent.performance) {
clock.performance = Object.create(null);
clock.performance.now = fakePerformanceNow;
}

if (hrtimePresent) {
if (isPresent.hrtime) {
clock.hrtime = hrtime;
}

Expand Down Expand Up @@ -1749,6 +1778,20 @@ function withGlobal(_global) {
);
}

/**
* @param {string} timer/object the name of the thing that is not present
* @param timer
*/
function handleMissingTimer(timer) {
if (config.ignoreMissingTimers) {
return;
}

throw new ReferenceError(
`non-existent timers and/or objects cannot be faked: '${timer}'`,
);
}

let i, l;
const clock = createClock(config.now, config.loopLimit);
clock.shouldClearNativeTimers = config.shouldClearNativeTimers;
Expand Down Expand Up @@ -1798,17 +1841,21 @@ function withGlobal(_global) {
}
});
} else if ((config.toFake || []).includes("performance")) {
// user explicitly tried to fake performance when not present
throw new ReferenceError(
"non-existent performance object cannot be faked",
);
return handleMissingTimer("performance");
}
}
if (_global === globalObject && timersModule) {
clock.timersModuleMethods = [];
}
for (i = 0, l = clock.methods.length; i < l; i++) {
const nameOfMethodToReplace = clock.methods[i];

if (!isPresent[nameOfMethodToReplace]) {
handleMissingTimer(nameOfMethodToReplace);
// eslint-disable-next-line
continue;
}

if (nameOfMethodToReplace === "hrtime") {
if (
_global.process &&
Expand Down

0 comments on commit dd831b1

Please sign in to comment.