From 5da97ccc74a5fd457eead2924300a24016995482 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Mon, 12 Feb 2018 16:13:49 +0000 Subject: [PATCH 1/3] Place all throws/notThrows tests in test/assert.js Move tests from test/promise.js and test/observable.js. --- test/assert.js | 194 ++++++++++++++++++++++++++++++++++++++++----- test/observable.js | 165 -------------------------------------- test/promise.js | 193 -------------------------------------------- 3 files changed, 175 insertions(+), 377 deletions(-) diff --git a/test/assert.js b/test/assert.js index 14ed1d95e..49c7c3467 100644 --- a/test/assert.js +++ b/test/assert.js @@ -27,7 +27,9 @@ const assertions = assert.wrapAssertions({ throw new Error('Expected testObj'); } - promise.catch(err => { + promise.then(() => { + lastPassed = true; + }, err => { lastFailure = err; }); }, @@ -75,16 +77,44 @@ function assertFailure(t, subset) { } } +let gathering = false; +let gatheringPromise = Promise.resolve(); +function gather(run) { + return t => { + if (gathering) { + throw new Error('Cannot nest gather()'); + } + + gathering = true; + try { + run(t); + return gatheringPromise; + } finally { + gathering = false; + gatheringPromise = Promise.resolve(); + } + }; +} +function add(fn) { + if (!gathering) { + throw new Error('Cannot add promise, must be called from gather() callback'); + } + gatheringPromise = gatheringPromise.then(fn); + return gatheringPromise; +} + function failsWith(t, fn, subset) { lastFailure = null; fn(); assertFailure(t, subset); } -function eventuallyFailsWith(t, promise, subset) { - lastFailure = null; - return promise.then(() => { - assertFailure(t, subset); +function eventuallyFailsWith(t, fn, subset) { + return add(() => { + lastFailure = null; + return fn().then(() => { + assertFailure(t, subset); + }); }); } @@ -98,6 +128,19 @@ function fails(t, fn) { } } +function eventuallyFails(t, fn) { + return add(() => { + lastFailure = null; + return fn().then(() => { + if (lastFailure) { + t.pass(); + } else { + t.fail('Expected assertion to fail'); + } + }); + }); +} + function passes(t, fn) { lastPassed = false; fn(); @@ -108,6 +151,19 @@ function passes(t, fn) { } } +function eventuallyPasses(t, fn) { + return add(() => { + lastPassed = false; + return fn().then(() => { + if (lastPassed) { + t.pass(); + } else { + t.fail('Expected assertion to pass'); + } + }); + }); +} + test('.pass()', t => { passes(t, () => { assertions.pass(); @@ -633,7 +689,8 @@ test('.notDeepEqual()', t => { t.end(); }); -test('.throws()', t => { +test('.throws()', gather(t => { + // Fails because function doesn't throw. failsWith(t, () => { assertions.throws(() => {}); }, { @@ -642,6 +699,8 @@ test('.throws()', t => { values: [] }); + // Fails because function doesn't throw. Asserts that 'my message' is used + // as the assertion message (*not* compared against the error). failsWith(t, () => { assertions.throws(() => {}, Error, 'my message'); }, { @@ -650,6 +709,7 @@ test('.throws()', t => { values: [] }); + // Fails because thrown error's message is not equal to 'bar' const err = new Error('foo'); failsWith(t, () => { assertions.throws(() => { @@ -661,14 +721,89 @@ test('.throws()', t => { values: [{label: 'Threw unexpected exception:', formatted: /foo/}] }); + // Passes because thrown error's message is equal to 'bar' + passes(t, () => { + assertions.throws(() => { + throw err; + }, 'foo'); + }); + + // Passes because an error is thrown. passes(t, () => { assertions.throws(() => { throw new Error('foo'); }); }); - t.end(); -}); + // Fails because the promise is resolved, not rejected. + eventuallyFailsWith(t, () => assertions.throws(Promise.resolve('foo')), { + assertion: 'throws', + message: 'Expected promise to be rejected, but it was resolved instead', + values: [{label: 'Resolved with:', formatted: /'foo'/}] + }); + + // Fails because the promise is resolved with an Error + eventuallyFailsWith(t, () => assertions.throws(Promise.resolve(new Error())), { + assertion: 'throws', + message: 'Expected promise to be rejected, but it was resolved instead', + values: [{label: 'Resolved with:', formatted: /Error/}] + }); + + // Fails because the function returned a promise that resolved, not rejected. + eventuallyFailsWith(t, () => assertions.throws(() => Promise.resolve('foo')), { + assertion: 'throws', + message: 'Expected promise to be rejected, but it was resolved instead', + values: [{label: 'Resolved with:', formatted: /'foo'/}] + }); + + // Passes because the promise was rejected with an error. + eventuallyPasses(t, () => assertions.throws(Promise.reject(new Error()))); + + // Passes because the function returned a promise rejected with an error. + eventuallyPasses(t, () => assertions.throws(() => Promise.reject(new Error()))); + + // Passes because the error's message matches the regex + eventuallyPasses(t, () => assertions.throws(Promise.reject(new Error('abc')), /abc/)); + + // Fails because the error's message does not match the regex + eventuallyFails(t, () => assertions.throws(Promise.reject(new Error('abc')), /def/)); + + const complete = arg => Observable.of(arg); + const error = err => new Observable(observer => observer.error(err)); + + // Fails because the observable completed, not errored. + eventuallyFailsWith(t, () => assertions.throws(complete('foo')), { + assertion: 'throws', + message: 'Expected promise to be rejected, but it was resolved instead', + values: [{label: 'Resolved with:', formatted: /'foo'/}] + }); + + // Fails because the observable completed with an Error + eventuallyFailsWith(t, () => assertions.throws(complete(new Error())), { + assertion: 'throws', + message: 'Expected promise to be rejected, but it was resolved instead', + values: [{label: 'Resolved with:', formatted: /Error/}] + }); + + // Fails because the function returned a observable that completed, not rejected. + eventuallyFailsWith(t, () => assertions.throws(() => complete('foo')), { + assertion: 'throws', + message: 'Expected promise to be rejected, but it was resolved instead', + values: [{label: 'Resolved with:', formatted: /'foo'/}] + }); + + // Passes because the observable errored with an error. + eventuallyPasses(t, () => assertions.throws(error(new Error()))); + + // Passes because the function returned an observable errored with an error. + eventuallyPasses(t, () => assertions.throws(() => error(new Error()))); + + // Passes because the error's message matches the regex + eventuallyPasses(t, () => assertions.throws(error(new Error('abc')), /abc/)); + + // Fails because the error's message does not match the regex + eventuallyFails(t, () => assertions.throws(error(new Error('abc')), /def/)); +})); test('.throws() returns the thrown error', t => { const expected = new Error(); @@ -724,19 +859,13 @@ test('.throws() fails if passed a bad value', t => { t.end(); }); -test('promise .throws() fails when promise is resolved', t => { - return eventuallyFailsWith(t, assertions.throws(Promise.resolve('foo')), { - assertion: 'throws', - message: 'Expected promise to be rejected, but it was resolved instead', - values: [{label: 'Resolved with:', formatted: /'foo'/}] - }); -}); - -test('.notThrows()', t => { +test('.notThrows()', gather(t => { + // Passes because the function doesn't throw passes(t, () => { assertions.notThrows(() => {}); }); + // Fails because the function throws. failsWith(t, () => { assertions.notThrows(() => { throw new Error('foo'); @@ -747,6 +876,8 @@ test('.notThrows()', t => { values: [{label: 'Threw:', formatted: /foo/}] }); + // Fails because the function throws. Asserts that message is used for the + // assertion, not to validate the thrown error. failsWith(t, () => { assertions.notThrows(() => { throw new Error('foo'); @@ -757,8 +888,33 @@ test('.notThrows()', t => { values: [{label: 'Threw:', formatted: /foo/}] }); - t.end(); -}); + // Passes because the promise is resolved + eventuallyPasses(t, () => assertions.notThrows(Promise.resolve())); + + // Fails because the promise is rejected + eventuallyFails(t, () => assertions.notThrows(Promise.reject(new Error()))); + + // Passes because the function returned a resolved promise + eventuallyPasses(t, () => assertions.notThrows(() => Promise.resolve())); + + // Fails because the function returned a rejected promise + eventuallyFails(t, () => assertions.notThrows(() => Promise.reject(new Error()))); + + const complete = arg => Observable.of(arg); + const error = err => new Observable(observer => observer.error(err)); + + // Passes because the observable completed + eventuallyPasses(t, () => assertions.notThrows(complete())); + + // Fails because the observable errored + eventuallyFails(t, () => assertions.notThrows(error(new Error()))); + + // Passes because the function returned a completed observable + eventuallyPasses(t, () => assertions.notThrows(() => complete())); + + // Fails because the function returned an errored observable + eventuallyFails(t, () => assertions.notThrows(() => error(new Error()))); +})); test('.notThrows() returns undefined for a fulfilled promise', t => { return assertions.notThrows(Promise.resolve(Symbol(''))).then(actual => { diff --git a/test/observable.js b/test/observable.js index 819fd4f59..f19a1e7d8 100644 --- a/test/observable.js +++ b/test/observable.js @@ -43,168 +43,3 @@ test('returning an observable from a legacy async fn is an error', t => { t.match(result.error.message, /Do not return observables/); }); }); - -test('handle throws with erroring observable', t => { - const instance = ava(a => { - a.plan(1); - - const observable = new Observable(observer => { - observer.error(new Error()); - }); - - return a.throws(observable); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -test('handle throws with erroring observable returned by function', t => { - const instance = ava(a => { - a.plan(1); - - const observable = new Observable(observer => { - observer.error(new Error()); - }); - - return a.throws(() => observable); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -test('handle throws with long running erroring observable', t => { - const instance = ava(a => { - a.plan(1); - - const observable = new Observable(observer => { - setTimeout(() => { - observer.error(new Error('abc')); - }, 2000); - }); - - return a.throws(observable, /abc/); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -test('handle throws with completed observable', t => { - return ava(a => { - a.plan(1); - - const observable = Observable.of(); - return a.throws(observable); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); - -test('handle throws with completed observable returned by function', t => { - return ava(a => { - a.plan(1); - - const observable = Observable.of(); - return a.throws(() => observable); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); - -test('handle throws with regex', t => { - const instance = ava(a => { - a.plan(1); - - const observable = new Observable(observer => { - observer.error(new Error('abc')); - }); - - return a.throws(observable, /abc/); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -test('handle throws with string', t => { - const instance = ava(a => { - a.plan(1); - - const observable = new Observable(observer => { - observer.error(new Error('abc')); - }); - - return a.throws(observable, 'abc'); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -test('handle throws with false-positive observable', t => { - return ava(a => { - a.plan(1); - - const observable = new Observable(observer => { - observer.next(new Error()); - observer.complete(); - }); - - return a.throws(observable); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); - -test('handle notThrows with completed observable', t => { - const instance = ava(a => { - a.plan(1); - - const observable = Observable.of(); - return a.notThrows(observable); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -test('handle notThrows with thrown observable', t => { - return ava(a => { - a.plan(1); - - const observable = new Observable(observer => { - observer.error(new Error()); - }); - - return a.notThrows(observable); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); - -test('handle notThrows with erroring observable returned by function', t => { - return ava(a => { - a.plan(1); - - const observable = new Observable(observer => { - observer.error(new Error()); - }); - - return a.notThrows(() => observable); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); diff --git a/test/promise.js b/test/promise.js index 7ad06bc59..86d2283b1 100644 --- a/test/promise.js +++ b/test/promise.js @@ -118,199 +118,6 @@ test('extra assertion will fail the test', t => { }); }); -test('handle throws with rejected promise', t => { - const instance = ava(a => { - a.plan(1); - - const promise = Promise.reject(new Error()); - return a.throws(promise); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -test('handle throws with rejected promise returned by function', t => { - const instance = ava(a => { - a.plan(1); - - const promise = Promise.reject(new Error()); - return a.throws(() => promise); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -// 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 => { - const instance = ava(a => { - a.plan(1); - - const promise = new Promise((resolve, reject) => { - setTimeout(() => { - reject(new Error('abc')); - }, 2000); - }); - - return a.throws(promise, /abc/); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -test('handle throws with resolved promise', t => { - return ava(a => { - a.plan(1); - - const promise = Promise.resolve(); - return a.throws(promise); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); - -test('handle throws with resolved promise returned by function', t => { - return ava(a => { - a.plan(1); - - const promise = Promise.resolve(); - return a.throws(() => promise); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); - -test('handle throws with regex', t => { - const instance = ava(a => { - a.plan(1); - - const promise = Promise.reject(new Error('abc')); - return a.throws(promise, /abc/); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -test('throws with regex will fail if error message does not match', t => { - return ava(a => { - a.plan(1); - - const promise = Promise.reject(new Error('abc')); - return a.throws(promise, /def/); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); - -test('handle throws with string', t => { - const instance = ava(a => { - a.plan(1); - - const promise = Promise.reject(new Error('abc')); - return a.throws(promise, 'abc'); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -test('throws with string argument will reject if message does not match', t => { - return ava(a => { - a.plan(1); - - const promise = Promise.reject(new Error('abc')); - return a.throws(promise, 'def'); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); - -test('does not handle throws with string reject', t => { - return ava(a => { - a.plan(1); - - const promise = Promise.reject('abc'); // eslint-disable-line prefer-promise-reject-errors - return a.throws(promise, 'abc'); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); - -test('handle throws with false-positive promise', t => { - return ava(a => { - a.plan(1); - - const promise = Promise.resolve(new Error()); - return a.throws(promise); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); - -test('handle notThrows with resolved promise', t => { - const instance = ava(a => { - a.plan(1); - - const promise = Promise.resolve(); - return a.notThrows(promise); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -test('handle notThrows with rejected promise', t => { - return ava(a => { - a.plan(1); - - const promise = Promise.reject(new Error()); - return a.notThrows(promise); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); - -test('handle notThrows with resolved promise returned by function', t => { - const instance = ava(a => { - a.plan(1); - - const promise = Promise.resolve(); - return a.notThrows(() => promise); - }); - return instance.run().then(result => { - t.is(result.passed, true); - t.is(instance.assertCount, 1); - }); -}); - -test('handle notThrows with rejected promise returned by function', t => { - return ava(a => { - a.plan(1); - - const promise = Promise.reject(new Error()); - return a.notThrows(() => promise); - }).run().then(result => { - t.is(result.passed, false); - t.is(result.error.name, 'AssertionError'); - }); -}); - test('assert pass', t => { const instance = ava(a => { return pass().then(() => { From d3b63e7af056effc8f58e1beb03708811347dc5f Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Mon, 12 Feb 2018 17:33:56 +0000 Subject: [PATCH 2/3] Implement `t.throws()` and `t.notThrows()` ourselves Remove `core-assert` dependency. When passed a function as the second argument, `t.throws()` now assumes its a constructor. This removes support for a validation function, which may or may not have worked. Refs #1047. `t.throws()` now fails if the exception is not an error. Fixes #1440. Regular expressions are now matched against the error message, not the result of casting the error to a string. Fixes #1445. Validate second argument to `t.throws()`. Refs #1676. Assertion failures now display how AVA arrived at the exception. Constructors are printed when the error is not a correct instance. Fixes #1471. --- index.d.ts | 2 +- index.js.flow | 2 +- lib/assert.js | 247 +++++++++++++++++++++++++++++----------------- package-lock.json | 4 +- package.json | 2 +- readme.md | 16 +-- test/assert.js | 127 +++++++++++++++++++----- 7 files changed, 274 insertions(+), 126 deletions(-) diff --git a/index.d.ts b/index.d.ts index 72c4a9e1b..d03099a52 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,7 +2,7 @@ export interface ObservableLike { subscribe(observer: (value: any) => void): void; } -export type ThrowsErrorValidator = (new (...args: Array) => any) | RegExp | string | ((error: any) => boolean); +export type ThrowsErrorValidator = (new (...args: Array) => any) | RegExp | string; export interface SnapshotOptions { id?: string; diff --git a/index.js.flow b/index.js.flow index 6c460f9c6..cb4be182e 100644 --- a/index.js.flow +++ b/index.js.flow @@ -7,7 +7,7 @@ export interface ObservableLike { subscribe(observer: (value: any) => void): void; } -export type ThrowsErrorValidator = Class<{constructor(...args: Array): any}> | RegExp | string | ((error: any) => boolean); +export type ThrowsErrorValidator = Class<{constructor(...args: Array): any}> | RegExp | string; export interface SnapshotOptions { id?: string; diff --git a/lib/assert.js b/lib/assert.js index 22d7befcb..762423fce 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -1,7 +1,7 @@ 'use strict'; const concordance = require('concordance'); -const coreAssert = require('core-assert'); const observableToPromise = require('observable-to-promise'); +const isError = require('is-error'); const isObservable = require('is-observable'); const isPromise = require('is-promise'); const concordanceOptions = require('./concordance-options').default; @@ -74,9 +74,6 @@ function wrapAssertions(callbacks) { const fail = callbacks.fail; const noop = () => {}; - const makeRethrow = reason => () => { - throw reason; - }; const assertions = { pass() { @@ -160,154 +157,228 @@ function wrapAssertions(callbacks) { } }, - throws(fn, err, message) { - let promise; - if (isPromise(fn)) { - promise = fn; - } else if (isObservable(fn)) { - promise = observableToPromise(fn); - } else if (typeof fn !== 'function') { + throws(thrower, expected, message) { + if (typeof thrower !== 'function' && !isPromise(thrower) && !isObservable(thrower)) { fail(this, new AssertionError({ assertion: 'throws', improperUsage: true, - message: '`t.throws()` must be called with a function, Promise, or Observable', - values: [formatWithLabel('Called with:', fn)] + message: '`t.throws()` must be called with a function, observable or promise', + values: [formatWithLabel('Called with:', thrower)] })); return; } - let coreAssertThrowsErrorArg; - if (typeof err === 'string') { - const expectedMessage = err; - coreAssertThrowsErrorArg = error => error.message === expectedMessage; - } else { - // Assume it's a constructor function or regular expression - coreAssertThrowsErrorArg = err; + if (typeof expected === 'function') { + expected = {of: expected}; + } else if (typeof expected === 'string' || expected instanceof RegExp) { + expected = {message: expected}; + } else if (arguments.length === 1) { + expected = {}; + } else if (expected !== null) { + fail(this, new AssertionError({ + assertion: 'throws', + improperUsage: true, + message: 'The second argument to `t.throws()` must be a function, string, regular expression or `null`', + values: [formatWithLabel('Called with:', expected)] + })); + return; } - let maybePromise; - const test = (fn, stack) => { - let actual; - let threw = false; - try { - coreAssert.throws(() => { - try { - maybePromise = fn(); - } catch (err) { - actual = err; - threw = true; - throw err; - } - }, coreAssertThrowsErrorArg); - return actual; - } catch (err) { + // Note: this function *must* throw exceptions, since it can be used + // as part of a pending assertion for observables and promises. + const assertExpected = (actual, prefix, stack) => { + if (!isError(actual)) { + throw new AssertionError({ + assertion: 'throws', + message, + stack, + values: [formatWithLabel(`${prefix} exception that is not an error:`, actual)] + }); + } + + if (expected.of && !(actual instanceof expected.of)) { + throw new AssertionError({ + assertion: 'throws', + message, + stack, + values: [ + formatWithLabel(`${prefix} unexpected exception:`, actual), + formatWithLabel('Expected instance of:', expected.of) + ] + }); + } + + if (typeof expected.message === 'string' && actual.message !== expected.message) { throw new AssertionError({ assertion: 'throws', message, stack, - values: threw ? - [formatWithLabel('Threw unexpected exception:', actual)] : - null + values: [ + formatWithLabel(`${prefix} unexpected exception:`, actual), + formatWithLabel('Expected message to equal:', expected.message) + ] }); } + + if (expected.message instanceof RegExp && !expected.message.test(actual.message)) { + throw new AssertionError({ + assertion: 'throws', + message, + stack, + values: [ + formatWithLabel(`${prefix} unexpected exception:`, actual), + formatWithLabel('Expected message to match:', expected.message) + ] + }); + } + }; + + const handleObservable = (observable, wasReturned) => { + // Record stack before it gets lost in the promise chain. + const stack = getStack(); + const intermediate = observableToPromise(observable).then(value => { + throw new AssertionError({ + assertion: 'throws', + message, + stack, + values: [formatWithLabel(`${wasReturned ? 'Returned observable' : 'Observable'} completed with:`, value)] + }); + }, reason => { + assertExpected(reason, `${wasReturned ? 'Returned observable' : 'Observable'} errored with`, stack); + return reason; + }); + + pending(this, intermediate); + // Don't reject the returned promise, even if the assertion fails. + return intermediate.catch(noop); }; - const handlePromise = promise => { + const handlePromise = (promise, wasReturned) => { // Record stack before it gets lost in the promise chain. const stack = getStack(); const intermediate = promise.then(value => { throw new AssertionError({ assertion: 'throws', - message: 'Expected promise to be rejected, but it was resolved instead', - values: [formatWithLabel('Resolved with:', value)] + message, + stack, + values: [formatWithLabel(`${wasReturned ? 'Returned promise' : 'Promise'} resolved with:`, value)] }); - }, reason => test(makeRethrow(reason), stack)); + }, reason => { + assertExpected(reason, `${wasReturned ? 'Returned promise' : 'Promise'} rejected with`, stack); + return reason; + }); pending(this, intermediate); // Don't reject the returned promise, even if the assertion fails. return intermediate.catch(noop); }; - if (promise) { - return handlePromise(promise); + if (isPromise(thrower)) { + return handlePromise(thrower, false); + } else if (isObservable(thrower)) { + return handleObservable(thrower, false); } + let retval; + let actual; + let threw = false; try { - const retval = test(fn); - pass(this); - return retval; + retval = thrower(); } catch (err) { - if (maybePromise) { - if (isPromise(maybePromise)) { - return handlePromise(maybePromise); - } - if (isObservable(maybePromise)) { - return handlePromise(observableToPromise(maybePromise)); - } + actual = err; + threw = true; + } + + if (!threw) { + if (isPromise(retval)) { + return handlePromise(retval, true); + } else if (isObservable(retval)) { + return handleObservable(retval, true); } + fail(this, new AssertionError({ + assertion: 'throws', + message, + values: [formatWithLabel('Function returned:', retval)] + })); + return; + } + + try { + assertExpected(actual, 'Function threw'); + pass(this); + return actual; + } catch (err) { fail(this, err); } }, - notThrows(fn, message) { - let promise; - if (isPromise(fn)) { - promise = fn; - } else if (isObservable(fn)) { - promise = observableToPromise(fn); - } else if (typeof fn !== 'function') { + notThrows(nonThrower, message) { + if (typeof nonThrower !== 'function' && !isPromise(nonThrower) && !isObservable(nonThrower)) { fail(this, new AssertionError({ assertion: 'notThrows', improperUsage: true, - message: '`t.notThrows()` must be called with a function, Promise, or Observable', - values: [formatWithLabel('Called with:', fn)] + message: '`t.notThrows()` must be called with a function, observable or promise', + values: [formatWithLabel('Called with:', nonThrower)] })); return; } - let maybePromise; - const test = (fn, stack) => { - try { - coreAssert.doesNotThrow(() => { - maybePromise = fn(); - }); - } catch (err) { + const handleObservable = (observable, wasReturned) => { + // Record stack before it gets lost in the promise chain. + const stack = getStack(); + const intermediate = observableToPromise(observable).then(noop, reason => { throw new AssertionError({ assertion: 'notThrows', message, stack, - values: [formatWithLabel('Threw:', err.actual)] + values: [formatWithLabel(`${wasReturned ? 'Returned observable' : 'Observable'} errored with:`, reason)] }); - } + }); + pending(this, intermediate); + // Don't reject the returned promise, even if the assertion fails. + return intermediate.catch(noop); }; - const handlePromise = promise => { + const handlePromise = (promise, wasReturned) => { // Record stack before it gets lost in the promise chain. const stack = getStack(); - const intermediate = promise.then(noop, reason => test(makeRethrow(reason), stack)); + const intermediate = promise.then(noop, reason => { + throw new AssertionError({ + assertion: 'notThrows', + message, + stack, + values: [formatWithLabel(`${wasReturned ? 'Returned promise' : 'Promise'} rejected with:`, reason)] + }); + }); pending(this, intermediate); // Don't reject the returned promise, even if the assertion fails. return intermediate.catch(noop); }; - if (promise) { - return handlePromise(promise); + if (isPromise(nonThrower)) { + return handlePromise(nonThrower, false); + } else if (isObservable(nonThrower)) { + return handleObservable(nonThrower, false); } + let retval; try { - test(fn); - if (maybePromise) { - if (isPromise(maybePromise)) { - return handlePromise(maybePromise); - } - if (isObservable(maybePromise)) { - return handlePromise(observableToPromise(maybePromise)); - } - } - pass(this); + retval = nonThrower(); } catch (err) { - fail(this, err); + fail(this, new AssertionError({ + assertion: 'notThrows', + message, + values: [formatWithLabel(`Function threw:`, err)] + })); + return; } + + if (isPromise(retval)) { + return handlePromise(retval, true); + } else if (isObservable(retval)) { + return handleObservable(retval, true); + } + pass(this); }, ifError(actual, message) { diff --git a/package-lock.json b/package-lock.json index 442826346..c424f5093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1330,7 +1330,8 @@ "buf-compare": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buf-compare/-/buf-compare-1.0.1.tgz", - "integrity": "sha1-/vKNqLgROgoNtEMLC2Rntpcws0o=" + "integrity": "sha1-/vKNqLgROgoNtEMLC2Rntpcws0o=", + "dev": true }, "builtin-modules": { "version": "1.1.1", @@ -2097,6 +2098,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/core-assert/-/core-assert-0.2.1.tgz", "integrity": "sha1-+F4s+b/tKPdzzIs/pcW2m9wC/j8=", + "dev": true, "requires": { "buf-compare": "1.0.1", "is-error": "2.2.1" diff --git a/package.json b/package.json index 1a6e099df..c5cdee456 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "common-path-prefix": "^1.0.0", "concordance": "^3.0.0", "convert-source-map": "^1.5.1", - "core-assert": "^0.2.0", "currently-unhandled": "^0.4.1", "debug": "^3.1.0", "dot-prop": "^4.2.0", @@ -104,6 +103,7 @@ "import-local": "^1.0.0", "indent-string": "^3.2.0", "is-ci": "^1.1.0", + "is-error": "^2.2.1", "is-generator-fn": "^1.0.0", "is-obj": "^1.0.0", "is-observable": "^1.1.0", diff --git a/readme.md b/readme.md index bab1f74e7..2bbc5dd7a 100644 --- a/readme.md +++ b/readme.md @@ -910,13 +910,13 @@ Assert that `value` is deeply equal to `expected`. See [Concordance](https://git Assert that `value` is not deeply equal to `expected`. The inverse of `.deepEqual()`. -### `.throws(function|promise, [error, [message]])` +### `.throws(thrower, [expected, [message]])` -Assert that `function` throws an error, `promise` rejects with an error, or `function` returns a rejected `promise`. +Assert that an error is thrown. `thrower` can be a function which should throw, or return a promise that should reject, or an observable that should error. Alternatively a promise or observable can be passed directly. -`error` can be an error constructor, error message, regex matched against the error message, or validation function. +The thrown value *must* be an error. It is returned so you can run more assertions against it. -Returns the error thrown by `function` or a promise for the rejection reason of the specified `promise`. +`expected` can be a constructor, in which case the thrown value must be an instance of the constructor. It can be a string, which is compared against the thrown error's message, or a regular expression which is matched against this message. `expected` does not need to be specified. If you don't need it but do want to set an assertion message you have to specify `null`. Example: @@ -943,7 +943,7 @@ test('rejects', async t => { }); ``` -When testing a promise you must wait for the assertion to complete: +When testing an observable or promise you must wait for the assertion to complete: ```js test('rejects', async t => { @@ -963,11 +963,11 @@ test('throws', async t => { }); ``` -### `.notThrows(function|promise, [message])` +### `.notThrows(nonThrower, [message])` -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. +Assert that no error is thrown. `thrower` can be a function which shouldn't throw, or return a promise that should resolve, or an observable that should complete. Alternatively a promise or an observable can be passed directly. -Like the `.throws()` assertion, when testing a promise you must wait for the assertion to complete: +Like the `.throws()` assertion, when testing a promise or an observable you must wait for the assertion to complete: ```js test('resolves', async t => { diff --git a/test/assert.js b/test/assert.js index 49c7c3467..0dd7fd83b 100644 --- a/test/assert.js +++ b/test/assert.js @@ -128,6 +128,7 @@ function fails(t, fn) { } } +/* Might be useful function eventuallyFails(t, fn) { return add(() => { lastFailure = null; @@ -140,6 +141,7 @@ function eventuallyFails(t, fn) { }); }); } +*/ function passes(t, fn) { lastPassed = false; @@ -696,17 +698,31 @@ test('.throws()', gather(t => { }, { assertion: 'throws', message: '', - values: [] + values: [{label: 'Function returned:', formatted: /undefined/}] }); // Fails because function doesn't throw. Asserts that 'my message' is used // as the assertion message (*not* compared against the error). failsWith(t, () => { - assertions.throws(() => {}, Error, 'my message'); + assertions.throws(() => {}, null, 'my message'); }, { assertion: 'throws', message: 'my message', - values: [] + values: [{label: 'Function returned:', formatted: /undefined/}] + }); + + // Fails because thrown exception is not an error + failsWith(t, () => { + assertions.throws(() => { + const err = 'foo'; + throw err; + }); + }, { + assertion: 'throws', + message: '', + values: [ + {label: 'Function threw exception that is not an error:', formatted: /'foo'/} + ] }); // Fails because thrown error's message is not equal to 'bar' @@ -718,7 +734,24 @@ test('.throws()', gather(t => { }, { assertion: 'throws', message: '', - values: [{label: 'Threw unexpected exception:', formatted: /foo/}] + values: [ + {label: 'Function threw unexpected exception:', formatted: /foo/}, + {label: 'Expected message to equal:', formatted: /bar/} + ] + }); + + // Fails because thrown error is not the right instance + failsWith(t, () => { + assertions.throws(() => { + throw err; + }, class Foo {}); + }, { + assertion: 'throws', + message: '', + values: [ + {label: 'Function threw unexpected exception:', formatted: /foo/}, + {label: 'Expected instance of:', formatted: /Foo/} + ] }); // Passes because thrown error's message is equal to 'bar' @@ -738,22 +771,22 @@ test('.throws()', gather(t => { // Fails because the promise is resolved, not rejected. eventuallyFailsWith(t, () => assertions.throws(Promise.resolve('foo')), { assertion: 'throws', - message: 'Expected promise to be rejected, but it was resolved instead', - values: [{label: 'Resolved with:', formatted: /'foo'/}] + message: '', + values: [{label: 'Promise resolved with:', formatted: /'foo'/}] }); // Fails because the promise is resolved with an Error eventuallyFailsWith(t, () => assertions.throws(Promise.resolve(new Error())), { assertion: 'throws', - message: 'Expected promise to be rejected, but it was resolved instead', - values: [{label: 'Resolved with:', formatted: /Error/}] + message: '', + values: [{label: 'Promise resolved with:', formatted: /Error/}] }); // Fails because the function returned a promise that resolved, not rejected. eventuallyFailsWith(t, () => assertions.throws(() => Promise.resolve('foo')), { assertion: 'throws', - message: 'Expected promise to be rejected, but it was resolved instead', - values: [{label: 'Resolved with:', formatted: /'foo'/}] + message: '', + values: [{label: 'Returned promise resolved with:', formatted: /'foo'/}] }); // Passes because the promise was rejected with an error. @@ -766,7 +799,14 @@ test('.throws()', gather(t => { eventuallyPasses(t, () => assertions.throws(Promise.reject(new Error('abc')), /abc/)); // Fails because the error's message does not match the regex - eventuallyFails(t, () => assertions.throws(Promise.reject(new Error('abc')), /def/)); + eventuallyFailsWith(t, () => assertions.throws(Promise.reject(new Error('abc')), /def/), { + assertion: 'throws', + message: '', + values: [ + {label: 'Promise rejected with unexpected exception:', formatted: /Error/}, + {label: 'Expected message to match:', formatted: /\/def\//} + ] + }); const complete = arg => Observable.of(arg); const error = err => new Observable(observer => observer.error(err)); @@ -774,22 +814,22 @@ test('.throws()', gather(t => { // Fails because the observable completed, not errored. eventuallyFailsWith(t, () => assertions.throws(complete('foo')), { assertion: 'throws', - message: 'Expected promise to be rejected, but it was resolved instead', - values: [{label: 'Resolved with:', formatted: /'foo'/}] + message: '', + values: [{label: 'Observable completed with:', formatted: /'foo'/}] }); // Fails because the observable completed with an Error eventuallyFailsWith(t, () => assertions.throws(complete(new Error())), { assertion: 'throws', - message: 'Expected promise to be rejected, but it was resolved instead', - values: [{label: 'Resolved with:', formatted: /Error/}] + message: '', + values: [{label: 'Observable completed with:', formatted: /Error/}] }); // Fails because the function returned a observable that completed, not rejected. eventuallyFailsWith(t, () => assertions.throws(() => complete('foo')), { assertion: 'throws', - message: 'Expected promise to be rejected, but it was resolved instead', - values: [{label: 'Resolved with:', formatted: /'foo'/}] + message: '', + values: [{label: 'Returned observable completed with:', formatted: /'foo'/}] }); // Passes because the observable errored with an error. @@ -802,7 +842,14 @@ test('.throws()', gather(t => { eventuallyPasses(t, () => assertions.throws(error(new Error('abc')), /abc/)); // Fails because the error's message does not match the regex - eventuallyFails(t, () => assertions.throws(error(new Error('abc')), /def/)); + eventuallyFailsWith(t, () => assertions.throws(error(new Error('abc')), /def/), { + assertion: 'throws', + message: '', + values: [ + {label: 'Observable errored with unexpected exception:', formatted: /Error/}, + {label: 'Expected message to match:', formatted: /\/def\//} + ] + }); })); test('.throws() returns the thrown error', t => { @@ -852,13 +899,25 @@ test('.throws() fails if passed a bad value', t => { assertions.throws('not a function'); }, { assertion: 'throws', - message: '`t.throws()` must be called with a function, Promise, or Observable', + message: '`t.throws()` must be called with a function, observable or promise', values: [{label: 'Called with:', formatted: /not a function/}] }); t.end(); }); +test('.throws() fails if passed a bad expectation', t => { + failsWith(t, () => { + assertions.throws(() => {}, true); + }, { + assertion: 'throws', + message: 'The second argument to `t.throws()` must be a function, string, regular expression or `null`', + values: [{label: 'Called with:', formatted: /true/}] + }); + + t.end(); +}); + test('.notThrows()', gather(t => { // Passes because the function doesn't throw passes(t, () => { @@ -873,7 +932,7 @@ test('.notThrows()', gather(t => { }, { assertion: 'notThrows', message: '', - values: [{label: 'Threw:', formatted: /foo/}] + values: [{label: 'Function threw:', formatted: /foo/}] }); // Fails because the function throws. Asserts that message is used for the @@ -885,20 +944,28 @@ test('.notThrows()', gather(t => { }, { assertion: 'notThrows', message: 'my message', - values: [{label: 'Threw:', formatted: /foo/}] + values: [{label: 'Function threw:', formatted: /foo/}] }); // Passes because the promise is resolved eventuallyPasses(t, () => assertions.notThrows(Promise.resolve())); // Fails because the promise is rejected - eventuallyFails(t, () => assertions.notThrows(Promise.reject(new Error()))); + eventuallyFailsWith(t, () => assertions.notThrows(Promise.reject(new Error())), { + assertion: 'notThrows', + message: '', + values: [{label: 'Promise rejected with:', formatted: /Error/}] + }); // Passes because the function returned a resolved promise eventuallyPasses(t, () => assertions.notThrows(() => Promise.resolve())); // Fails because the function returned a rejected promise - eventuallyFails(t, () => assertions.notThrows(() => Promise.reject(new Error()))); + eventuallyFailsWith(t, () => assertions.notThrows(() => Promise.reject(new Error())), { + assertion: 'notThrows', + message: '', + values: [{label: 'Returned promise rejected with:', formatted: /Error/}] + }); const complete = arg => Observable.of(arg); const error = err => new Observable(observer => observer.error(err)); @@ -907,13 +974,21 @@ test('.notThrows()', gather(t => { eventuallyPasses(t, () => assertions.notThrows(complete())); // Fails because the observable errored - eventuallyFails(t, () => assertions.notThrows(error(new Error()))); + eventuallyFailsWith(t, () => assertions.notThrows(error(new Error())), { + assertion: 'notThrows', + message: '', + values: [{label: 'Observable errored with:', formatted: /Error/}] + }); // Passes because the function returned a completed observable eventuallyPasses(t, () => assertions.notThrows(() => complete())); // Fails because the function returned an errored observable - eventuallyFails(t, () => assertions.notThrows(() => error(new Error()))); + eventuallyFailsWith(t, () => assertions.notThrows(() => error(new Error())), { + assertion: 'notThrows', + message: '', + values: [{label: 'Returned observable errored with:', formatted: /Error/}] + }); })); test('.notThrows() returns undefined for a fulfilled promise', t => { @@ -943,7 +1018,7 @@ test('.notThrows() fails if passed a bad value', t => { assertions.notThrows('not a function'); }, { assertion: 'notThrows', - message: '`t.notThrows()` must be called with a function, Promise, or Observable', + message: '`t.notThrows()` must be called with a function, observable or promise', values: [{label: 'Called with:', formatted: /not a function/}] }); From 94ac0947dbb733df64d4a42be82666d38d97a120 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 13 Feb 2018 14:15:21 +0000 Subject: [PATCH 3/3] Support expectation object in t.throws() Fixes #1047. Fixes #1676. A combination of the following expectations is supported: ```js t.throws(fn, {of: SyntaxError}) // err instanceof SyntaxError t.throws(fn, {name: 'SyntaxError'}) // err.name === 'SyntaxError' t.throws(fn, {is: expectedErrorInstance}) // err === expectedErrorInstance t.throws(fn, {message: 'expected error message'}) // err.message === 'expected error message' t.throws(fn, {message: /expected error message/}) // /expected error message/.test(err.message) ``` --- index.d.ts | 11 ++++- index.js.flow | 11 ++++- lib/assert.js | 85 +++++++++++++++++++++++++++++++++++---- readme.md | 15 ++++--- test/assert.js | 106 +++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 209 insertions(+), 19 deletions(-) diff --git a/index.d.ts b/index.d.ts index d03099a52..a520e8766 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,7 +2,16 @@ export interface ObservableLike { subscribe(observer: (value: any) => void): void; } -export type ThrowsErrorValidator = (new (...args: Array) => any) | RegExp | string; +export type Constructor = (new (...args: Array) => any); + +export type ThrowsExpectation = { + instanceOf?: Constructor; + is?: Error; + message?: string | RegExp; + name?: string; +}; + +export type ThrowsErrorValidator = Constructor | RegExp | string | ThrowsExpectation; export interface SnapshotOptions { id?: string; diff --git a/index.js.flow b/index.js.flow index cb4be182e..73f83f16d 100644 --- a/index.js.flow +++ b/index.js.flow @@ -7,7 +7,16 @@ export interface ObservableLike { subscribe(observer: (value: any) => void): void; } -export type ThrowsErrorValidator = Class<{constructor(...args: Array): any}> | RegExp | string; +export type Constructor = Class<{constructor(...args: Array): any}>; + +export type ThrowsExpectation = { + instanceOf?: Constructor; + is?: Error; + message?: string | RegExp; + name?: string; +}; + +export type ThrowsErrorValidator = Constructor | RegExp | string | ThrowsExpectation; export interface SnapshotOptions { id?: string; diff --git a/lib/assert.js b/lib/assert.js index 762423fce..28b2785ab 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -28,6 +28,8 @@ function formatWithLabel(label, value) { return formatDescriptorWithLabel(label, concordance.describe(value, concordanceOptions)); } +const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); + class AssertionError extends Error { constructor(opts) { super(opts.message || ''); @@ -157,7 +159,7 @@ function wrapAssertions(callbacks) { } }, - throws(thrower, expected, message) { + throws(thrower, expected, message) { // eslint-disable-line complexity if (typeof thrower !== 'function' && !isPromise(thrower) && !isObservable(thrower)) { fail(this, new AssertionError({ assertion: 'throws', @@ -169,19 +171,62 @@ function wrapAssertions(callbacks) { } if (typeof expected === 'function') { - expected = {of: expected}; + expected = {instanceOf: expected}; } else if (typeof expected === 'string' || expected instanceof RegExp) { expected = {message: expected}; - } else if (arguments.length === 1) { + } else if (arguments.length === 1 || expected === null) { expected = {}; - } else if (expected !== null) { + } else if (typeof expected !== 'object' || Array.isArray(expected) || Object.keys(expected).length === 0) { fail(this, new AssertionError({ assertion: 'throws', - improperUsage: true, - message: 'The second argument to `t.throws()` must be a function, string, regular expression or `null`', + message: 'The second argument to `t.throws()` must be a function, string, regular expression, expectation object or `null`', values: [formatWithLabel('Called with:', expected)] })); return; + } else { + if (hasOwnProperty(expected, 'instanceOf') && typeof expected.instanceOf !== 'function') { + fail(this, new AssertionError({ + assertion: 'throws', + message: 'The `instanceOf` property of the second argument to `t.throws()` must be a function', + values: [formatWithLabel('Called with:', expected)] + })); + return; + } + + if (hasOwnProperty(expected, 'message') && typeof expected.message !== 'string' && !(expected.message instanceof RegExp)) { + fail(this, new AssertionError({ + assertion: 'throws', + message: 'The `message` property of the second argument to `t.throws()` must be a string or regular expression', + values: [formatWithLabel('Called with:', expected)] + })); + return; + } + + if (hasOwnProperty(expected, 'name') && typeof expected.name !== 'string') { + fail(this, new AssertionError({ + assertion: 'throws', + message: 'The `name` property of the second argument to `t.throws()` must be a string', + values: [formatWithLabel('Called with:', expected)] + })); + return; + } + + for (const key of Object.keys(expected)) { + switch (key) { + case 'instanceOf': + case 'is': + case 'message': + case 'name': + continue; + default: + fail(this, new AssertionError({ + assertion: 'throws', + message: 'The second argument to `t.throws()` contains unexpected properties', + values: [formatWithLabel('Called with:', expected)] + })); + return; + } + } } // Note: this function *must* throw exceptions, since it can be used @@ -196,14 +241,38 @@ function wrapAssertions(callbacks) { }); } - if (expected.of && !(actual instanceof expected.of)) { + if (hasOwnProperty(expected, 'is') && actual !== expected.is) { + throw new AssertionError({ + assertion: 'throws', + message, + stack, + values: [ + formatWithLabel(`${prefix} unexpected exception:`, actual), + formatWithLabel('Expected to be strictly equal to:', expected.is) + ] + }); + } + + if (expected.instanceOf && !(actual instanceof expected.instanceOf)) { + throw new AssertionError({ + assertion: 'throws', + message, + stack, + values: [ + formatWithLabel(`${prefix} unexpected exception:`, actual), + formatWithLabel('Expected instance of:', expected.instanceOf) + ] + }); + } + + if (typeof expected.name === 'string' && actual.name !== expected.name) { throw new AssertionError({ assertion: 'throws', message, stack, values: [ formatWithLabel(`${prefix} unexpected exception:`, actual), - formatWithLabel('Expected instance of:', expected.of) + formatWithLabel('Expected name to equal:', expected.name) ] }); } diff --git a/readme.md b/readme.md index 2bbc5dd7a..062b2b554 100644 --- a/readme.md +++ b/readme.md @@ -916,7 +916,14 @@ Assert that an error is thrown. `thrower` can be a function which should throw, The thrown value *must* be an error. It is returned so you can run more assertions against it. -`expected` can be a constructor, in which case the thrown value must be an instance of the constructor. It can be a string, which is compared against the thrown error's message, or a regular expression which is matched against this message. `expected` does not need to be specified. If you don't need it but do want to set an assertion message you have to specify `null`. +`expected` can be a constructor, in which case the thrown error must be an instance of the constructor. It can be a string, which is compared against the thrown error's message, or a regular expression which is matched against this message. You can also specify a matcher object with one or more of the following properties: + +* `instanceOf`: a constructor, the thrown error must be an instance of +* `is`: the thrown error must be strictly equal to `expected.is` +* `message`: either a string, which is compared against the thrown error's message, or a regular expression, which is matched against this message +* `name`: the expected `.name` value of the thrown error + +`expected` does not need to be specified. If you don't need it but do want to set an assertion message you have to specify `null`. Example: @@ -955,11 +962,9 @@ When testing an asynchronous function you must also wait for the assertion to co ```js test('throws', async t => { - const error = await t.throws(async () => { + await t.throws(async () => { throw new TypeError('🦄'); - }, TypeError); - - t.is(error.message, '🦄'); + }, {instanceOf: TypeError, message: '🦄'}); }); ``` diff --git a/test/assert.js b/test/assert.js index 0dd7fd83b..0d3deb65a 100644 --- a/test/assert.js +++ b/test/assert.js @@ -145,22 +145,24 @@ function eventuallyFails(t, fn) { function passes(t, fn) { lastPassed = false; + lastFailure = null; fn(); if (lastPassed) { t.pass(); } else { - t.fail('Expected assertion to pass'); + t.ifError(lastFailure, 'Expected assertion to pass'); } } function eventuallyPasses(t, fn) { return add(() => { lastPassed = false; + lastFailure = null; return fn().then(() => { if (lastPassed) { t.pass(); } else { - t.fail('Expected assertion to pass'); + t.ifError(lastFailure, 'Expected assertion to pass'); } }); }); @@ -726,8 +728,8 @@ test('.throws()', gather(t => { }); // Fails because thrown error's message is not equal to 'bar' - const err = new Error('foo'); failsWith(t, () => { + const err = new Error('foo'); assertions.throws(() => { throw err; }, 'bar'); @@ -742,6 +744,7 @@ test('.throws()', gather(t => { // Fails because thrown error is not the right instance failsWith(t, () => { + const err = new Error('foo'); assertions.throws(() => { throw err; }, class Foo {}); @@ -756,6 +759,7 @@ test('.throws()', gather(t => { // Passes because thrown error's message is equal to 'bar' passes(t, () => { + const err = new Error('foo'); assertions.throws(() => { throw err; }, 'foo'); @@ -768,6 +772,52 @@ test('.throws()', gather(t => { }); }); + // Passes because the correct error is thrown. + passes(t, () => { + const err = new Error('foo'); + assertions.throws(() => { + throw err; + }, {is: err}); + }); + + // Fails because the thrown value is not an error + fails(t, () => { + const obj = {}; + assertions.throws(() => { + throw obj; + }, {is: obj}); + }); + + // Fails because the thrown value is not the right one + fails(t, () => { + const err = new Error('foo'); + assertions.throws(() => { + throw err; + }, {is: {}}); + }); + + // Passes because the correct error is thrown. + passes(t, () => { + assertions.throws(() => { + throw new TypeError(); + }, {name: 'TypeError'}); + }); + + // Fails because the thrown value is not an error + fails(t, () => { + assertions.throws(() => { + const err = {name: 'Bob'}; + throw err; + }, {name: 'Bob'}); + }); + + // Fails because the thrown value is not the right one + fails(t, () => { + assertions.throws(() => { + throw new Error('foo'); + }, {name: 'TypeError'}); + }); + // Fails because the promise is resolved, not rejected. eventuallyFailsWith(t, () => assertions.throws(Promise.resolve('foo')), { assertion: 'throws', @@ -911,10 +961,58 @@ test('.throws() fails if passed a bad expectation', t => { assertions.throws(() => {}, true); }, { assertion: 'throws', - message: 'The second argument to `t.throws()` must be a function, string, regular expression or `null`', + message: 'The second argument to `t.throws()` must be a function, string, regular expression, expectation object or `null`', values: [{label: 'Called with:', formatted: /true/}] }); + failsWith(t, () => { + assertions.throws(() => {}, {}); + }, { + assertion: 'throws', + message: 'The second argument to `t.throws()` must be a function, string, regular expression, expectation object or `null`', + values: [{label: 'Called with:', formatted: /\{\}/}] + }); + + failsWith(t, () => { + assertions.throws(() => {}, []); + }, { + assertion: 'throws', + message: 'The second argument to `t.throws()` must be a function, string, regular expression, expectation object or `null`', + values: [{label: 'Called with:', formatted: /\[\]/}] + }); + + failsWith(t, () => { + assertions.throws(() => {}, {instanceOf: null}); + }, { + assertion: 'throws', + message: 'The `instanceOf` property of the second argument to `t.throws()` must be a function', + values: [{label: 'Called with:', formatted: /instanceOf: null/}] + }); + + failsWith(t, () => { + assertions.throws(() => {}, {message: null}); + }, { + assertion: 'throws', + message: 'The `message` property of the second argument to `t.throws()` must be a string or regular expression', + values: [{label: 'Called with:', formatted: /message: null/}] + }); + + failsWith(t, () => { + assertions.throws(() => {}, {name: null}); + }, { + assertion: 'throws', + message: 'The `name` property of the second argument to `t.throws()` must be a string', + values: [{label: 'Called with:', formatted: /name: null/}] + }); + + failsWith(t, () => { + assertions.throws(() => {}, {is: {}, message: '', name: '', of() {}, foo: null}); + }, { + assertion: 'throws', + message: 'The second argument to `t.throws()` contains unexpected properties', + values: [{label: 'Called with:', formatted: /foo: null/}] + }); + t.end(); });