Repository: sinonjs/lolex
Pull request #237 (2019-04-19 13:52:57)

Upgrade lolex with async versions of all timer-executing calls (2019)
This is a rebased and updated version of #105.

I still have to do a full review and test in our code base, probably might need to update some documentation, but just wanted to open this up. I probably also need to re-read all the threads on this... But hopefully this is now closer to getting merged than it was before :)


quasicomputational: I've just tried this out, dropping this branch in instead of some local code that implemented async timers, and I hit some snags along the way - but, once I overcame them, I can report it worked fine. I do have one problem to report. Specifically, sequential calls to `runAllAsync` can fail to be idempotent, which is quite contrary to my expectations - since, if it runs the queue to exhaustion, how can a second call find any more timers to run?

If I don't flush the timer queue at the end of every test, my tests stall and time out. OK, so I need to write my tests like this:

```js
const itAsync = (name, fn) => {
    it(name, async () => {
        fn();
        await clock.runAllAsync();
        // D
    });
};
itAsync("flarbs the blah", async () => {
    // A
    await clock.runAllAsync();
    // B
    await clock.runAllAsync();
    // C
});
```

Unfortunately, concurrent `runAllAsync` didn't behave like I wanted it to. The order of execution I observed was ABDC, as opposed to the hoped-for ABCD. This is quite problematic for me, since then the code at C runs after Jest considers the test to be over and done with.

If, however, `itAsync` is written like this:

```js
const itAsync = (name, fn) => {
    it(name, async () => {
        let running = true;
        Promise.resolve(fn()).finally(() => {
            running = false;
        });
        while (running) {
            await clock.runAllAsync();
        }
    });
};
```

Then it works, and the code at C gets run before the end of the test. But this is clearly demonstrating that `runAllAsync` isn't idempontent. I think it'd be better if, even with multiple concurrent invocations, `runAllAsync` was guaranteed to be idempotent.
quasicomputational: Actually, upon reflection, concurrent `runAllAsync` can't guarantee idempotency, because the code run after any one of them completes could schedule further tasks. So I think that what this warrants is a big scary warning in the docs, saying that this is not something you can rely on despite what a naive mental model might suggest.
benjamingr: I don't think this will work with async/await. I think the way to make this work is to use the devtools protocol.

I am not opposed to landing a stop-gap though.
benjamingr: I will also ping my employer and ask for a full day of work on lolex to close all the stuff I need. Sorry! I was hoping to do this in May but then all the conferences happened.
dominykas: Rebased this on top of `master` and tested it out on an internal project - works like a charm.

Will try to add some docs to this.
dominykas: @quasicomputational in your example, if you add an `await fn()` (line 3) - does it work correctly?
dominykas: I didn't perform as thorough a review as I would at my day job, but @kylefleming's #105 looks very well thought out, so I hope we can merge this :)

I know there's comments about different approaches on how this could have been implemented differently, but I don't think the required functionality is present in node nor in the browsers, and this hasn't changed in the last 2 years, so quite possibly this is as good as it will ever get...
fatso83: @dominykas Can you add some docs that explain the limits of the `runAllAsync` feature? "Using it in simple examples like A will work, but B needs this (*) workaround", where we document @quasicomputational 's workaround and a link to https://github.com/sinonjs/lolex/pull/237#issuecomment-498825901 for what a proper fix entails?

I'm inline with you on this with regards to having a stopgap solution that works in 80% of the cases, while waiting for The Perfect Fix :tm:.

As an alternative to trying to fix bigger examples into the API docs, I could add a Lolex how-to for doing this on the SinonJS how-to page, but I'd still need a somewhat useful template. To not stop this from expanding, I could split that into a separate issue.
quasicomputational: FWIW I think that this PR is as good as can be done, and that there's not even a theoretical 'better fix' that merging should block on. The bug is in user expectations, not library code.
voxpelli: Wanted to chime in with a 👍 on this. I'm using a quick wrapper (built by my colleague @digli) for this functionality in some current projects and it has shown a very real value.

The wrapper:

```javascript
const tick = async (clock, ms) => { clock.tick(ms); };
// ...
await tick(clock, 300);
```
dominykas: @fatso83 sure, I'll see what I can do.
mroderick: @fatso83 go ahead and merge this
dominykas: I ran `eslint . --fix` after resolving the conflict... best viewed with whitespace ignore: https://github.com/sinonjs/lolex/pull/237/commits/ccbefa23c99261cdc1eadc95f4269c26836626aa
dominykas: 🎉 ✅

mroderick: I think this needs a rebase
dominykas: > I think this needs a rebase

Is that to keep the commit history clean?

I can do that tomorrow, I suppose.

mroderick: > Is that to keep the commit history clean?
> 
> I can do that tomorrow, I suppose.

