From 190a13ed96c80818265c77a679ae5cedd62d5126 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Thu, 25 Sep 2025 20:56:19 -0700 Subject: [PATCH] Prevent mock clock timing fns from being spied on Fixes #826 --- lib/jasmine-core/jasmine.js | 13 +++++++++++++ spec/core/ClockSpec.js | 35 +++++++++++++++++++++++++++++++++++ spec/core/SpyRegistrySpec.js | 12 ++++++++++++ src/core/Clock.js | 7 +++++++ src/core/SpyRegistry.js | 6 ++++++ 5 files changed, 73 insertions(+) diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 3c046ce9..cc9769ce 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -2912,6 +2912,8 @@ getJasmineRequireObj().Clock = function() { process.versions && typeof process.versions.node === 'string'; + const IsMockClockTimingFn = Symbol('IsMockClockTimingFn'); + /** * @class Clock * @since 1.3.0 @@ -3090,6 +3092,10 @@ callbacks to execute _before_ running the next one. advanceUntilModeChanges(); }; + setTimeout[IsMockClockTimingFn] = true; + clearTimeout[IsMockClockTimingFn] = true; + setInterval[IsMockClockTimingFn] = true; + clearInterval[IsMockClockTimingFn] = true; return this; // Advances the Clock's time until the mode changes. @@ -3242,6 +3248,7 @@ callbacks to execute _before_ running the next one. return this; }; + Clock.IsMockClockTimingFn = IsMockClockTimingFn; return Clock; }; @@ -10046,6 +10053,12 @@ getJasmineRequireObj().SpyRegistry = function(j$) { throw new Error(getErrorMsg(methodName + '() method does not exist')); } + // Spying on mock clock timing fns would prevent the real ones from being + // restored. + if (obj[methodName] && obj[methodName][j$.Clock.IsMockClockTimingFn]) { + throw new Error("Mock clock timing functions can't be spied on"); + } + if (obj[methodName] && j$.isSpy(obj[methodName])) { if (this.respy) { return obj[methodName]; diff --git a/spec/core/ClockSpec.js b/spec/core/ClockSpec.js index ad47f823..2fb48d65 100644 --- a/spec/core/ClockSpec.js +++ b/spec/core/ClockSpec.js @@ -408,6 +408,41 @@ describe('Clock', function() { expect(delayedFunctionScheduler.scheduleFunction).not.toHaveBeenCalled(); }); + it('identifies its timing functions', function() { + const fakeSetTimeout = jasmine.createSpy('global setTimeout'); + const fakeGlobal = { setTimeout: fakeSetTimeout }; + const delayedFunctionScheduler = jasmine.createSpyObj( + 'delayedFunctionScheduler', + ['scheduleFunction'] + ); + const mockDate = { + install: function() {}, + tick: function() {}, + uninstall: function() {} + }; + const clock = new jasmineUnderTest.Clock( + fakeGlobal, + function() { + return delayedFunctionScheduler; + }, + mockDate + ); + clock.install(); + + expect( + fakeGlobal.setTimeout[jasmineUnderTest.Clock.IsMockClockTimingFn] + ).toEqual(true); + expect( + fakeGlobal.clearTimeout[jasmineUnderTest.Clock.IsMockClockTimingFn] + ).toEqual(true); + expect( + fakeGlobal.setInterval[jasmineUnderTest.Clock.IsMockClockTimingFn] + ).toEqual(true); + expect( + fakeGlobal.clearInterval[jasmineUnderTest.Clock.IsMockClockTimingFn] + ).toEqual(true); + }); + describe('setTimeout', function() { it('schedules the delayed function with the fake timer', function() { const fakeSetTimeout = jasmine.createSpy('setTimeout'), diff --git a/spec/core/SpyRegistrySpec.js b/spec/core/SpyRegistrySpec.js index 5e72f90e..74ba4a2d 100644 --- a/spec/core/SpyRegistrySpec.js +++ b/spec/core/SpyRegistrySpec.js @@ -135,6 +135,18 @@ describe('SpyRegistry', function() { target.spiedFunc(); expect(originalFunctionWasCalled).toBe(false); }); + + it('throws if the method is a mock clock method', function() { + const spyRegistry = new jasmineUnderTest.SpyRegistry({ + createSpy: createSpy + }); + const target = { spiedFunc: function() {} }; + target.spiedFunc[jasmineUnderTest.Clock.IsMockClockTimingFn] = true; + + expect(function() { + spyRegistry.spyOn(target, 'spiedFunc'); + }).toThrowError("Mock clock timing functions can't be spied on"); + }); }); describe('#spyOnProperty', function() { diff --git a/src/core/Clock.js b/src/core/Clock.js index c537d069..4956fcac 100644 --- a/src/core/Clock.js +++ b/src/core/Clock.js @@ -5,6 +5,8 @@ getJasmineRequireObj().Clock = function() { process.versions && typeof process.versions.node === 'string'; + const IsMockClockTimingFn = Symbol('IsMockClockTimingFn'); + /** * @class Clock * @since 1.3.0 @@ -183,6 +185,10 @@ callbacks to execute _before_ running the next one. advanceUntilModeChanges(); }; + setTimeout[IsMockClockTimingFn] = true; + clearTimeout[IsMockClockTimingFn] = true; + setInterval[IsMockClockTimingFn] = true; + clearInterval[IsMockClockTimingFn] = true; return this; // Advances the Clock's time until the mode changes. @@ -335,5 +341,6 @@ callbacks to execute _before_ running the next one. return this; }; + Clock.IsMockClockTimingFn = IsMockClockTimingFn; return Clock; }; diff --git a/src/core/SpyRegistry.js b/src/core/SpyRegistry.js index 7042388b..b4a246da 100644 --- a/src/core/SpyRegistry.js +++ b/src/core/SpyRegistry.js @@ -41,6 +41,12 @@ getJasmineRequireObj().SpyRegistry = function(j$) { throw new Error(getErrorMsg(methodName + '() method does not exist')); } + // Spying on mock clock timing fns would prevent the real ones from being + // restored. + if (obj[methodName] && obj[methodName][j$.Clock.IsMockClockTimingFn]) { + throw new Error("Mock clock timing functions can't be spied on"); + } + if (obj[methodName] && j$.isSpy(obj[methodName])) { if (this.respy) { return obj[methodName];