Repository: sinonjs/fake-timers (js)

Issue:
[Feature] Jump forward in time to simulate throttled timers
> We understand you have a problem and are in a hurry, but please provide us with some info to make it much more likely for your issue to be understood, worked on and resolved quickly.

-   FakeTimers version : 10.0.0
-   Environment : Jest/JSDOM
-   Example URL : N/A
-   Other libraries you are using: N/A

**What did you expect to happen?**

I expected there would be an API to enable skipping forward in time in a similar way that [browsers will throttle a `setInterval` when the page is backgrounded](https://developer.chrome.com/blog/timer-throttling-in-chrome-88/). That is to say, if I have the following code:

```js
setInterval(() => {
    console.log("Here");
}, 100);
```

This would print 10 times a second. So running `clock.tick(2000)` would print this 20 times. I expected that there would be a `clock.jump(2000)` which would print this only once, effectively advancing the clock without any intermediary tasks in the event loop being processed. Another way to look at it is that all pending tasks were throttled to whatever value is passed to the `clock.jump` method.

**What actually happens**

There currently is no way to test code which is written to handle this browser behavior of throttling timers.

**How to reproduce**

N/A


Pull request:
Add clock.jump method
#### Purpose (TL;DR)

Closes #452 by creating the `clock.jump(time)` method enabling skipping forward in time and running any timers at most once.

Patch:
diff --git a/README.md b/README.md
index 22f40d9..049f067 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 4fb35aa..607d91f 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<number>} 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;


Test patch:
diff --git a/test/fake-timers-test.js b/test/fake-timers-test.js
index 2a0d122..5971a31 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) {