Repository: sinonjs/lolex
Pull request #50 (2016-04-06 11:25:03)

Run to last existing timer
Add the ability to run to the last currently scheduled timer.

https://github.com/sinonjs/lolex/issues/44

This is useful when you want to run a test to completion, but the test recursively sets timers that would cause `runAll` to trigger an infinite loop warning.


Patch:
diff --git a/Readme.md b/Readme.md
index ab2e334..312a050 100644
--- a/Readme.md
+++ b/Readme.md
@@ -51,8 +51,8 @@ Upon executing the last line, an interesting fact about the
 the screen. If you want to simulate asynchronous behavior, you have to use your
 imagination when calling the various functions.
 
-The `next` method is available to advance to the next scheduled timer. See the
-API Reference for more.
+The `next`, `runAll`, and `runToLast` methods are available to advance the clock. See the
+API Reference for more details.
 
 ### Faking the native timers
 
@@ -191,6 +191,17 @@ This makes it easier to run asynchronous tests to completion without worrying ab
 
 It runs a maximum of `loopLimit` times after which it assumes there is an infinite loop of timers and throws an error.
 
+### `clock.runToLast()`
+
+This takes note of the last scheduled timer when it is run, and advances the
+clock to that time firing callbacks as necessary.
+
+If new timers are added while it is executing they will be run only if they
+would occur before this time.
+
+This is useful when you want to run a test to completion, but the test recursively
+sets timers that would cause `runAll` to trigger an infinite loop warning.
+
 ### `clock.setSystemTime([now])`
 
 This simulates a user changing the system clock while your program is running.
diff --git a/src/lolex-src.js b/src/lolex-src.js
index 4fe7b54..63a6bf8 100644
--- a/src/lolex-src.js
+++ b/src/lolex-src.js
@@ -263,6 +263,22 @@
         return timer;
     }
 
+    function lastTimer(clock) {
+        var timers = clock.timers,
+            timer = null,
+            id;
+
+        for (id in timers) {
+            if (timers.hasOwnProperty(id)) {
+                if (!timer || compareTimers(timer, timers[id]) === -1) {
+                    timer = timers[id];
+                }
+            }
+        }
+
+        return timer;
+    }
+
     function callTimer(clock, timer) {
         var exception;
 
@@ -534,6 +550,15 @@
             throw new Error('Aborting after running ' + clock.loopLimit + 'timers, assuming an infinite loop!');
         };
 
+        clock.runToLast = function runToLast() {
+            var timer = lastTimer(clock);
+            if (!timer) {
+                return clock.now;
+            }
+
+            return clock.tick(timer.callAt);
+        };
+
         clock.reset = function reset() {
             clock.timers = {};
         };


Test patch:
diff --git a/test/lolex-test.js b/test/lolex-test.js
index f5c7af3..8a71fd6 100644
--- a/test/lolex-test.js
+++ b/test/lolex-test.js
@@ -873,6 +873,110 @@ describe("lolex", function () {
 
     });
 
+    describe('runToLast', function() {
+
+        it('returns current time when there are no timers', function() {
+            this.clock = lolex.createClock();
+
+            var time = this.clock.runToLast();
+
+            assert.equals(time, 0);
+        });
+
+        it('runs all existing timers', function() {
+            this.clock = lolex.createClock();
+            var spies = [sinon.spy(), sinon.spy()];
+            this.clock.setTimeout(spies[0], 10);
+            this.clock.setTimeout(spies[1], 50);
+
+            this.clock.runToLast();
+
+            assert(spies[0].called);
+            assert(spies[1].called);
+        });
+
+        it('returns time of the last timer', function() {
+            this.clock = lolex.createClock();
+            var spies = [sinon.spy(), sinon.spy()];
+            this.clock.setTimeout(spies[0], 10);
+            this.clock.setTimeout(spies[1], 50);
+
+            var time = this.clock.runToLast();
+
+            assert.equals(time, 50);
+        });
+
+        it('runs all existing timers when two timers are matched for being last', function() {
+            this.clock = lolex.createClock();
+            var spies = [sinon.spy(), sinon.spy()];
+            this.clock.setTimeout(spies[0], 10);
+            this.clock.setTimeout(spies[1], 10);
+
+            this.clock.runToLast();
+
+            assert(spies[0].called);
+            assert(spies[1].called);
+        });
+
+        it('new timers added with a call time later than the last existing timer are NOT run', function() {
+            this.clock = lolex.createClock();
+            var test = this;
+            var spies = [
+                sinon.spy(function() {
+                    test.clock.setTimeout(spies[1], 50);
+                }),
+                sinon.spy()
+            ];
+
+            // Spy calls another setTimeout
+            this.clock.setTimeout(spies[0], 10);
+
+            this.clock.runToLast();
+
+            assert.isTrue(spies[0].called);
+            assert.isFalse(spies[1].called);
+        });
+
+        it('new timers added with a call time ealier than the last existing timer are run', function() {
+            this.clock = lolex.createClock();
+            var test = this;
+            var spies = [
+                sinon.spy(),
+                sinon.spy(function() {
+                    test.clock.setTimeout(spies[2], 50);
+                }),
+                sinon.spy()
+            ];
+
+            this.clock.setTimeout(spies[0], 100);
+            // Spy calls another setTimeout
+            this.clock.setTimeout(spies[1], 10);
+
+            this.clock.runToLast();
+
+            assert.isTrue(spies[0].called);
+            assert.isTrue(spies[1].called);
+            assert.isTrue(spies[2].called);
+        });
+
+        it('new timers cannot cause an infinite loop', function() {
+            this.clock = lolex.createClock();
+            var test = this;
+            var spy = sinon.spy();
+            var recursiveCallback = function() {
+                test.clock.setTimeout(recursiveCallback, 0);
+            };
+
+            this.clock.setTimeout(recursiveCallback, 0);
+            this.clock.setTimeout(spy, 100);
+
+            this.clock.runToLast();
+
+            assert.isTrue(spy.called);
+        });
+
+    });
+
     describe("clearTimeout", function () {
 
         beforeEach(function () {