![2019-10-10 at 15 48](https://user-images.githubusercontent.com/20321/66579877-69c91e80-eb75-11e9-96d3-2f379b3a32a1.png)

fatso83: It needs a little bit more work, @dominykas, as I just ran the `npm run test-cloud` command and it failed in Firefox:

```
 285 passing (44s)
  43 pending
  3 failing

  1) lolex
       tickAsync
         passes 2 hours, 34 minutes and 10 seconds:
     Timeout of 10000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.
  require<[187]</<[32]</</Runnable.prototype._timeoutError@about:blank line 2 > scriptElement:46839:10
  require<[187]</<[32]</</Runnable.prototype.resetTimeout/this.timer<@about:blank line 2 > scriptElement:46650:24
  

  2) lolex
       runAllAsync
         throws before allowing infinite recursion:
     Timeout of 10000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.
  require<[187]</<[32]</</Runnable.prototype._timeoutError@about:blank line 2 > scriptElement:46839:10
  require<[187]</<[32]</</Runnable.prototype.resetTimeout/this.timer<@about:blank line 2 > scriptElement:46650:24
  

  3) lolex
       runAllAsync
         throws before allowing infinite recursion from promises:
     Timeout of 10000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.
  require<[187]</<[32]</</Runnable.prototype._timeoutError@about:blank line 2 > scriptElement:46839:10
  require<[187]</<[32]</</Runnable.prototype.resetTimeout/this.timer<@about:blank line 2 > scriptElement:46650:24
``` 

Are you able to modify and run the test in Firefox yourself? We can provide you with sauce labs credentials (privately in a dm; my email is public).

BTW, I rebased and pushed to a branch of my own: https://github.com/fatso83/lolex/tree/async-upgrade-2019
dominykas: Cheers, I'll take a look at the Firefox issue.

Patch:
diff --git a/README.md b/README.md
index 3cdc886..d78cfc0 100644
--- a/README.md
+++ b/README.md
@@ -224,24 +224,30 @@ Only available in Node.js, mimics `process.nextTick` to enable completely synchr
 Only available in browser environments, mimicks performance.now().
 
 
-### `clock.tick(time)`
+### `clock.tick(time)` / `await clock.tickAsync(time)`
 
 Advance the clock, firing callbacks if necessary. `time` may be the number of
 milliseconds to advance the clock by or a human-readable string. Valid string
 formats are `"08"` for eight seconds, `"01:00"` for one minute and `"02:34:10"`
 for two hours, 34 minutes and ten seconds.
 
-### `clock.next()`
+The `tickAsync()` will also break the event loop, allowing any scheduled promise 
+callbacks to execute _before_ running the timers.
+
+### `clock.next()` / `await clock.nextAsync()`
 
 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.reset()`
 
 Removes all timers and ticks without firing them, and sets `now` to `config.now`
 that was provided to `lolex.install` or to `0` if `config.now` was not provided.
 Useful to reset the state of the clock without having to `uninstall` and `install` it.
 
-### `clock.runAll()`
+### `clock.runAll()` / `await clock.runAllAsync()`
 
 This runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well.
 
@@ -249,6 +255,9 @@ 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.
 
+The `runAllAsync()` will also break the event loop, allowing any scheduled promise 
+callbacks to execute _before_ running the timers.
+
 ### `clock.runMicrotasks()`
 
 This runs all pending microtasks scheduled with `nextTick` but none of the timers and is mostly useful for libraries using lolex underneath and for running `nextTick` items without any timers.
@@ -258,7 +267,7 @@ This runs all pending microtasks scheduled with `nextTick` but none of the timer
 Advances the clock to the next frame, firing all scheduled animation frame callbacks,
 if any, for that frame as well as any other timers scheduled along the way.
 
-### `clock.runToLast()`
+### `clock.runToLast()` / `await clock.runToLastAsync()`
 
 This takes note of the last scheduled timer when it is run, and advances the
 clock to that time firing callbacks as necessary.
@@ -269,6 +278,9 @@ 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.
 
+The `runToLastAsync()` will also break the event loop, allowing any scheduled promise 
+callbacks to execute _before_ running the timers.
+
 ### `clock.setSystemTime([now])`
 
 This simulates a user changing the system clock while your program is running.
diff --git a/package.json b/package.json
index 918f1b5..45114c1 100644
--- a/package.json
+++ b/package.json
@@ -16,8 +16,8 @@
   "scripts": {
     "lint": "eslint .",
     "test-node": "mocha test/ integration-test/ -R dot --check-leaks",
-    "test-headless": "mochify --no-detect-globals",
-    "test-cloud": "mochify --wd --no-detect-globals",
+    "test-headless": "mochify --no-detect-globals --timeout=10000",
+    "test-cloud": "mochify --wd --no-detect-globals --timeout=10000",
     "test": "npm run lint && npm run test-node && npm run test-headless",
     "bundle": "browserify --no-detect-globals -s lolex -o lolex.js src/lolex-src.js",
     "prepublishOnly": "npm run bundle",
diff --git a/src/lolex-src.js b/src/lolex-src.js
index 6309198..d52ab7a 100644
--- a/src/lolex-src.js
+++ b/src/lolex-src.js
@@ -638,6 +638,8 @@ function withGlobal(_global) {
             return ks;
         };
 
+    var originalSetTimeout = _global.setImmediate || _global.setTimeout;
+
     /**
      * @param start {Date|number} the system time - non-integer values are floored
      * @param loopLimit {number}  maximum number of timers that will be run when calling runAll()
@@ -810,10 +812,7 @@ function withGlobal(_global) {
             runJobs(clock);
         };
 
-        /**
-         * @param {tickValue} {String|Number} number of milliseconds or a human-readable value like "01:11:15"
-         */
-        clock.tick = function tick(tickValue) {
+        function doTick(tickValue, isAsync, resolve, reject) {
             var msFloat =
                 typeof tickValue === "number"
                     ? tickValue
@@ -836,7 +835,12 @@ function withGlobal(_global) {
             nanos = nanosTotal;
             var tickFrom = clock.now;
             var previous = clock.now;
-            var timer, firstException, oldNow;
+            var timer,
+                firstException,
+                oldNow,
+                nextPromiseTick,
+                compensationCheck,
+                postTimerCall;
 
             clock.duringTick = true;
 
@@ -849,63 +853,122 @@ function withGlobal(_global) {
                 tickTo += clock.now - oldNow;
             }
 
-            // perform each timer in the requested range
-            timer = firstTimerInRange(clock, tickFrom, tickTo);
-            while (timer && tickFrom <= tickTo) {
-                if (clock.timers[timer.id]) {
-                    tickFrom = timer.callAt;
-                    clock.now = timer.callAt;
-                    oldNow = clock.now;
+            function doTickInner() {
+                // perform each timer in the requested range
+                timer = firstTimerInRange(clock, tickFrom, tickTo);
+                // eslint-disable-next-line no-unmodified-loop-condition
+                while (timer && tickFrom <= tickTo) {
+                    if (clock.timers[timer.id]) {
+                        tickFrom = timer.callAt;
+                        clock.now = timer.callAt;
+                        oldNow = clock.now;
+                        try {
+                            runJobs(clock);
+                            callTimer(clock, timer);
+                        } catch (e) {
+                            firstException = firstException || e;
+                        }
+
+                        if (isAsync) {
+                            // finish up after native setImmediate callback to allow
+                            // all native es6 promises to process their callbacks after
+                            // each timer fires.
+                            originalSetTimeout(nextPromiseTick);
+                            return;
+                        }
+
+                        compensationCheck();
+                    }
+
+                    postTimerCall();
+                }
+
+                // perform process.nextTick()s again
+                oldNow = clock.now;
+                runJobs(clock);
+                if (oldNow !== clock.now) {
+                    // compensate for any setSystemTime() call during process.nextTick() callback
+                    tickFrom += clock.now - oldNow;
+                    tickTo += clock.now - oldNow;
+                }
+                clock.duringTick = false;
+
+                // corner case: during runJobs new timers were scheduled which could be in the range [clock.now, tickTo]
+                timer = firstTimerInRange(clock, tickFrom, tickTo);
+                if (timer) {
                     try {
-                        runJobs(clock);
-                        callTimer(clock, timer);
+                        clock.tick(tickTo - clock.now); // do it all again - for the remainder of the requested range
                     } catch (e) {
                         firstException = firstException || e;
                     }
+                } else {
+                    // no timers remaining in the requested range: move the clock all the way to the end
+                    clock.now = tickTo;
+
+                    // update nanos
+                    nanos = nanosTotal;
+                }
+                if (firstException) {
+                    throw firstException;
+                }
+
+                if (isAsync) {
+                    resolve(clock.now);
+                } else {
+                    return clock.now;
+                }
+            }
 
-                    // compensate for any setSystemTime() call during timer callback
-                    if (oldNow !== clock.now) {
-                        tickFrom += clock.now - oldNow;
-                        tickTo += clock.now - oldNow;
-                        previous += clock.now - oldNow;
+            nextPromiseTick =
+                isAsync &&
+                function() {
+                    try {
+                        compensationCheck();
+                        postTimerCall();
+                        doTickInner();
+                    } catch (e) {
+                        reject(e);
                     }
+                };
+
+            compensationCheck = function() {
+                // compensate for any setSystemTime() call during timer callback
+                if (oldNow !== clock.now) {
+                    tickFrom += clock.now - oldNow;
+                    tickTo += clock.now - oldNow;
+                    previous += clock.now - oldNow;
                 }
+            };
 
+            postTimerCall = function() {
                 timer = firstTimerInRange(clock, previous, tickTo);
                 previous = tickFrom;
-            }
+            };
 
-            // perform process.nextTick()s again
-            oldNow = clock.now;
-            runJobs(clock);
-            if (oldNow !== clock.now) {
-                // compensate for any setSystemTime() call during process.nextTick() callback
-                tickFrom += clock.now - oldNow;
-                tickTo += clock.now - oldNow;
-            }
-            clock.duringTick = false;
-
-            // corner case: during runJobs, new timers were scheduled which could be in the range [clock.now, tickTo]
-            timer = firstTimerInRange(clock, tickFrom, tickTo);
-            if (timer) {
-                try {
-                    clock.tick(tickTo - clock.now); // do it all again - for the remainder of the requested range
-                } catch (e) {
-                    firstException = firstException || e;
-                }
-            } else {
-                // no timers remaining in the requested range: move the clock all the way to the end
-                clock.now = tickTo;
+            return doTickInner();
+        }
 
-                // update nanos
-                nanos = nanosTotal;
-            }
-            if (firstException) {
-                throw firstException;
-            }
-            return clock.now;
+        /**
+         * @param {tickValue} {String|Number} number of milliseconds or a human-readable value like "01:11:15"
+         */
+        clock.tick = function tick(tickValue) {
+            return doTick(tickValue, false);
         };
 
+        if (typeof global.Promise !== "undefined") {
+            clock.tickAsync = function tickAsync(ms) {
+                return new global.Promise(function(resolve, reject) {
+                    originalSetTimeout(function() {
+                        try {
+                            doTick(ms, true, resolve, reject);
+                        } catch (e) {
+                            reject(e);
+                        }
+                    });
+                });
+            };
+        }
+
         clock.next = function next() {
             runJobs(clock);
             var timer = firstTimer(clock);
@@ -924,6 +987,42 @@ function withGlobal(_global) {
             }
         };
 
+        if (typeof global.Promise !== "undefined") {
+            clock.nextAsync = function nextAsync() {
+                return new global.Promise(function(resolve, reject) {
+                    originalSetTimeout(function() {
+                        try {
+                            var timer = firstTimer(clock);
+                            if (!timer) {
+                                resolve(clock.now);
+                                return;
+                            }
+
+                            var err;
+                            clock.duringTick = true;
+                            clock.now = timer.callAt;
+                            try {
+                                callTimer(clock, timer);
+                            } catch (e) {
+                                err = e;
+                            }
+                            clock.duringTick = false;
+
+                            originalSetTimeout(function() {
+                                if (err) {
+                                    reject(err);
+                                } else {
+                                    resolve(clock.now);
+                                }
+                            });
+                        } catch (e) {
+                            reject(e);
+                        }
+                    });
+                });
+            };
+        }
+
         clock.runAll = function runAll() {
             var numTimers, i;
             runJobs(clock);
@@ -951,6 +1050,52 @@ function withGlobal(_global) {
             return clock.tick(getTimeToNextFrame());
         };
 
+        if (typeof global.Promise !== "undefined") {
+            clock.runAllAsync = function runAllAsync() {
+                return new global.Promise(function(resolve, reject) {
+                    var i = 0;
+                    function doRun() {
+                        originalSetTimeout(function() {
+                            try {
+                                var numTimers;
+                                if (i < clock.loopLimit) {
+                                    if (!clock.timers) {
+                                        resolve(clock.now);
+                                        return;
+                                    }
+
+                                    numTimers = Object.keys(clock.timers)
+                                        .length;
+                                    if (numTimers === 0) {
+                                        resolve(clock.now);
+                                        return;
+                                    }
+
+                                    clock.next();
+
+                                    i++;
+
+                                    doRun();
+                                    return;
+                                }
+
+                                reject(
+                                    new Error(
+                                        "Aborting after running " +
+                                            clock.loopLimit +
+                                            " timers, assuming an infinite loop!"
+                                    )
+                                );
+                            } catch (e) {
+                                reject(e);
+                            }
+                        });
+                    }
+                    doRun();
+                });
+            };
+        }
+
         clock.runToLast = function runToLast() {
             var timer = lastTimer(clock);
             if (!timer) {
@@ -961,6 +1106,25 @@ function withGlobal(_global) {
             return clock.tick(timer.callAt - clock.now);
         };
 
+        if (typeof global.Promise !== "undefined") {
+            clock.runToLastAsync = function runToLastAsync() {
+                return new global.Promise(function(resolve, reject) {
+                    originalSetTimeout(function() {
+                        try {
+                            var timer = lastTimer(clock);
+                            if (!timer) {
+                                resolve(clock.now);
+                            }
+
+                            resolve(clock.tickAsync(timer.callAt));
+                        } catch (e) {
+                            reject(e);
+                        }
+                    });
+                });
+            };
+        }
+
         clock.reset = function reset() {
             nanos = 0;
             clock.timers = {};


Test patch:
diff --git a/test/lolex-test.js b/test/lolex-test.js
index 7222c6a..34d4c6c 100644
--- a/test/lolex-test.js
+++ b/test/lolex-test.js
@@ -1026,6 +1026,697 @@ describe("lolex", function() {
         });
     });
 
+    if (typeof global.Promise !== "undefined") {
+        describe("tickAsync", function() {
+            beforeEach(function() {
+                this.clock = lolex.install();
+            });
+
+            afterEach(function() {
+                this.clock.uninstall();
+            });
+
+            it("triggers immediately without specified delay", function() {
+                var stub = sinon.stub();
+                this.clock.setTimeout(stub);
+
+                return this.clock.tickAsync(0).then(function() {
+                    assert(stub.called);
+                });
+            });
+
+            it("does not trigger without sufficient delay", function() {
+                var stub = sinon.stub();
+                this.clock.setTimeout(stub, 100);
+
+                return this.clock.tickAsync(10).then(function() {
+                    assert.isFalse(stub.called);
+                });
+            });
+
+            it("triggers after sufficient delay", function() {
+                var stub = sinon.stub();
+                this.clock.setTimeout(stub, 100);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert(stub.called);
+                });
+            });
+
+            it("triggers simultaneous timers", function() {
+                var spies = [sinon.spy(), sinon.spy()];
+                this.clock.setTimeout(spies[0], 100);
+                this.clock.setTimeout(spies[1], 100);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert(spies[0].called);
+                    assert(spies[1].called);
+                });
+            });
+
+            it("triggers multiple simultaneous timers", function() {
+                var spies = [
+                    sinon.spy(),
+                    sinon.spy(),
+                    sinon.spy(),
+                    sinon.spy()
+                ];
+                this.clock.setTimeout(spies[0], 100);
+                this.clock.setTimeout(spies[1], 100);
+                this.clock.setTimeout(spies[2], 99);
+                this.clock.setTimeout(spies[3], 100);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert(spies[0].called);
+                    assert(spies[1].called);
+                    assert(spies[2].called);
+                    assert(spies[3].called);
+                });
+            });
+
+            it("triggers multiple simultaneous timers with zero callAt", function() {
+                var test = this;
+                var spies = [
+                    sinon.spy(function() {
+                        test.clock.setTimeout(spies[1], 0);
+                    }),
+                    sinon.spy(),
+                    sinon.spy()
+                ];
+
+                // First spy calls another setTimeout with delay=0
+                this.clock.setTimeout(spies[0], 0);
+                this.clock.setTimeout(spies[2], 10);
+
+                return this.clock.tickAsync(10).then(function() {
+                    assert(spies[0].called);
+                    assert(spies[1].called);
+                    assert(spies[2].called);
+                });
+            });
+
+            it("triggers multiple simultaneous timers with zero callAt created in promises", function() {
+                var test = this;
+                var spies = [
+                    sinon.spy(function() {
+                        global.Promise.resolve().then(function() {
+                            test.clock.setTimeout(spies[1], 0);
+                        });
+                    }),
+                    sinon.spy(),
+                    sinon.spy()
+                ];
+
+                // First spy calls another setTimeout with delay=0
+                this.clock.setTimeout(spies[0], 0);
+                this.clock.setTimeout(spies[2], 10);
+
+                return this.clock.tickAsync(10).then(function() {
+                    assert(spies[0].called);
+                    assert(spies[1].called);
+                    assert(spies[2].called);
+                });
+            });
+
+            it("waits after setTimeout was called", function() {
+                var clock = this.clock;
+                var stub = sinon.stub();
+
+                return clock
+                    .tickAsync(100)
+                    .then(function() {
+                        clock.setTimeout(stub, 150);
+                        return clock.tickAsync(50);
+                    })
+                    .then(function() {
+                        assert.isFalse(stub.called);
+                        return clock.tickAsync(100);
+                    })
+                    .then(function() {
+                        assert(stub.called);
+                    });
+            });
+
+            it("mini integration test", function() {
+                var clock = this.clock;
+                var stubs = [sinon.stub(), sinon.stub(), sinon.stub()];
+                clock.setTimeout(stubs[0], 100);
+                clock.setTimeout(stubs[1], 120);
+
+                return clock
+                    .tickAsync(10)
+                    .then(function() {
+                        return clock.tickAsync(89);
+                    })
+                    .then(function() {
+                        assert.isFalse(stubs[0].called);
+                        assert.isFalse(stubs[1].called);
+                        clock.setTimeout(stubs[2], 20);
+                        return clock.tickAsync(1);
+                    })
+                    .then(function() {
+                        assert(stubs[0].called);
+                        assert.isFalse(stubs[1].called);
+                        assert.isFalse(stubs[2].called);
+                        return clock.tickAsync(19);
+                    })
+                    .then(function() {
+                        assert.isFalse(stubs[1].called);
+                        assert(stubs[2].called);
+                        return clock.tickAsync(1);
+                    })
+                    .then(function() {
+                        assert(stubs[1].called);
+                    });
+            });
+
+            it("triggers even when some throw", function() {
+                var clock = this.clock;
+                var stubs = [sinon.stub().throws(), sinon.stub()];
+                var catchSpy = sinon.spy();
+
+                clock.setTimeout(stubs[0], 100);
+                clock.setTimeout(stubs[1], 120);
+
+                return clock
+                    .tickAsync(120)
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+                        assert(stubs[0].called);
+                        assert(stubs[1].called);
+                    });
+            });
+
+            it("calls function with global object or null (strict mode) as this", function() {
+                var clock = this.clock;
+                var stub = sinon.stub().throws();
+                var catchSpy = sinon.spy();
+                clock.setTimeout(stub, 100);
+
+                return clock
+                    .tickAsync(100)
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+                        assert(stub.calledOn(global) || stub.calledOn(null));
+                    });
+            });
+
+            it("triggers in the order scheduled", function() {
+                var spies = [sinon.spy(), sinon.spy()];
+                this.clock.setTimeout(spies[0], 13);
+                this.clock.setTimeout(spies[1], 11);
+
+                return this.clock.tickAsync(15).then(function() {
+                    assert(spies[1].calledBefore(spies[0]));
+                });
+            });
+
+            it("creates updated Date while ticking", function() {
+                var spy = sinon.spy();
+
+                this.clock.setInterval(function() {
+                    spy(new Date().getTime());
+                }, 10);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert.equals(spy.callCount, 10);
+                    assert(spy.calledWith(10));
+                    assert(spy.calledWith(20));
+                    assert(spy.calledWith(30));
+                    assert(spy.calledWith(40));
+                    assert(spy.calledWith(50));
+                    assert(spy.calledWith(60));
+                    assert(spy.calledWith(70));
+                    assert(spy.calledWith(80));
+                    assert(spy.calledWith(90));
+                    assert(spy.calledWith(100));
+                });
+            });
+
+            it("creates updated Date while ticking promises", function() {
+                var spy = sinon.spy();
+
+                this.clock.setInterval(function() {
+                    global.Promise.resolve().then(function() {
+                        spy(new Date().getTime());
+                    });
+                }, 10);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert.equals(spy.callCount, 10);
+                    assert(spy.calledWith(10));
+                    assert(spy.calledWith(20));
+                    assert(spy.calledWith(30));
+                    assert(spy.calledWith(40));
+                    assert(spy.calledWith(50));
+                    assert(spy.calledWith(60));
+                    assert(spy.calledWith(70));
+                    assert(spy.calledWith(80));
+                    assert(spy.calledWith(90));
+                    assert(spy.calledWith(100));
+                });
+            });
+
+            it("fires timer in intervals of 13", function() {
+                var spy = sinon.spy();
+                this.clock.setInterval(spy, 13);
+
+                return this.clock.tickAsync(500).then(function() {
+                    assert.equals(spy.callCount, 38);
+                });
+            });
+
+            it("fires timers in correct order", function() {
+                var spy13 = sinon.spy();
+                var spy10 = sinon.spy();
+
+                this.clock.setInterval(function() {
+                    spy13(new Date().getTime());
+                }, 13);
+
+                this.clock.setInterval(function() {
+                    spy10(new Date().getTime());
+                }, 10);
+
+                return this.clock.tickAsync(500).then(function() {
+                    assert.equals(spy13.callCount, 38);
+                    assert.equals(spy10.callCount, 50);
+
+                    assert(spy13.calledWith(416));
+                    assert(spy10.calledWith(320));
+
+                    assert(spy10.getCall(0).calledBefore(spy13.getCall(0)));
+                    assert(spy10.getCall(4).calledBefore(spy13.getCall(3)));
+                });
+            });
+
+            it("fires promise timers in correct order", function() {
+                var spy13 = sinon.spy();
+                var spy10 = sinon.spy();
+
+                this.clock.setInterval(function() {
+                    global.Promise.resolve().then(function() {
+                        spy13(new Date().getTime());
+                    });
+                }, 13);
+
+                this.clock.setInterval(function() {
+                    global.Promise.resolve().then(function() {
+                        spy10(new Date().getTime());
+                    });
+                }, 10);
+
+                return this.clock.tickAsync(500).then(function() {
+                    assert.equals(spy13.callCount, 38);
+                    assert.equals(spy10.callCount, 50);
+
+                    assert(spy13.calledWith(416));
+                    assert(spy10.calledWith(320));
+
+                    assert(spy10.getCall(0).calledBefore(spy13.getCall(0)));
+                    assert(spy10.getCall(4).calledBefore(spy13.getCall(3)));
+                });
+            });
+
+            it("triggers timeouts and intervals in the order scheduled", function() {
+                var spies = [sinon.spy(), sinon.spy()];
+                this.clock.setInterval(spies[0], 10);
+                this.clock.setTimeout(spies[1], 50);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert(spies[0].calledBefore(spies[1]));
+                    assert.equals(spies[0].callCount, 10);
+                    assert.equals(spies[1].callCount, 1);
+                });
+            });
+
+            it("does not fire canceled intervals", function() {
+                var id;
+                var callback = sinon.spy(function() {
+                    if (callback.callCount === 3) {
+                        clearInterval(id);
+                    }
+                });
+
+                id = this.clock.setInterval(callback, 10);
+                return this.clock.tickAsync(100).then(function() {
+                    assert.equals(callback.callCount, 3);
+                });
+            });
+
+            it("does not fire intervals canceled in a promise", function() {
+                var id;
+                var callback = sinon.spy(function() {
+                    if (callback.callCount === 3) {
+                        global.Promise.resolve().then(function() {
+                            clearInterval(id);
+                        });
+                    }
+                });
+
+                id = this.clock.setInterval(callback, 10);
+                return this.clock.tickAsync(100).then(function() {
+                    assert.equals(callback.callCount, 3);
+                });
+            });
+
+            it("passes 8 seconds", function() {
+                var spy = sinon.spy();
+                this.clock.setInterval(spy, 4000);
+
+                return this.clock.tickAsync("08").then(function() {
+                    assert.equals(spy.callCount, 2);
+                });
+            });
+
+            it("passes 1 minute", function() {
+                var spy = sinon.spy();
+                this.clock.setInterval(spy, 6000);
+
+                return this.clock.tickAsync("01:00").then(function() {
+                    assert.equals(spy.callCount, 10);
+                });
+            });
+
+            it("passes 2 hours, 34 minutes and 10 seconds", function() {
+                var spy = sinon.spy();
+                this.clock.setInterval(spy, 100000);
+
+                return this.clock.tickAsync("02:34:10").then(function() {
+                    assert.equals(spy.callCount, 92);
+                });
+            });
+
+            it("throws for invalid format", function() {
+                var spy = sinon.spy();
+                this.clock.setInterval(spy, 10000);
+                var test = this;
+                var catchSpy = sinon.spy();
+
+                return test.clock
+                    .tickAsync("12:02:34:10")
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+                        assert.equals(spy.callCount, 0);
+                    });
+            });
+
+            it("throws for invalid minutes", function() {
+                var spy = sinon.spy();
+                this.clock.setInterval(spy, 10000);
+                var test = this;
+                var catchSpy = sinon.spy();
+
+                return test.clock
+                    .tickAsync("67:10")
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+                        assert.equals(spy.callCount, 0);
+                    });
+            });
+
+            it("throws for negative minutes", function() {
+                var spy = sinon.spy();
+                this.clock.setInterval(spy, 10000);
+                var test = this;
+                var catchSpy = sinon.spy();
+
+                return test.clock
+                    .tickAsync("-7:10")
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+                        assert.equals(spy.callCount, 0);
+                    });
+            });
+
+            it("treats missing argument as 0", function() {
+                var clock = this.clock;
+                return this.clock.tickAsync().then(function() {
+                    assert.equals(clock.now, 0);
+                });
+            });
+
+            it("fires nested setTimeout calls properly", function() {
+                var i = 0;
+                var clock = this.clock;
+
+                var callback = function() {
+                    ++i;
+                    clock.setTimeout(function() {
+                        callback();
+                    }, 100);
+                };
+
+                callback();
+
+                return clock.tickAsync(1000).then(function() {
+                    assert.equals(i, 11);
+                });
+            });
+
+            it("fires nested setTimeout calls in user-created promises properly", function() {
+                var i = 0;
+                var clock = this.clock;
+
+                var callback = function() {
+                    global.Promise.resolve().then(function() {
+                        ++i;
+                        clock.setTimeout(function() {
+                            global.Promise.resolve().then(function() {
+                                callback();
+                            });
+                        }, 100);
+                    });
+                };
+
+                callback();
+
+                return clock.tickAsync(1000).then(function() {
+                    assert.equals(i, 11);
+                });
+            });
+
+            it("does not silently catch errors", function() {
+                var clock = this.clock;
+                var catchSpy = sinon.spy();
+
+                clock.setTimeout(function() {
+                    throw new Error("oh no!");
+                }, 1000);
+
+                return clock
+                    .tickAsync(1000)
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+                    });
+            });
+
+            it("returns the current now value", function() {
+                var clock = this.clock;
+                return clock.tickAsync(200).then(function(value) {
+                    assert.equals(clock.now, value);
+                });
+            });
+
+            it("is not influenced by forward system clock changes", function() {
+                var clock = this.clock;
+                var callback = function() {
+                    clock.setSystemTime(new clock.Date().getTime() + 1000);
+                };
+                var stub = sinon.stub();
+                clock.setTimeout(callback, 1000);
+                clock.setTimeout(stub, 2000);
+                return clock
+                    .tickAsync(1990)
+                    .then(function() {
+                        assert.equals(stub.callCount, 0);
+                        return clock.tickAsync(20);
+                    })
+                    .then(function() {
+                        assert.equals(stub.callCount, 1);
+                    });
+            });
+
+            it("is not influenced by forward system clock changes in promises", function() {
+                var clock = this.clock;
+                var callback = function() {
+                    global.Promise.resolve().then(function() {
+                        clock.setSystemTime(new clock.Date().getTime() + 1000);
+                    });
+                };
+                var stub = sinon.stub();
+                clock.setTimeout(callback, 1000);
+                clock.setTimeout(stub, 2000);
+                return clock
+                    .tickAsync(1990)
+                    .then(function() {
+                        assert.equals(stub.callCount, 0);
+                        return clock.tickAsync(20);
+                    })
+                    .then(function() {
+                        assert.equals(stub.callCount, 1);
+                    });
+            });
+
+            it("is not influenced by forward system clock changes when an error is thrown", function() {
+                var clock = this.clock;
+                var callback = function() {
+                    clock.setSystemTime(new clock.Date().getTime() + 1000);
+                    throw new Error();
+                };
+                var stub = sinon.stub();
+                var catchSpy = sinon.spy();
+                clock.setTimeout(callback, 1000);
+                clock.setTimeout(stub, 2000);
+                return clock
+                    .tickAsync(1990)
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+                        assert.equals(stub.callCount, 0);
+                        return clock.tickAsync(20);
+                    })
+                    .then(function() {
+                        assert.equals(stub.callCount, 1);
+                    });
+            });
+
+            it("should settle user-created promises", function() {
+                var spy = sinon.spy();
+
+                setTimeout(function() {
+                    global.Promise.resolve().then(spy);
+                }, 100);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert(spy.calledOnce);
+                });
+            });
+
+            it("should settle chained user-created promises", function() {
+                var spies = [sinon.spy(), sinon.spy(), sinon.spy()];
+
+                setTimeout(function() {
+                    global.Promise.resolve()
+                        .then(spies[0])
+                        .then(spies[1])
+                        .then(spies[2]);
+                }, 100);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert(spies[0].calledOnce);
+                    assert(spies[1].calledOnce);
+                    assert(spies[2].calledOnce);
+                });
+            });
+
+            it("should settle multiple user-created promises", function() {
+                var spies = [sinon.spy(), sinon.spy(), sinon.spy()];
+
+                setTimeout(function() {
+                    global.Promise.resolve().then(spies[0]);
+                    global.Promise.resolve().then(spies[1]);
+                    global.Promise.resolve().then(spies[2]);
+                }, 100);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert(spies[0].calledOnce);
+                    assert(spies[1].calledOnce);
+                    assert(spies[2].calledOnce);
+                });
+            });
+
+            it("should settle nested user-created promises", function() {
+                var spy = sinon.spy();
+
+                setTimeout(function() {
+                    global.Promise.resolve().then(function() {
+                        global.Promise.resolve().then(function() {
+                            global.Promise.resolve().then(spy);
+                        });
+                    });
+                }, 100);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert(spy.calledOnce);
+                });
+            });
+
+            it("should settle user-created promises even if some throw", function() {
+                var spies = [
+                    sinon.spy(),
+                    sinon.spy(),
+                    sinon.spy(),
+                    sinon.spy()
+                ];
+
+                setTimeout(function() {
+                    global.Promise.reject()
+                        .then(spies[0])
+                        .catch(spies[1]);
+                    global.Promise.resolve()
+                        .then(spies[2])
+                        .catch(spies[3]);
+                }, 100);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert(spies[0].notCalled);
+                    assert(spies[1].calledOnce);
+                    assert(spies[2].calledOnce);
+                    assert(spies[3].notCalled);
+                });
+            });
+
+            it("should settle user-created promises before calling more timeouts", function() {
+                var spies = [sinon.spy(), sinon.spy()];
+
+                setTimeout(function() {
+                    global.Promise.resolve().then(spies[0]);
+                }, 100);
+
+                setTimeout(spies[1], 200);
+
+                return this.clock.tickAsync(200).then(function() {
+                    assert(spies[0].calledBefore(spies[1]));
+                });
+            });
+
+            it("should settle local promises before calling timeouts", function() {
+                var spies = [sinon.spy(), sinon.spy()];
+
+                global.Promise.resolve().then(spies[0]);
+
+                setTimeout(spies[1], 100);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert(spies[0].calledBefore(spies[1]));
+                });
+            });
+
+            it("should settle local nested promises before calling timeouts", function() {
+                var spies = [sinon.spy(), sinon.spy()];
+
+                global.Promise.resolve().then(function() {
+                    global.Promise.resolve().then(function() {
+                        global.Promise.resolve().then(spies[0]);
+                    });
+                });
+
+                setTimeout(spies[1], 100);
+
+                return this.clock.tickAsync(100).then(function() {
+                    assert(spies[0].calledBefore(spies[1]));
+                });
+            });
+        });
+    }
+
     describe("next", function() {
         beforeEach(function() {
             this.clock = lolex.install({ now: 0 });
@@ -1222,6 +1913,381 @@ describe("lolex", function() {
         });
     });
 
+    if (typeof global.Promise !== "undefined") {
+        describe("nextAsync", function() {
+            beforeEach(function() {
+                this.clock = lolex.install();
+            });
+
+            afterEach(function() {
+                this.clock.uninstall();
+            });
+
+            it("triggers the next timer", function() {
+                var stub = sinon.stub();
+                this.clock.setTimeout(stub, 100);
+
+                return this.clock.nextAsync().then(function() {
+                    assert(stub.called);
+                });
+            });
+
+            it("does not trigger simultaneous timers", function() {
+                var spies = [sinon.spy(), sinon.spy()];
+                this.clock.setTimeout(spies[0], 100);
+                this.clock.setTimeout(spies[1], 100);
+
+                return this.clock.nextAsync().then(function() {
+                    assert(spies[0].called);
+                    assert.isFalse(spies[1].called);
+                });
+            });
+
+            it("subsequent calls trigger simultaneous timers", function() {
+                var spies = [
+                    sinon.spy(),
+                    sinon.spy(),
+                    sinon.spy(),
+                    sinon.spy()
+                ];
+                var clock = this.clock;
+                this.clock.setTimeout(spies[0], 100);
+                this.clock.setTimeout(spies[1], 100);
+                this.clock.setTimeout(spies[2], 99);
+                this.clock.setTimeout(spies[3], 100);
+
+                return this.clock
+                    .nextAsync()
+                    .then(function() {
+                        assert(spies[2].called);
+                        assert.isFalse(spies[0].called);
+                        assert.isFalse(spies[1].called);
+                        assert.isFalse(spies[3].called);
+                        return clock.nextAsync();
+                    })
+                    .then(function() {
+                        assert(spies[0].called);
+                        assert.isFalse(spies[1].called);
+                        assert.isFalse(spies[3].called);
+
+                        return clock.nextAsync();
+                    })
+                    .then(function() {
+                        assert(spies[1].called);
+                        assert.isFalse(spies[3].called);
+
+                        return clock.nextAsync();
+                    })
+                    .then(function() {
+                        assert(spies[3].called);
+                    });
+            });
+
+            it("subsequent calls triggers simultaneous timers with zero callAt", function() {
+                var test = this;
+                var spies = [
+                    sinon.spy(function() {
+                        test.clock.setTimeout(spies[1], 0);
+                    }),
+                    sinon.spy(),
+                    sinon.spy()
+                ];
+
+                // First spy calls another setTimeout with delay=0
+                this.clock.setTimeout(spies[0], 0);
+                this.clock.setTimeout(spies[2], 10);
+
+                return this.clock
+                    .nextAsync()
+                    .then(function() {
+                        assert(spies[0].called);
+                        assert.isFalse(spies[1].called);
+
+                        return test.clock.nextAsync();
+                    })
+                    .then(function() {
+                        assert(spies[1].called);
+
+                        return test.clock.nextAsync();
+                    })
+                    .then(function() {
+                        assert(spies[2].called);
+                    });
+            });
+
+            it("subsequent calls in promises triggers simultaneous timers with zero callAt", function() {
+                var test = this;
+                var spies = [
+                    sinon.spy(function() {
+                        global.Promise.resolve().then(function() {
+                            test.clock.setTimeout(spies[1], 0);
+                        });
+                    }),
+                    sinon.spy(),
+                    sinon.spy()
+                ];
+
+                // First spy calls another setTimeout with delay=0
+                this.clock.setTimeout(spies[0], 0);
+                this.clock.setTimeout(spies[2], 10);
+
+                return this.clock
+                    .nextAsync()
+                    .then(function() {
+                        assert(spies[0].called);
+                        assert.isFalse(spies[1].called);
+
+                        return test.clock.nextAsync();
+                    })
+                    .then(function() {
+                        assert(spies[1].called);
+
+                        return test.clock.nextAsync();
+                    })
+                    .then(function() {
+                        assert(spies[2].called);
+                    });
+            });
+
+            it("throws exception thrown by timer", function() {
+                var clock = this.clock;
+                var stub = sinon.stub().throws();
+                var catchSpy = sinon.spy();
+
+                clock.setTimeout(stub, 100);
+
+                return clock
+                    .nextAsync()
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+
+                        assert(stub.called);
+                    });
+            });
+
+            it("calls function with global object or null (strict mode) as this", function() {
+                var clock = this.clock;
+                var stub = sinon.stub().throws();
+                var catchSpy = sinon.spy();
+                clock.setTimeout(stub, 100);
+
+                return clock
+                    .nextAsync()
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+
+                        assert(stub.calledOn(global) || stub.calledOn(null));
+                    });
+            });
+
+            it("subsequent calls trigger in the order scheduled", function() {
+                var clock = this.clock;
+                var spies = [sinon.spy(), sinon.spy()];
+                this.clock.setTimeout(spies[0], 13);
+                this.clock.setTimeout(spies[1], 11);
+
+                return this.clock
+                    .nextAsync()
+                    .then(function() {
+                        return clock.nextAsync();
+                    })
+                    .then(function() {
+                        assert(spies[1].calledBefore(spies[0]));
+                    });
+            });
+
+            it("subsequent calls create updated Date", function() {
+                var clock = this.clock;
+                var spy = sinon.spy();
+
+                this.clock.setInterval(function() {
+                    spy(new Date().getTime());
+                }, 10);
+
+                return this.clock
+                    .nextAsync()
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(function() {
+                        assert.equals(spy.callCount, 10);
+                        assert(spy.calledWith(10));
+                        assert(spy.calledWith(20));
+                        assert(spy.calledWith(30));
+                        assert(spy.calledWith(40));
+                        assert(spy.calledWith(50));
+                        assert(spy.calledWith(60));
+                        assert(spy.calledWith(70));
+                        assert(spy.calledWith(80));
+                        assert(spy.calledWith(90));
+                        assert(spy.calledWith(100));
+                    });
+            });
+
+            it("subsequent calls in promises create updated Date", function() {
+                var clock = this.clock;
+                var spy = sinon.spy();
+
+                this.clock.setInterval(function() {
+                    global.Promise.resolve().then(function() {
+                        spy(new Date().getTime());
+                    });
+                }, 10);
+
+                return this.clock
+                    .nextAsync()
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(function() {
+                        assert.equals(spy.callCount, 10);
+                        assert(spy.calledWith(10));
+                        assert(spy.calledWith(20));
+                        assert(spy.calledWith(30));
+                        assert(spy.calledWith(40));
+                        assert(spy.calledWith(50));
+                        assert(spy.calledWith(60));
+                        assert(spy.calledWith(70));
+                        assert(spy.calledWith(80));
+                        assert(spy.calledWith(90));
+                        assert(spy.calledWith(100));
+                    });
+            });
+
+            it("subsequent calls trigger timeouts and intervals in the order scheduled", function() {
+                var clock = this.clock;
+                var spies = [sinon.spy(), sinon.spy()];
+                this.clock.setInterval(spies[0], 10);
+                this.clock.setTimeout(spies[1], 50);
+
+                return this.clock
+                    .nextAsync()
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(function() {
+                        assert(spies[0].calledBefore(spies[1]));
+                        assert.equals(spies[0].callCount, 5);
+                        assert.equals(spies[1].callCount, 1);
+                    });
+            });
+
+            it("subsequent calls do not fire canceled intervals", function() {
+                var id;
+                var clock = this.clock;
+                var callback = sinon.spy(function() {
+                    if (callback.callCount === 3) {
+                        clearInterval(id);
+                    }
+                });
+
+                id = this.clock.setInterval(callback, 10);
+                return this.clock
+                    .nextAsync()
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(function() {
+                        assert.equals(callback.callCount, 3);
+                    });
+            });
+
+            it("subsequent calls do not fire intervals canceled in promises", function() {
+                var id;
+                var clock = this.clock;
+                var callback = sinon.spy(function() {
+                    if (callback.callCount === 3) {
+                        global.Promise.resolve().then(function() {
+                            clearInterval(id);
+                        });
+                    }
+                });
+
+                id = this.clock.setInterval(callback, 10);
+                return this.clock
+                    .nextAsync()
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(clock.nextAsync)
+                    .then(function() {
+                        assert.equals(callback.callCount, 3);
+                    });
+            });
+
+            it("advances the clock based on when the timer was supposed to be called", function() {
+                var clock = this.clock;
+                clock.setTimeout(sinon.spy(), 55);
+                return clock.nextAsync().then(function() {
+                    assert.equals(clock.now, 55);
+                });
+            });
+
+            it("returns the current now value", function() {
+                var clock = this.clock;
+                clock.setTimeout(sinon.spy(), 55);
+                return clock.nextAsync().then(function(value) {
+                    assert.equals(clock.now, value);
+                });
+            });
+
+            it("should settle user-created promises", function() {
+                var spy = sinon.spy();
+
+                setTimeout(function() {
+                    global.Promise.resolve().then(spy);
+                }, 55);
+
+                return this.clock.nextAsync().then(function() {
+                    assert(spy.calledOnce);
+                });
+            });
+
+            it("should settle nested user-created promises", function() {
+                var spy = sinon.spy();
+
+                setTimeout(function() {
+                    global.Promise.resolve().then(function() {
+                        global.Promise.resolve().then(function() {
+                            global.Promise.resolve().then(spy);
+                        });
+                    });
+                }, 55);
+
+                return this.clock.nextAsync().then(function() {
+                    assert(spy.calledOnce);
+                });
+            });
+
+            it("should settle local promises before firing timers", function() {
+                var spies = [sinon.spy(), sinon.spy()];
+
+                global.Promise.resolve().then(spies[0]);
+
+                setTimeout(spies[1], 55);
+
+                return this.clock.nextAsync().then(function() {
+                    assert(spies[0].calledBefore(spies[1]));
+                });
+            });
+        });
+    }
+
     describe("runAll", function() {
         it("if there are no timers just return", function() {
             this.clock = lolex.createClock();
@@ -1302,6 +2368,199 @@ describe("lolex", function() {
         });
     });
 
+    if (typeof global.Promise !== "undefined") {
+        describe("runAllAsync", function() {
+            it("if there are no timers just return", function() {
+                this.clock = lolex.createClock();
+                return this.clock.runAllAsync();
+            });
+
+            it("runs all timers", function() {
+                this.clock = lolex.createClock();
+                var spies = [sinon.spy(), sinon.spy()];
+                this.clock.setTimeout(spies[0], 10);
+                this.clock.setTimeout(spies[1], 50);
+
+                return this.clock.runAllAsync().then(function() {
+                    assert(spies[0].called);
+                    assert(spies[1].called);
+                });
+            });
+
+            it("new timers added while running are also 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);
+
+                return this.clock.runAllAsync().then(function() {
+                    assert(spies[0].called);
+                    assert(spies[1].called);
+                });
+            });
+
+            it("new timers added in promises while running are also run", function() {
+                this.clock = lolex.createClock();
+                var test = this;
+                var spies = [
+                    sinon.spy(function() {
+                        global.Promise.resolve().then(function() {
+                            test.clock.setTimeout(spies[1], 50);
+                        });
+                    }),
+                    sinon.spy()
+                ];
+
+                // Spy calls another setTimeout
+                this.clock.setTimeout(spies[0], 10);
+
+                return this.clock.runAllAsync().then(function() {
+                    assert(spies[0].called);
+                    assert(spies[1].called);
+                });
+            });
+
+            it("throws before allowing infinite recursion", function() {
+                this.clock = lolex.createClock(0, 100);
+                var test = this;
+                var recursiveCallback = function() {
+                    test.clock.setTimeout(recursiveCallback, 10);
+                };
+                var catchSpy = sinon.spy();
+
+                this.clock.setTimeout(recursiveCallback, 10);
+
+                return test.clock
+                    .runAllAsync()
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+                    });
+            });
+
+            it("throws before allowing infinite recursion from promises", function() {
+                this.clock = lolex.createClock(0, 100);
+                var test = this;
+                var recursiveCallback = function() {
+                    global.Promise.resolve().then(function() {
+                        test.clock.setTimeout(recursiveCallback, 10);
+                    });
+                };
+                var catchSpy = sinon.spy();
+
+                this.clock.setTimeout(recursiveCallback, 10);
+
+                return test.clock
+                    .runAllAsync()
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+                    });
+            });
+
+            it("the loop limit can be set when creating a clock", function() {
+                this.clock = lolex.createClock(0, 1);
+                var test = this;
+                var catchSpy = sinon.spy();
+
+                var spies = [sinon.spy(), sinon.spy()];
+                this.clock.setTimeout(spies[0], 10);
+                this.clock.setTimeout(spies[1], 50);
+
+                return test.clock
+                    .runAllAsync()
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+                    });
+            });
+
+            it("the loop limit can be set when installing a clock", function() {
+                this.clock = lolex.install({ loopLimit: 1 });
+                var test = this;
+                var catchSpy = sinon.spy();
+
+                var spies = [sinon.spy(), sinon.spy()];
+                setTimeout(spies[0], 10);
+                setTimeout(spies[1], 50);
+
+                return test.clock
+                    .runAllAsync()
+                    .catch(catchSpy)
+                    .then(function() {
+                        assert(catchSpy.calledOnce);
+
+                        test.clock.uninstall();
+                    });
+            });
+
+            it("should settle user-created promises", function() {
+                this.clock = lolex.createClock();
+                var spy = sinon.spy();
+
+                this.clock.setTimeout(function() {
+                    global.Promise.resolve().then(spy);
+                }, 55);
+
+                return this.clock.runAllAsync().then(function() {
+                    assert(spy.calledOnce);
+                });
+            });
+
+            it("should settle nested user-created promises", function() {
+                this.clock = lolex.createClock();
+                var spy = sinon.spy();
+
+                this.clock.setTimeout(function() {
+                    global.Promise.resolve().then(function() {
+                        global.Promise.resolve().then(function() {
+                            global.Promise.resolve().then(spy);
+                        });
+                    });
+                }, 55);
+
+                return this.clock.runAllAsync().then(function() {
+                    assert(spy.calledOnce);
+                });
+            });
+
+            it("should settle local promises before firing timers", function() {
+                this.clock = lolex.createClock();
+                var spies = [sinon.spy(), sinon.spy()];
+
+                global.Promise.resolve().then(spies[0]);
+
+                this.clock.setTimeout(spies[1], 55);
+
+                return this.clock.runAllAsync().then(function() {
+                    assert(spies[0].calledBefore(spies[1]));
+                });
+            });
+
+            it("should settle user-created promises before firing more timers", function() {
+                this.clock = lolex.createClock();
+                var spies = [sinon.spy(), sinon.spy()];
+
+                this.clock.setTimeout(function() {
+                    global.Promise.resolve().then(spies[0]);
+                }, 55);
+
+                this.clock.setTimeout(spies[1], 75);
+
+                return this.clock.runAllAsync().then(function() {
+                    assert(spies[0].calledBefore(spies[1]));
+                });
+            });
+        });
+    }
+
     describe("runToLast", function() {
         it("returns current time when there are no timers", function() {
             this.clock = lolex.createClock();
@@ -1419,6 +2678,239 @@ describe("lolex", function() {
         });
     });
 
+    if (typeof global.Promise !== "undefined") {
+        describe("runToLastAsync", function() {
+            it("returns current time when there are no timers", function() {
+                this.clock = lolex.createClock();
+
+                return this.clock.runToLastAsync().then(function(time) {
+                    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);
+
+                return this.clock.runToLastAsync().then(function() {
+                    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);
+
+                return this.clock.runToLastAsync().then(function(time) {
+                    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);
+
+                return this.clock.runToLastAsync().then(function() {
+                    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);
+
+                return this.clock.runToLastAsync().then(function() {
+                    assert.isTrue(spies[0].called);
+                    assert.isFalse(spies[1].called);
+                });
+            });
+
+            it(
+                "new timers added from a promise 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() {
+                            global.Promise.resolve().then(function() {
+                                test.clock.setTimeout(spies[1], 50);
+                            });
+                        }),
+                        sinon.spy()
+                    ];
+
+                    // Spy calls another setTimeout
+                    this.clock.setTimeout(spies[0], 10);
+
+                    return this.clock.runToLastAsync().then(function() {
+                        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);
+
+                return this.clock.runToLastAsync().then(function() {
+                    assert.isTrue(spies[0].called);
+                    assert.isTrue(spies[1].called);
+                    assert.isTrue(spies[2].called);
+                });
+            });
+
+            it(
+                "new timers added from a promise 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() {
+                            global.Promise.resolve().then(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);
+
+                    return this.clock.runToLastAsync().then(function() {
+                        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);
+
+                return this.clock.runToLastAsync().then(function() {
+                    assert.isTrue(spy.called);
+                });
+            });
+
+            it("new timers created from promises cannot cause an infinite loop", function() {
+                this.clock = lolex.createClock();
+                var test = this;
+                var spy = sinon.spy();
+                var recursiveCallback = function() {
+                    global.Promise.resolve().then(function() {
+                        test.clock.setTimeout(recursiveCallback, 0);
+                    });
+                };
+
+                this.clock.setTimeout(recursiveCallback, 0);
+                this.clock.setTimeout(spy, 100);
+
+                return this.clock.runToLastAsync().then(function() {
+                    assert.isTrue(spy.called);
+                });
+            });
+
+            it("should settle user-created promises", function() {
+                this.clock = lolex.createClock();
+                var spy = sinon.spy();
+
+                this.clock.setTimeout(function() {
+                    global.Promise.resolve().then(spy);
+                }, 55);
+
+                return this.clock.runToLastAsync().then(function() {
+                    assert(spy.calledOnce);
+                });
+            });
+
+            it("should settle nested user-created promises", function() {
+                this.clock = lolex.createClock();
+                var spy = sinon.spy();
+
+                this.clock.setTimeout(function() {
+                    global.Promise.resolve().then(function() {
+                        global.Promise.resolve().then(function() {
+                            global.Promise.resolve().then(spy);
+                        });
+                    });
+                }, 55);
+
+                return this.clock.runToLastAsync().then(function() {
+                    assert(spy.calledOnce);
+                });
+            });
+
+            it("should settle local promises before firing timers", function() {
+                this.clock = lolex.createClock();
+                var spies = [sinon.spy(), sinon.spy()];
+
+                global.Promise.resolve().then(spies[0]);
+
+                this.clock.setTimeout(spies[1], 55);
+
+                return this.clock.runToLastAsync().then(function() {
+                    assert(spies[0].calledBefore(spies[1]));
+                });
+            });
+
+            it("should settle user-created promises before firing more timers", function() {
+                this.clock = lolex.createClock();
+                var spies = [sinon.spy(), sinon.spy()];
+
+                this.clock.setTimeout(function() {
+                    global.Promise.resolve().then(spies[0]);
+                }, 55);
+
+                this.clock.setTimeout(spies[1], 75);
+
+                return this.clock.runToLastAsync().then(function() {
+                    assert(spies[0].calledBefore(spies[1]));
+                });
+            });
+        });
+    }
+
     describe("clearTimeout", function() {
         beforeEach(function() {
             this.clock = lolex.createClock();