diff --git a/index.js.flow b/index.js.flow index d21470695..ff9914606 100644 --- a/index.js.flow +++ b/index.js.flow @@ -55,13 +55,13 @@ type AssertContext = { deepEqual(value: U, expected: U, message?: string): void; // Assert that value is not deep equal to expected. notDeepEqual(value: U, expected: U, message?: string): void; - // Assert that function throws an error or promise rejects. + // Assert that the promise rejects, or the function throws or returns a rejected promise. // @param error Can be a constructor, regex, error message or validation function. throws: { (value: PromiseLike, error?: ErrorValidator, message?: string): Promise; (value: () => mixed, error?: ErrorValidator, message?: string): any; }; - // Assert that function doesn't throw an error or promise resolves. + // Assert that the promise resolves, or the function doesn't throw or return a resolved promise. notThrows: { (value: PromiseLike, message?: string): Promise; (value: () => mixed, message?: string): void; diff --git a/lib/assert.js b/lib/assert.js index fd79e2698..53e83e308 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -198,13 +198,14 @@ function wrapAssertions(callbacks) { coreAssertThrowsErrorArg = err; } + let maybePromise; const test = (fn, stack) => { let actual; let threw = false; try { coreAssert.throws(() => { try { - fn(); + maybePromise = fn(); } catch (err) { actual = err; threw = true; @@ -224,7 +225,7 @@ function wrapAssertions(callbacks) { } }; - if (promise) { + const handlePromise = promise => { // Record stack before it gets lost in the promise chain. const stack = getStack(); const intermediate = promise.then(value => { @@ -238,6 +239,10 @@ function wrapAssertions(callbacks) { pending(this, intermediate); // Don't reject the returned promise, even if the assertion fails. return intermediate.catch(noop); + }; + + if (promise) { + return handlePromise(promise); } try { @@ -245,6 +250,14 @@ function wrapAssertions(callbacks) { pass(this); return retval; } catch (err) { + if (maybePromise) { + if (isPromise(maybePromise)) { + return handlePromise(maybePromise); + } + if (isObservable(maybePromise)) { + return handlePromise(observableToPromise(maybePromise)); + } + } fail(this, err); } }, @@ -265,9 +278,12 @@ function wrapAssertions(callbacks) { return; } + let maybePromise; const test = (fn, stack) => { try { - coreAssert.doesNotThrow(fn); + coreAssert.doesNotThrow(() => { + maybePromise = fn(); + }); } catch (err) { throw new AssertionError({ assertion: 'notThrows', @@ -278,17 +294,29 @@ function wrapAssertions(callbacks) { } }; - if (promise) { + const handlePromise = promise => { // Record stack before it gets lost in the promise chain. const stack = getStack(); const intermediate = promise.then(noop, reason => test(makeRethrow(reason), stack)); pending(this, intermediate); // Don't reject the returned promise, even if the assertion fails. return intermediate.catch(noop); + }; + + if (promise) { + return handlePromise(promise); } try { test(fn); + if (maybePromise) { + if (isPromise(maybePromise)) { + return handlePromise(maybePromise); + } + if (isObservable(maybePromise)) { + return handlePromise(observableToPromise(maybePromise)); + } + } pass(this); } catch (err) { fail(this, err); diff --git a/readme.md b/readme.md index ae3b9df03..98a98bcdc 100644 --- a/readme.md +++ b/readme.md @@ -913,7 +913,7 @@ Assert that `value` is not deeply equal to `expected`. The inverse of `.deepEqua ### `.throws(function|promise, [error, [message]])` -Assert that `function` throws an error, or `promise` rejects with an error. +Assert that `function` throws an error, `promise` rejects with an error, or `function` returns a rejected `promise`. `error` can be an error constructor, error message, regex matched against the error message, or validation function. @@ -952,9 +952,21 @@ test('rejects', async t => { }); ``` +When testing an asynchronous function you must also wait for the assertion to complete: + +```js +test('throws', async t => { + const error = await t.throws(async () => { + throw new TypeError('🦄'); + }, TypeError); + + t.is(error.message, '🦄'); +}); +``` + ### `.notThrows(function|promise, [message])` -Assert that `function` does not throw an error or that `promise` does not reject with an error. +Assert that `function` does not throw an error, `promise` does not reject with an error, or `function` returns a promise that does not reject with an error. Like the `.throws()` assertion, when testing a promise you must wait for the assertion to complete: diff --git a/test/assert.js b/test/assert.js index 513d82092..4edfdcfe0 100644 --- a/test/assert.js +++ b/test/assert.js @@ -6,6 +6,7 @@ const stripAnsi = require('strip-ansi'); const React = require('react'); const renderer = require('react-test-renderer'); const test = require('tap').test; +const Observable = require('zen-observable'); const assert = require('../lib/assert'); const snapshotManager = require('../lib/snapshot-manager'); const Test = require('../lib/test'); @@ -678,6 +679,28 @@ test('.throws() returns the rejection reason of promise', t => { }); }); +test('.throws() returns the rejection reason of a promise returned by the function', t => { + const expected = new Error(); + + return assertions.throws(() => { + return Promise.reject(expected); + }).then(actual => { + t.is(actual, expected); + t.end(); + }); +}); + +test('.throws() returns the error of an observable returned by the function', t => { + const expected = new Error(); + + return assertions.throws(() => { + return new Observable(observer => observer.error(expected)); + }).then(actual => { + t.is(actual, expected); + t.end(); + }); +}); + test('.throws() fails if passed a bad value', t => { failsWith(t, () => { assertions.throws('not a function'); @@ -732,6 +755,22 @@ test('.notThrows() returns undefined for a fulfilled promise', t => { }); }); +test('.notThrows() returns undefined for a fulfilled promise returned by the function', t => { + return assertions.notThrows(() => { + return Promise.resolve(Symbol('')); + }).then(actual => { + t.is(actual, undefined); + }); +}); + +test('.notThrows() returns undefined for an observable returned by the function', t => { + return assertions.notThrows(() => { + return Observable.of(Symbol('')); + }).then(actual => { + t.is(actual, undefined); + }); +}); + test('.notThrows() fails if passed a bad value', t => { failsWith(t, () => { assertions.notThrows('not a function'); diff --git a/test/observable.js b/test/observable.js index 4943de9ee..5027cddd3 100644 --- a/test/observable.js +++ b/test/observable.js @@ -48,7 +48,7 @@ test('returning an observable from a legacy async fn is an error', t => { t.end(); }); -test('handle throws with thrown observable', t => { +test('handle throws with erroring observable', t => { let result; ava(a => { a.plan(1); @@ -67,7 +67,26 @@ test('handle throws with thrown observable', t => { }); }); -test('handle throws with long running thrown observable', t => { +test('handle throws with erroring observable returned by function', t => { + let result; + ava(a => { + a.plan(1); + + const observable = new Observable(observer => { + observer.error(new Error()); + }); + + return a.throws(() => observable); + }, r => { + result = r; + }).run().then(passed => { + t.is(passed, true); + t.is(result.result.assertCount, 1); + t.end(); + }); +}); + +test('handle throws with long running erroring observable', t => { let result; ava(a => { a.plan(1); @@ -104,6 +123,22 @@ test('handle throws with completed observable', t => { }); }); +test('handle throws with completed observable returned by function', t => { + let result; + ava(a => { + a.plan(1); + + const observable = Observable.of(); + return a.throws(() => observable); + }, r => { + result = r; + }).run().then(passed => { + t.is(passed, false); + t.is(result.reason.name, 'AssertionError'); + t.end(); + }); +}); + test('handle throws with regex', t => { let result; ava(a => { @@ -196,3 +231,22 @@ test('handle notThrows with thrown observable', t => { t.end(); }); }); + +test('handle notThrows with erroring observable returned by function', t => { + let result; + ava(a => { + a.plan(1); + + const observable = new Observable(observer => { + observer.error(new Error()); + }); + + return a.notThrows(() => observable); + }, r => { + result = r; + }).run().then(passed => { + t.is(passed, false); + t.is(result.reason.name, 'AssertionError'); + t.end(); + }); +}); diff --git a/test/promise.js b/test/promise.js index 87611e1a7..318132161 100644 --- a/test/promise.js +++ b/test/promise.js @@ -151,6 +151,22 @@ test('handle throws with rejected promise', t => { }); }); +test('handle throws with rejected promise returned by function', t => { + let result; + ava(a => { + a.plan(1); + + const promise = Promise.reject(new Error()); + return a.throws(() => promise); + }, r => { + result = r; + }).run().then(passed => { + t.is(passed, true); + t.is(result.result.assertCount, 1); + t.end(); + }); +}); + // TODO(team): This is a very slow test, and I can't figure out why we need it - James test('handle throws with long running rejected promise', t => { let result; @@ -189,6 +205,22 @@ test('handle throws with resolved promise', t => { }); }); +test('handle throws with resolved promise returned by function', t => { + let result; + ava(a => { + a.plan(1); + + const promise = Promise.resolve(); + return a.throws(() => promise); + }, r => { + result = r; + }).run().then(passed => { + t.is(passed, false); + t.is(result.reason.name, 'AssertionError'); + t.end(); + }); +}); + test('handle throws with regex', t => { let result; ava(a => { @@ -317,6 +349,38 @@ test('handle notThrows with rejected promise', t => { }); }); +test('handle notThrows with resolved promise returned by function', t => { + let result; + ava(a => { + a.plan(1); + + const promise = Promise.resolve(); + return a.notThrows(() => promise); + }, r => { + result = r; + }).run().then(passed => { + t.is(passed, true); + t.is(result.result.assertCount, 1); + t.end(); + }); +}); + +test('handle notThrows with rejected promise returned by function', t => { + let result; + ava(a => { + a.plan(1); + + const promise = Promise.reject(new Error()); + return a.notThrows(() => promise); + }, r => { + result = r; + }).run().then(passed => { + t.is(passed, false); + t.is(result.reason.name, 'AssertionError'); + t.end(); + }); +}); + test('assert pass', t => { let result; ava(a => { diff --git a/types/base.d.ts b/types/base.d.ts index 774db3fa1..f92ab4631 100644 --- a/types/base.d.ts +++ b/types/base.d.ts @@ -67,11 +67,13 @@ export interface AssertContext { * @param error Can be a constructor, regex, error message or validation function. */ throws(value: PromiseLike, error?: ErrorValidator, message?: string): Promise; + throws(value: () => PromiseLike, error?: ErrorValidator, message?: string): Promise; throws(value: () => void, error?: ErrorValidator, message?: string): any; /** * Assert that function doesn't throw an error or promise resolves. */ notThrows(value: PromiseLike, message?: string): Promise; + notThrows(value: () => PromiseLike, message?: string): Promise; notThrows(value: () => void, message?: string): void; /** * Assert that contents matches regex.