diff --git a/src/fake-timers-src.js b/src/fake-timers-src.js index 0ce7217..fb8eaaf 100644 --- a/src/fake-timers-src.js +++ b/src/fake-timers-src.js @@ -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 <> 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 */ @@ -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 && @@ -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} @@ -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; } @@ -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(), @@ -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; } @@ -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, @@ -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; } @@ -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; @@ -1798,10 +1828,7 @@ 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) { @@ -1809,6 +1836,13 @@ function withGlobal(_global) { } 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 && diff --git a/test/fake-timers-test.js b/test/fake-timers-test.js index 73d3929..58fadb2 100644 --- a/test/fake-timers-test.js +++ b/test/fake-timers-test.js @@ -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(); @@ -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 () { @@ -4883,7 +4853,9 @@ describe("FakeTimers", function () { Date: Date, setTimeout: sinon.fake(), clearTimeout: sinon.fake(), - }).install(); + }).install({ + ignoreMissingTimers: true, + }); assert.same(timersModule.setTimeout, original); }); }); @@ -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], + }); + //}); + }); + }); +}); diff --git a/test/issue-1852-test.js b/test/issue-1852-test.js index 1c84ee6..fff0813 100644 --- a/test/issue-1852-test.js +++ b/test/issue-1852-test.js @@ -4,15 +4,11 @@ const { FakeTimers, assert } = require("./helpers/setup-tests"); describe("issue sinon#1852", function () { it("throws when creating a clock and global has no Date", function () { - const clock = FakeTimers.withGlobal({ - setTimeout: function () {}, - clearTimeout: function () {}, - }); - assert.exception(function () { - clock.createClock(); - }); assert.exception(function () { - clock.install(); + FakeTimers.withGlobal({ + setTimeout: function () {}, + clearTimeout: function () {}, + }); }); }); }); diff --git a/test/issue-2449-test.js b/test/issue-2449-test.js index 416e402..cf32d04 100644 --- a/test/issue-2449-test.js +++ b/test/issue-2449-test.js @@ -21,20 +21,25 @@ describe("issue #2449: permanent loss of native functions", function () { }); it("should not fake faked timers on a custom target", function () { - const setTimeoutFake = sinon.fake(); const context = { Date: Date, - setTimeout: setTimeoutFake, + setTimeout: sinon.fake(), clearTimeout: sinon.fake(), }; - let clock = FakeTimers.withGlobal(context).install(); + let clock = FakeTimers.withGlobal(context).install({ + ignoreMissingTimers: true, + }); assert.exception(function () { - clock = FakeTimers.withGlobal(context).install(); + clock = FakeTimers.withGlobal(context).install({ + ignoreMissingTimers: true, + }); }); clock.uninstall(); // After uninstaling we should be able to install without issue - clock = FakeTimers.withGlobal(context).install(); + clock = FakeTimers.withGlobal(context).install({ + ignoreMissingTimers: true, + }); clock.uninstall(); }); @@ -49,7 +54,9 @@ describe("issue #2449: permanent loss of native functions", function () { }; assert.equals(new context.Date().getTime(), 0); assert.exception(function () { - FakeTimers.withGlobal(context).install(); + FakeTimers.withGlobal(context).install({ + ignoreMissingTimers: true, + }); }); globalClock.uninstall(); @@ -63,7 +70,9 @@ describe("issue #2449: permanent loss of native functions", function () { setTimeout: setTimeoutFake, clearTimeout: sinon.fake(), }; - const clock = FakeTimers.withGlobal(context).install(); + const clock = FakeTimers.withGlobal(context).install({ + ignoreMissingTimers: true, + }); assert.equals(new context.Date().getTime(), 0); refute.equals(new Date().getTime(), 0); const globalClock = FakeTimers.install(); diff --git a/test/issue-59-test.js b/test/issue-59-test.js index e0202a2..c8e30ef 100644 --- a/test/issue-59-test.js +++ b/test/issue-59-test.js @@ -10,7 +10,9 @@ describe("issue #59", function () { setTimeout: setTimeoutFake, clearTimeout: sinon.fake(), }; - const clock = FakeTimers.withGlobal(context).install(); + const clock = FakeTimers.withGlobal(context).install({ + ignoreMissingTimers: true, + }); assert.equals(setTimeoutFake.callCount, 1); clock.setTimeout(NOOP, 0); assert.equals(setTimeoutFake.callCount, 1);