From 2aeebb3a5927d28c072cadf09aec5891f1ddaab4 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 17 Dec 2017 18:06:36 -0500 Subject: [PATCH 1/2] Assert: Add `assert.rejects`. --- src/assert.js | 107 +++++++++++++++++++++++++ test/main/assert.js | 191 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+) diff --git a/src/assert.js b/src/assert.js index 52e7e4a74..1df8e6710 100644 --- a/src/assert.js +++ b/src/assert.js @@ -305,6 +305,113 @@ class Assert { message } ); } + + rejects( promise, expected, message ) { + let result = false; + + const currentTest = ( this instanceof Assert && this.test ) || config.current; + + // 'expected' is optional unless doing string comparison + if ( objectType( expected ) === "string" ) { + if ( message === undefined ) { + message = expected; + expected = undefined; + } else { + message = "assert.rejects does not accept a string value for the expected " + + "argument.\nUse a non-string object value (e.g. validator function) instead " + + "if necessary."; + + currentTest.assert.pushResult( { + result: false, + message: message + } ); + + return; + } + } + + const then = promise && promise.then; + if ( objectType( then ) !== "function" ) { + const message = "The value provided to `assert.rejects` in " + + "\"" + currentTest.testName + "\" was not a promise."; + + currentTest.assert.pushResult( { + result: false, + message: message, + actual: promise + } ); + + return; + } + + const done = this.async(); + + return then.call( + promise, + function handleFulfillment() { + const message = "The promise returned by the `assert.rejects` callback in " + + "\"" + currentTest.testName + "\" did not reject."; + + currentTest.assert.pushResult( { + result: false, + message: message, + actual: promise + } ); + + done(); + }, + + function handleRejection( actual ) { + if ( actual ) { + const expectedType = objectType( expected ); + + // We don't want to validate + if ( expected === undefined ) { + result = true; + expected = null; + + // Expected is a regexp + } else if ( expectedType === "regexp" ) { + result = expected.test( errorString( actual ) ); + + // Expected is a constructor, maybe an Error constructor + } else if ( expectedType === "function" && actual instanceof expected ) { + result = true; + + // Expected is an Error object + } else if ( expectedType === "object" ) { + result = actual instanceof expected.constructor && + actual.name === expected.name && + actual.message === expected.message; + + // Expected is a validation function which returns true if validation passed + } else { + if ( expectedType === "function" ) { + result = expected.call( {}, actual ) === true; + expected = null; + + // Expected is some other invalid type + } else { + result = false; + message = "invalid expected value provided to `assert.rejects` " + + "callback in \"" + currentTest.testName + "\": " + + expectedType + "."; + } + } + } + + + currentTest.assert.pushResult( { + result, + actual, + expected, + message + } ); + + done(); + } + ); + } } // Provide an alternative to assert.throws(), for environments that consider throws a reserved word diff --git a/test/main/assert.js b/test/main/assert.js index ed5c166a1..354598981 100644 --- a/test/main/assert.js +++ b/test/main/assert.js @@ -1,3 +1,23 @@ +function buildMockPromise( settledValue, shouldFulfill ) { + + // Return a mock self-fulfilling Promise ("thenable") + var thenable = { + then: function( fulfilledCallback, rejectedCallback ) { + setTimeout( function() { + return shouldFulfill ? + fulfilledCallback.call( thenable, settledValue ) : + rejectedCallback.call( thenable, settledValue ); + }, 13 ); + + // returning another thennable for easy confirmation + // of return value + return buildMockPromise( "final promise", true ); + } + }; + + return thenable; +} + QUnit.module( "assert" ); QUnit.test( "ok", function( assert ) { @@ -266,6 +286,118 @@ QUnit.test( "throws", function( assert ) { ); } ); +QUnit.test( "rejects", function( assert ) { + assert.expect( 15 ); + + function CustomError( message ) { + this.message = message; + } + + CustomError.prototype.toString = function() { + return this.message; + }; + + const rejectsReturnValue = assert.rejects( + buildMockPromise( "my error" ) + ); + + assert.equal( + typeof rejectsReturnValue.then, + "function", + "rejects returns a thennable" + ); + + assert.rejects( + buildMockPromise( "my error" ), + "simple string rejection, no 'expected' value given" + ); + + // This test is for IE 7 and prior which does not properly + // implement Error.prototype.toString + assert.rejects( + buildMockPromise( new Error( "error message" ) ), + /error message/, + "use regexp against instance of Error" + ); + + assert.rejects( + buildMockPromise( new TypeError() ), + Error, + "thrown TypeError without a message is an instance of Error" + ); + + assert.rejects( + buildMockPromise( new TypeError() ), + TypeError, + "thrown TypeError without a message is an instance of TypeError" + ); + + assert.rejects( + buildMockPromise( new TypeError( "error message" ) ), + Error, + "thrown TypeError with a message is an instance of Error" + ); + + // This test is for IE 8 and prior which goes against the standards + // by considering that the native Error constructors, such TypeError, + // are also instances of the Error constructor. As such, the assertion + // sometimes went down the wrong path. + assert.rejects( + buildMockPromise( new TypeError( "error message" ) ), + TypeError, + "thrown TypeError with a message is an instance of TypeError" + ); + + assert.rejects( + buildMockPromise( new CustomError( "some error description" ) ), + CustomError, + "thrown error is an instance of CustomError" + ); + + assert.rejects( + buildMockPromise( new Error( "some error description" ) ), + /description/, + "use a regex to match against the stringified error" + ); + + assert.rejects( + buildMockPromise( new Error( "foo" ) ), + new Error( "foo" ), + "thrown error object is similar to the expected Error object" + ); + + assert.rejects( + buildMockPromise( new CustomError( "some error description" ) ), + new CustomError( "some error description" ), + "thrown error object is similar to the expected CustomError object" + ); + + assert.rejects( + buildMockPromise( { + name: "SomeName", + message: "some message" + } ), + { name: "SomeName", message: "some message" }, + "thrown error object is similar to the expected plain object" + ); + + assert.rejects( + buildMockPromise( new CustomError( "some error description" ) ), + function( err ) { + return err instanceof CustomError && /description/.test( err ); + }, + "custom validation function" + ); + + this.CustomError = CustomError; + + assert.rejects( + buildMockPromise( new this.CustomError( "some error description" ) ), + /description/, + "throw error from property of 'this' context" + ); +} ); + QUnit.test( "raises, alias for throws", function( assert ) { assert.strictEqual( assert.raises, assert.throws ); } ); @@ -391,6 +523,65 @@ QUnit.test( "throws", function( assert ) { ); } ); +QUnit.test( "rejects", function( assert ) { + assert.rejects( + buildMockPromise( "some random value", /* shouldResolve */ true ), + "fails when the provided promise fulfills" + ); + + assert.rejects( + buildMockPromise( "foo" ), + /bar/, + "rejects fails when regexp does not match" + ); + + assert.rejects( + buildMockPromise( new Error( "foo" ) ), + function RandomConstructor() { }, + "rejects fails when rejected value is not an instance of the provided constructor" + ); + + function SomeConstructor() { } + + assert.rejects( + buildMockPromise( new SomeConstructor() ), + function OtherRandomConstructor() { }, + "rejects fails when rejected value is not an instance of the provided constructor" + ); + + assert.rejects( + buildMockPromise( "some value" ), + function() { return false; }, + "rejects fails when the expected function returns false" + ); + + assert.rejects( null ); + + assert.rejects( + buildMockPromise( "foo" ), + 2, + "rejects fails when provided a number" + ); + + assert.rejects( + buildMockPromise( "foo" ), + "string matcher", + "rejects fails when provided a number" + ); + + assert.rejects( + buildMockPromise( "foo" ), + false, + "rejects fails when provided a boolean" + ); + + assert.rejects( + buildMockPromise( "foo" ), + [], + "rejects fails when provided an array" + ); +} ); + ( function() { var previousTestAssert; From dc451e6f1d9977595026f66c55f3f39b83db3f15 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 17 Dec 2017 18:23:26 -0500 Subject: [PATCH 2/2] Docs: Add assert.rejects API documentation. --- docs/assert/rejects.md | 83 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/assert/rejects.md diff --git a/docs/assert/rejects.md b/docs/assert/rejects.md new file mode 100644 index 000000000..765d75ee6 --- /dev/null +++ b/docs/assert/rejects.md @@ -0,0 +1,83 @@ +--- +layout: default +title: rejects +description: Test if the provided promise rejects, and optionally compare the rejection value. +categories: + - assert +--- + +## `rejects( promise, expectedMatcher [, message ] )` + +Test if the provided promise rejects, and optionally compare the rejection value. + +| name | description | +|--------------------|--------------------------------------| +| `promise` (thenable) | promise to test for rejection | +| `expectedMatcher` | Rejection value matcher | +| `message` (string) | A short description of the assertion | + + +### Description + +When testing code that is expected to return a rejected promise based on a +specific set of circumstances, use `assert.rejects()` for testing and +comparison. + +The `expectedMatcher` argument can be: + +* A function that returns `true` when the assertion should be considered passing. +* A base constructor to use ala `rejectionValue instanceof expectedMatcher` +* A RegExp that matches (or partially matches) `rejectionValue.toString()` + +Note: in order to avoid confusion between the `message` and the `expectedMatcher`, the `expectedMatcher` **can not** be a string. + +### Example + +Assert the correct error message is received for a custom error object. + +```js +QUnit.test( "rejects", function( assert ) { + + assert.rejects(Promise.reject("some error description")); + + assert.rejects( + Promise.reject(new Error("some error description")), + "rejects with just a message, not using the 'expectedMatcher' argument" + ); + + assert.rejects( + Promise.reject(new Error("some error description")), + /description/, + "`rejectionValue.toString()` contains `description`" + ); + + // Using a custom error like object + function CustomError( message ) { + this.message = message; + } + + CustomError.prototype.toString = function() { + return this.message; + }; + + assert.rejects( + Promise.reject(new CustomError()), + CustomError, + "raised error is an instance of CustomError" + ); + + assert.rejects( + Promise.reject(new CustomError("some error description")), + new CustomError("some error description"), + "raised error instance matches the CustomError instance" + ); + + assert.rejects( + Promise.reject(throw new CustomError("some error description")), + function( err ) { + return err.toString() === "some error description"; + }, + "raised error instance satisfies the callback function" + ); +}); +```