Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fail when a timer is not available in the context #491

Merged
merged 2 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
116 changes: 75 additions & 41 deletions src/fake-timers-src.js
Expand Up @@ -47,13 +47,13 @@

/**
* @typedef RequestAnimationFrame
* @property {function(number):void} requestAnimationFrame

Check warning on line 50 in src/fake-timers-src.js

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @Property "requestAnimationFrame" description
* @returns {number} - the id
*/

/**
* @typedef Performance
* @property {function(): number} now

Check warning on line 56 in src/fake-timers-src.js

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @Property "now" description
*/

/* eslint-disable jsdoc/require-property-description */
Expand Down Expand Up @@ -107,6 +107,7 @@
* @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 @@
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 @@
_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 @@ -307,7 +330,7 @@
return timer && timer.callAt >= from && timer.callAt <= to;
}

/**

Check warning on line 333 in src/fake-timers-src.js

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @returns declaration
* @param {Clock} clock
* @param {Timer} job
*/
Expand Down Expand Up @@ -658,7 +681,7 @@
}

/* eslint consistent-return: "off" */
/**

Check warning on line 684 in src/fake-timers-src.js

View workflow job for this annotation

GitHub Actions / lint

JSDoc @returns declaration present but return expression not available in function
* Timer comparitor
*
* @param {Timer} a
Expand Down Expand Up @@ -790,7 +813,7 @@
}
}

/**

Check warning on line 816 in src/fake-timers-src.js

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @returns declaration
* Gets clear handler name for a given timer type
*
* @param {string} ttype
Expand All @@ -802,7 +825,7 @@
return `clear${ttype}`;
}

/**

Check warning on line 828 in src/fake-timers-src.js

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @returns declaration
* Gets schedule handler name for a given timer type
*
* @param {string} ttype
Expand All @@ -814,7 +837,7 @@
return `set${ttype}`;
}

/**

Check warning on line 840 in src/fake-timers-src.js

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @returns declaration
* Creates an anonymous function to warn only once
*/
function createWarnOnce() {
Expand All @@ -826,7 +849,7 @@
}
const warnOnce = createWarnOnce();

/**

Check warning on line 852 in src/fake-timers-src.js

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @returns declaration
* @param {Clock} clock
* @param {number} timerId
* @param {string} ttype
Expand Down Expand Up @@ -1015,8 +1038,8 @@

/**
* @typedef {object} Timers
* @property {setTimeout} setTimeout

Check warning on line 1041 in src/fake-timers-src.js

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @Property "setTimeout" description
* @property {clearTimeout} clearTimeout

Check warning on line 1042 in src/fake-timers-src.js

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @Property "clearTimeout" description
* @property {setInterval} setInterval
* @property {clearInterval} clearInterval
* @property {Date} Date
Expand All @@ -1042,44 +1065,44 @@
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 @@
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 @@
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 @@
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 @@
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 @@
);
}

/**
* @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 @@
}
});
} 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],
});
//});
});
});
});