From d45117435e7596d9fc2aaa37aeef88111eeced13 Mon Sep 17 00:00:00 2001 From: Oky Antoro Date: Tue, 23 Jan 2018 21:11:15 +0700 Subject: [PATCH 1/2] Make `t.throws()` and `t.notThrows()` accept async function as parameter. Refs: https://github.com/avajs/ava/issues/1371 --- index.js.flow | 4 ++-- lib/assert.js | 36 +++++++++++++++++++++++++++++---- readme.md | 16 +++++++++++++-- test/assert.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++-- types/base.d.ts | 2 ++ 5 files changed, 101 insertions(+), 10 deletions(-) diff --git a/index.js.flow b/index.js.flow index d21470695..548889fa2 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 throws or returns 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..4e48ee33a 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 `async 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..301decf52 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'); @@ -655,7 +656,11 @@ test('.throws()', t => { }); }); - t.end(); + 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('.throws() returns the thrown error', t => { @@ -678,6 +683,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'); @@ -723,7 +750,13 @@ test('.notThrows()', t => { values: [{label: 'Threw:', formatted: /foo/}] }); - t.end(); + return eventuallyFailsWith(t, assertions.notThrows(() => { + return Promise.reject(new Error('foo')); + }), { + assertion: 'notThrows', + message: '', + values: [{label: 'Threw:', formatted: /foo/}] + }); }); test('.notThrows() returns undefined for a fulfilled promise', t => { @@ -732,6 +765,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/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. From ad010ad901bd4f1389b4d889d0a21ddfc5454eea Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Mon, 29 Jan 2018 12:18:36 +0000 Subject: [PATCH 2/2] Minor tweaks --- index.js.flow | 2 +- readme.md | 2 +- test/assert.js | 14 ++-------- test/observable.js | 58 +++++++++++++++++++++++++++++++++++++++-- test/promise.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 16 deletions(-) diff --git a/index.js.flow b/index.js.flow index 548889fa2..ff9914606 100644 --- a/index.js.flow +++ b/index.js.flow @@ -61,7 +61,7 @@ type AssertContext = { (value: PromiseLike, error?: ErrorValidator, message?: string): Promise; (value: () => mixed, error?: ErrorValidator, message?: string): any; }; - // Assert that the promise resolves, or the function doesn't throws or returns a resolved promise. + // 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/readme.md b/readme.md index 4e48ee33a..98a98bcdc 100644 --- a/readme.md +++ b/readme.md @@ -952,7 +952,7 @@ test('rejects', async t => { }); ``` -When testing an `async function` you must also wait for the assertion to complete: +When testing an asynchronous function you must also wait for the assertion to complete: ```js test('throws', async t => { diff --git a/test/assert.js b/test/assert.js index 301decf52..4edfdcfe0 100644 --- a/test/assert.js +++ b/test/assert.js @@ -656,11 +656,7 @@ test('.throws()', 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'/}] - }); + t.end(); }); test('.throws() returns the thrown error', t => { @@ -750,13 +746,7 @@ test('.notThrows()', t => { values: [{label: 'Threw:', formatted: /foo/}] }); - return eventuallyFailsWith(t, assertions.notThrows(() => { - return Promise.reject(new Error('foo')); - }), { - assertion: 'notThrows', - message: '', - values: [{label: 'Threw:', formatted: /foo/}] - }); + t.end(); }); test('.notThrows() returns undefined for a fulfilled promise', t => { 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 => {