diff --git a/README.md b/README.md index 3cdc8868..d78cfc04 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 918f1b59..45114c19 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 63091983..d52ab7ab 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 = {}; diff --git a/test/lolex-test.js b/test/lolex-test.js index 7222c6a3..34d4c6c3 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();