Skip to content

Commit

Permalink
Fail when a timer is not available in the context (#491)
Browse files Browse the repository at this point in the history
* Fail when a timer is not available in the context

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.

* Remove JSDoc type as it gave no intellisense improvement
  • Loading branch information
fatso83 committed Feb 12, 2024
1 parent cc58937 commit 3412033
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 108 deletions.
116 changes: 75 additions & 41 deletions src/fake-timers-src.js
Expand Up @@ -107,6 +107,7 @@ 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
*/

/* eslint-disable jsdoc/require-property-description */
Expand Down Expand Up @@ -151,16 +152,26 @@ function withGlobal(_global) {
const NOOP_ARRAY = function () {
return [];
};
const timeoutResult = _global.setTimeout(NOOP, 0);
const addTimerReturnsObject = typeof timeoutResult === "object";
const hrtimePresent =
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 +180,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 +1065,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 +1121,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 +1181,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 +1273,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 +1712,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 +1765,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 +1828,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
109 changes: 58 additions & 51 deletions test/fake-timers-test.js
Expand Up @@ -3558,27 +3558,6 @@ describe("FakeTimers", function () {
});
}

it("throws when adding performance to tofake array when performance not present", function () {
assert.exception(
function () {
const setTimeoutFake = sinon.fake();
const context = {
Date: Date,
setTimeout: setTimeoutFake,
clearTimeout: sinon.fake(),
performance: undefined,
};
FakeTimers.withGlobal(context).install({
toFake: ["performance"],
});
},
{
name: "ReferenceError",
message: "non-existent performance object cannot be faked",
},
);
});

if (performanceNowPresent) {
it("replaces global performance.now", function () {
this.clock = FakeTimers.install();
Expand Down Expand Up @@ -3694,42 +3673,33 @@ describe("FakeTimers", function () {
}

/* eslint-disable mocha/no-setup-in-describe */
if (Object.getPrototypeOf(global)) {
delete global.hasOwnPropertyTest;
Object.getPrototypeOf(global).hasOwnPropertyTest = function () {};

if (!global.hasOwnProperty("hasOwnPropertyTest")) {
it("deletes global property on uninstall if it was inherited onto the global object", function () {
// Give the global object an inherited 'tick' method
delete global.tick;
Object.getPrototypeOf(global).tick = function () {};

this.clock = FakeTimers.install({
now: 0,
toFake: ["tick"],
});
assert.isTrue(global.hasOwnProperty("tick"));
this.clock.uninstall();
it("deletes global property on uninstall if it was inherited onto the global object", function () {
// Give the global object an inherited 'setTimeout' method
const proto = { Date, setTimeout: NOOP };
const myGlobal = Object.create(proto);

assert.isFalse(global.hasOwnProperty("tick"));
delete Object.getPrototypeOf(global).tick;
});
}
this.clock = FakeTimers.withGlobal(myGlobal).install({
now: 0,
toFake: ["setTimeout"],
});
assert.isTrue(myGlobal.hasOwnProperty("setTimeout"));
this.clock.uninstall();

delete Object.getPrototypeOf(global).hasOwnPropertyTest;
}
/* eslint-enable mocha/no-setup-in-describe */
assert.isFalse(myGlobal.hasOwnProperty("setTimeout"));
});

it("uninstalls global property on uninstall if it is present on the global object itself", function () {
// Directly give the global object a tick method
global.tick = NOOP;
// Directly give the global object a setTimeout method
const myGlobal = { Date, setTimeout: NOOP };

this.clock = FakeTimers.install({ now: 0, toFake: ["tick"] });
assert.isTrue(global.hasOwnProperty("tick"));
this.clock = FakeTimers.withGlobal(myGlobal).install({
now: 0,
toFake: ["setTimeout"],
});
assert.isTrue(myGlobal.hasOwnProperty("setTimeout"));
this.clock.uninstall();

assert.isTrue(global.hasOwnProperty("tick"));
delete global.tick;
assert.isTrue(myGlobal.hasOwnProperty("setTimeout"));
});

it("fakes Date constructor", function () {
Expand Down Expand Up @@ -4883,7 +4853,9 @@ describe("FakeTimers", function () {
Date: Date,
setTimeout: sinon.fake(),
clearTimeout: sinon.fake(),
}).install();
}).install({
ignoreMissingTimers: true,
});
assert.same(timersModule.setTimeout, original);
});
});
Expand Down Expand Up @@ -5401,3 +5373,38 @@ describe("Intl API", function () {
assert.equals(rtf.format(2, "day"), "in 2 days");
});
});

describe("missing timers", function () {
const timers = [
"performance",
"setTimeout",
"setImmediate",
"someWeirdlyNamedFutureTimer",
];

// eslint-disable-next-line mocha/no-setup-in-describe
timers.forEach((timer) => {
it(`should throw on encountering timers in toFake not present in "global": [${timer}]`, function () {
assert.exception(
function () {
FakeTimers.withGlobal({ Date }).install({
toFake: [timer],
});
},
{
name: "ReferenceError",
message: `non-existent timers and/or objects cannot be faked: '${timer}'`,
},
);
});

it(`should ignore timers in toFake that are not present in "global" when passed the ignore flag: [${timer}]`, function () {
//refute.exception(function () {
FakeTimers.withGlobal({ Date }).install({
ignoreMissingTimers: true,
toFake: [timer],
});
//});
});
});
});

0 comments on commit 3412033

Please sign in to comment.