diff --git a/lib/test.js b/lib/test.js index 0cb9aea8b..1bffacf94 100644 --- a/lib/test.js +++ b/lib/test.js @@ -25,11 +25,18 @@ function Test(title, fn) { this.title = title || fnName(fn) || '[anonymous]'; this.fn = isGeneratorFn(fn) ? co.wrap(fn) : fn; - this.assertCount = 0; + this.assertions = []; this.planCount = null; this.duration = null; this.assertError = undefined; + Object.defineProperty(this, 'assertCount', { + enumerable: true, + get: function () { + return this.assertions.length; + } + }); + // TODO(jamestalmage): make this an optional constructor arg instead of having Runner set it after the fact. // metadata should just always exist, otherwise it requires a bunch of ugly checks all over the place. this.metadata = {}; @@ -46,8 +53,8 @@ function Test(title, fn) { module.exports = Test; -Test.prototype._assert = function () { - this.assertCount++; +Test.prototype._assert = function (promise) { + this.assertions.push(promise); }; Test.prototype._setAssertError = function (err) { @@ -162,18 +169,10 @@ Test.prototype._end = function (err) { this.exit(); }; -Test.prototype.exit = function () { - var self = this; - - // calculate total time spent in test - this.duration = globals.now() - this._timeStart; - - // stop infinite timer - globals.clearTimeout(this._timeout); - - if (this.assertError === undefined && this.planCount !== null && this.planCount !== this.assertCount) { +Test.prototype._checkPlanCount = function () { + if (this.assertError === undefined && this.planCount !== null && this.planCount !== this.assertions.length) { this._setAssertError(new assert.AssertionError({ - actual: this.assertCount, + actual: this.assertions.length, expected: this.planCount, message: 'Assertion count does not match planned', operator: 'plan' @@ -181,19 +180,39 @@ Test.prototype.exit = function () { this.assertError.stack = this.planStack; } +}; - if (!this.ended) { - this.ended = true; +Test.prototype.exit = function () { + var self = this; - globals.setImmediate(function () { - if (self.assertError !== undefined) { - self.promise.reject(self.assertError); - return; - } + this._checkPlanCount(); + + Promise.all(this.assertions) + .catch(function (err) { + self._setAssertError(err); + }) + .finally(function () { + // calculate total time spent in test + self.duration = globals.now() - self._timeStart; - self.promise.resolve(self); + // stop infinite timer + globals.clearTimeout(self._timeout); + + self._checkPlanCount(); + + if (!self.ended) { + self.ended = true; + + globals.setImmediate(function () { + if (self.assertError !== undefined) { + self.promise.reject(self.assertError); + return; + } + + self.promise.resolve(self); + }); + } }); - } }; Test.prototype._publicApi = function () { @@ -230,33 +249,23 @@ Test.prototype._publicApi = function () { api.plan = this.plan.bind(this); function skipFn() { - self._assert(); + self._assert(Promise.resolve()); } function onAssertionEvent(event) { + var promise; if (event.assertionThrew) { event.error.powerAssertContext = event.powerAssertContext; - event.error.originalMessage = event.originalMessage; - self._setAssertError(event.error); - self._assert(); - return null; + promise = Promise.reject(event.error); + } else { + promise = Promise.resolve(observableToPromise(event.returnValue)); } - - var fn = observableToPromise(event.returnValue); - - if (isPromise(fn)) { - return Promise.resolve(fn) - .catch(function (err) { - err.originalMessage = event.originalMessage; - self._setAssertError(err); - }) - .finally(function () { - self._assert(); - }); - } - - self._assert(); - return null; + promise = promise + .catch(function (err) { + err.originalMessage = event.originalMessage; + return Promise.reject(err); + }); + self._assert(promise); } var enhanced = enhanceAssert({ diff --git a/package.json b/package.json index 63e405971..9a558d617 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,7 @@ }, "devDependencies": { "coveralls": "^2.11.4", + "delay": "^1.3.0", "signal-exit": "^2.1.2", "sinon": "^1.17.2", "source-map-fixtures": "^0.4.0", diff --git a/test/test.js b/test/test.js index 51e75e866..fb0ca9d1f 100644 --- a/test/test.js +++ b/test/test.js @@ -1,5 +1,7 @@ 'use strict'; var test = require('tap').test; +var Promise = global.Promise = require('bluebird'); +var delay = require('delay'); var _ava = require('../lib/test'); function ava() { @@ -351,3 +353,119 @@ test('skipped assertions count towards the plan', function (t) { t.end(); }); }); + +test('throws and doesNotThrow work with promises', function (t) { + var asyncCalled = false; + ava(function (a) { + a.plan(2); + a.throws(delay.reject(10, new Error('foo')), 'foo'); + a.doesNotThrow(delay(20).then(function () { + asyncCalled = true; + })); + }).run().then(function (a) { + t.ifError(a.assertError); + t.is(a.planCount, 2); + t.is(a.assertCount, 2); + t.is(asyncCalled, true); + t.end(); + }); +}); + +test('waits for t.throws to resolve after t.end is called', function (t) { + ava.cb(function (a) { + a.plan(1); + a.doesNotThrow(delay(10), 'foo'); + a.end(); + }).run().then(function (a) { + t.ifError(a.assertError); + t.is(a.planCount, 1); + t.is(a.assertCount, 1); + t.end(); + }); +}); + +test('waits for t.throws to reject after t.end is called', function (t) { + ava.cb(function (a) { + a.plan(1); + a.throws(delay.reject(10, new Error('foo')), 'foo'); + a.end(); + }).run().then(function (a) { + t.ifError(a.assertError); + t.is(a.planCount, 1); + t.is(a.assertCount, 1); + t.end(); + }); +}); + +test('waits for t.throws to resolve after the promise returned from the test resolves', function (t) { + ava(function (a) { + a.plan(1); + a.doesNotThrow(delay(10), 'foo'); + return Promise.resolve(); + }).run().then(function (a) { + t.ifError(a.assertError); + t.is(a.planCount, 1); + t.is(a.assertCount, 1); + t.end(); + }); +}); + +test('waits for t.throws to reject after the promise returned from the test resolves', function (t) { + ava(function (a) { + a.plan(1); + a.throws(delay.reject(10, new Error('foo')), 'foo'); + return Promise.resolve(); + }).run().then(function (a) { + t.ifError(a.assertError); + t.is(a.planCount, 1); + t.is(a.assertCount, 1); + t.end(); + }); +}); + +test('multiple resolving and rejecting promises passed to t.throws/t.doesNotThrow', function (t) { + ava(function (a) { + a.plan(6); + for (var i = 0; i < 3; ++i) { + a.throws(delay.reject(10, new Error('foo')), 'foo'); + a.doesNotThrow(delay(10), 'foo'); + } + }).run().then(function (a) { + t.ifError(a.assertError); + t.is(a.planCount, 6); + t.is(a.assertCount, 6); + t.end(); + }); +}); + +test('number of assertions matches t.plan when the test exits, but before all promises resolve another is added', function (t) { + ava(function (a) { + a.plan(2); + a.throws(delay.reject(10, new Error('foo')), 'foo'); + a.doesNotThrow(delay(10), 'foo'); + setTimeout(function () { + a.throws(Promise.reject(new Error('foo')), 'foo'); + }, 5); + }).run().catch(function (err) { + t.is(err.operator, 'plan'); + t.is(err.actual, 3); + t.is(err.expected, 2); + t.end(); + }); +}); + +test('number of assertions doesn\'t match plan when the test exits, but before all promises resolve another is added', function (t) { + ava(function (a) { + a.plan(3); + a.throws(delay.reject(10, new Error('foo')), 'foo'); + a.doesNotThrow(delay(10), 'foo'); + setTimeout(function () { + a.throws(Promise.reject(new Error('foo')), 'foo'); + }, 5); + }).run().catch(function (err) { + t.is(err.operator, 'plan'); + t.is(err.actual, 2); + t.is(err.expected, 3); + t.end(); + }); +});