diff --git a/README.md b/README.md index 22f40d91..049f0672 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,13 @@ Advances the clock to the the moment of the first scheduled timer, firing it. The `nextAsync()` will also break the event loop, allowing any scheduled promise callbacks to execute _before_ running the timers. +### `clock.jump(time)` + +Advance the clock by jumping forward in time, firing callbacks at most once. +`time` takes the same formats as [`clock.tick`](#clockticktime--await-clocktickasynctime). + +This can be used to simulate the JS engine (such as a browser) being put to sleep and resumed later, skipping intermediary timers. + ### `clock.reset()` Removes all timers and ticks without firing them, and sets `now` to `config.now` diff --git a/src/fake-timers-src.js b/src/fake-timers-src.js index 4fb35aa4..607d91fb 100644 --- a/src/fake-timers-src.js +++ b/src/fake-timers-src.js @@ -79,6 +79,7 @@ const globalObject = require("@sinonjs/commons").global; * @property {function(): Promise} runToLastAsync * @property {function(): void} reset * @property {function(number | Date): void} setSystemTime + * @property {function(number): void} jump * @property {Performance} performance * @property {function(number[]): number[]} hrtime - process.hrtime (legacy) * @property {function(): void} uninstall Uninstall the clock. @@ -1605,6 +1606,25 @@ function withGlobal(_global) { } }; + /** + * @param {string|number} tickValue number of milliseconds or a human-readable value like "01:11:15" + * @returns {number} will return the new `now` value + */ + clock.jump = function jump(tickValue) { + const msFloat = + typeof tickValue === "number" + ? tickValue + : parseTime(tickValue); + const ms = Math.floor(msFloat); + + for (const timer of Object.values(clock.timers)) { + if (clock.now + ms > timer.callAt) { + timer.callAt = clock.now + ms; + } + } + clock.tick(ms); + }; + if (performancePresent) { clock.performance = Object.create(null); clock.performance.now = fakePerformanceNow; diff --git a/test/fake-timers-test.js b/test/fake-timers-test.js index 2a0d122a..5971a31f 100644 --- a/test/fake-timers-test.js +++ b/test/fake-timers-test.js @@ -4103,6 +4103,65 @@ describe("FakeTimers", function () { }); }); + describe("jump", function () { + beforeEach(function () { + this.clock = FakeTimers.install({ now: 0 }); + }); + + afterEach(function () { + this.clock.uninstall(); + }); + + it("ignores timers which wouldn't be run", function () { + const stub = sinon.stub(); + this.clock.setTimeout(stub, 1000); + + this.clock.jump(500); + + assert(stub.notCalled); + }); + + it("pushes back execution time for skipped timers", function () { + const stub = sinon.stub(); + this.clock.setTimeout(() => { + stub(this.clock.Date.now()); + }, 1000); + + this.clock.jump(2000); + + assert(stub.calledOnce); + assert(stub.calledWith(2000)); + }); + + it("handles multiple pending timers and types", function () { + const longTimers = [sinon.stub(), sinon.stub()]; + const shortTimers = [sinon.stub(), sinon.stub(), sinon.stub()]; + this.clock.setTimeout(longTimers[0], 2000); + this.clock.setInterval(longTimers[1], 2500); + this.clock.setTimeout(shortTimers[0], 250); + this.clock.setInterval(shortTimers[1], 100); + this.clock.requestAnimationFrame(shortTimers[2]); + + this.clock.jump(1500); + + for (const stub of longTimers) { + assert(stub.notCalled); + } + for (const stub of shortTimers) { + assert(stub.calledOnce); + } + }); + + it("supports string time arguments", function () { + const stub = sinon.stub(); + this.clock.setTimeout(stub, 100000); // 100000 = 1:40 + + this.clock.jump("01:50"); + + assert(stub.calledOnce); + }); + }); + describe("performance.now()", function () { before(function () { if (!performanceNowPresent) {