Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fulfill or betray promises #68

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c291a07
add `promise` method
jandockx Aug 19, 2017
f7bcc4e
add `promise` documentation
jandockx Aug 19, 2017
a64f863
add unit test for `promise` - ok for failures, but fails for things t…
jandockx Aug 19, 2017
ece3d5c
`promise` is a function, not a property with a getter
jandockx Aug 19, 2017
45d510e
remove NOK warning - promise tests are green
jandockx Aug 19, 2017
66c5ae7
add nop helper function for `fulfill` and `betray`
jandockx Aug 19, 2017
d3a71d1
add `fulfill` method
jandockx Aug 19, 2017
bb50814
add `fulfill` documentation
jandockx Aug 19, 2017
9e14a5f
`fulfill` is a function, not a property with a getter
jandockx Aug 19, 2017
592807a
add `betray` method
jandockx Aug 19, 2017
ef3fb0f
add `betray` documentation
jandockx Aug 19, 2017
c06a334
`betray` is a function, not a property with a getter
jandockx Aug 19, 2017
1cb2baf
add new methods to TS definition
jandockx Aug 19, 2017
8c61412
generalise failing promise tests
jandockx Aug 19, 2017
b737286
fix mistake in promise_test
jandockx Aug 19, 2017
3d8a5ea
Copy Promise require from resolve test, and remove optionality of Pro…
jandockx Aug 19, 2017
e6fc085
Refactor isPromise test out, and repeat a little code, to get a good …
jandockx Aug 19, 2017
46b67ff
fix a bug: make sure result of resolve or reject is propagated also w…
jandockx Aug 19, 2017
ce39985
Make clear that not is confusing in documentation (it is not being te…
jandockx Aug 19, 2017
8711db1
Add GREEN tests for `fulfill`
jandockx Aug 19, 2017
524f87b
add unit tests for `betray`, with 1 particular annoying test still fa…
jandockx Aug 19, 2017
1be6601
cleanup (removing unused code) of fulfill test
jandockx Aug 19, 2017
03bb54c
marked test that has issues
jandockx Aug 19, 2017
43b4ede
fixed broken test
jandockx Aug 19, 2017
0117313
deal with UnhandledPromiseRejectionWarning
jandockx Aug 19, 2017
e753a56
add Node v8 (LTS) to supported Node versions ("node" is now v9)
jandockx Dec 11, 2017
a112127
fix mocha timeout with node 0.10 - we now get clear failing tests
jandockx Dec 11, 2017
c77e39c
fix the problem with Promise stringification in test in Node 0.10 by …
jandockx Dec 11, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ node_js:
- "5"
- "6"
- "7"
- "8"

notifications:
email: ["[email protected]"]
42 changes: 42 additions & 0 deletions must.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ interface Must {
be: CallableMust;
before(expected): Must;
below(expected): Must;
betray<TResult>(catchCondition?: (reason: any) => TResult | PromiseLike<TResult>): Promise<TResult>;
betray(catchCondition?: (reason: any) => void): Promise<any>;
between(begin, end): Must;
boolean(): Must;
contain(expected): Must;
Expand All @@ -24,6 +26,8 @@ interface Must {
false(): Must;
falsy(): Must;
frozen(): Must;
fulfill<TResult>(fulfilledCondition?: (value?: any) => TResult | PromiseLike<TResult>): Promise<TResult>;
fulfill(fulfilledCondition?: (value?: any) => void): Promise<any>;
function(): Must;
gt(expected: number): Must;
gte(expected: number): Must;
Expand Down Expand Up @@ -52,6 +56,7 @@ interface Must {
ownProperties(properties: any): Must;
ownProperty(property: string, value?): Must;
permutationOf(expected: Array<any>): Must;
promise(): Must;
properties(properties: any): Must;
property(property: string, value?): Must;
regexp(): Must;
Expand Down Expand Up @@ -93,4 +98,41 @@ declare global {
interface Array<T> {
must: Must;
}

// copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/es6-shim/index.d.ts
interface PromiseLike<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): PromiseLike<TResult>;

then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => void): PromiseLike<TResult>;
}

/**
* Represents the completion of an asynchronous operation
*/
interface Promise<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): Promise<TResult>;

then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => void): Promise<TResult>;

/**
* Attaches a callback for only the rejection of the Promise.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of the callback.
*/
catch(onrejected?: (reason: any) => T | PromiseLike<T>): Promise<T>;

catch(onrejected?: (reason: any) => void): Promise<T>;
}
}
149 changes: 149 additions & 0 deletions must.js
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,149 @@ defineGetter(Must.prototype, "reject", function() {
return Rejectable(this)
})

/**
* Assert that an object is a promise, and returns it.
*
* The determination uses duck typing, i.e., it checks whether the object has
* a `then` and a `catch` method.
*
* There are several implementations of promises in the wild, and a promise you
* receive from a library might not be an `instanceof` _your_ `Promise` type,
* although they can work together.
*
* ```javascript
* promise.must.be.a(Promise)
* ```
*
* might fail, while
*
* ```javascript
* promise.must.be.a.promise
* ```
*
* might be upheld.
*
* @example
* Promise.resolve(42).must.be.a.promise()
* Promise.reject(42).must.be.a.promise()
*
* @method promise
*/
Must.prototype.promise = function() {
this.assert(isPromise(this.actual), isPromiseMsg, { actual: this.actual })
return this.actual
}

/**
* Assert that an object is a promise (see `promise`), that eventually resolves.
* The assertion returns a promise that settles to the outcome (resolve result or
* reject error) of `fulfilledCondition`. `fulfilledCondition` is called with the
* resolution (resolve result) of the original promise when it resolves.
* If the original promise is rejected, this assertion fails, and `fulfilledCondition`
* is not called. `fulfilledCondition` is optional.
*
* This approach makes it possible to immediate express assertions about the original
* promise's resolve result.
*
* You should not use `not` to negate `fulfill`. Things will get weird. Use `betray`
* to express that the promise should be rejected instead.
*
* @example
* Promise.resolve(42).must.fulfill()
* Promise.resolve(42).must.fulfill(function(result) {
* result.must.be.a.number()
* result.must.be.truthy()
* return result // the resulting promise will be fulfilled
* })
* Promise.resolve(42).must.fulfill(function(result) {
* result.must.be.a.number()
* result.must.be.truthy()
* throw result // the resulting promise will be rejected
* })
* Promise.resolve(42).must.fulfill(function(result) {
* result.must.not.be.a.number() // fails
* result.must.be.truthy()
* return result
* })
* Promise.reject(new Error()).must.fulfill(function(result) { // fulfill fails, callback is not executed
* result.must.not.be.a.number()
* result.must.be.truthy()
* return result
* })
*
* @method fulfill
* @param fulfilledCondition
*/
Must.prototype.fulfill = function(fulfilledCondition) {
var must = this
must.assert(isPromise(this.actual), isPromiseMsg, {actual: this.actual})
var caught = must.actual.catch(function(err) {
must.assert(
false,
"resolve, but got rejected with \'" + (err && err.message ? err.message : err) + "\'",
{actual: must.actual}
)
})
return fulfilledCondition ? caught.then(fulfilledCondition) : caught
}

/**
* Assert that an object is a promise (see `promise`), that eventually rejects ("betrays" the promise).
* The assertion returns a promise that settles to the outcome (resolve result or
* reject error) of `catchCondition`. `catchCondition` is called with the
* error (reject result) of the original promise when it rejects.
* If the original promise is fulfilled (resolved), this assertion fails, and `catchCondition`
* is not called. `catchCondition` is optional.
*
* This approach makes it possible to immediate express assertions about the original
* promise's reject error.
*
* You should not use `not` to negate `betray`. Things will get weird. Use `fulfill`
* to express that the promise should be resolved instead.
*
* @example
* Promise.reject(new Error()).must.betray()
* Promise.reject(42).must.betray(function(err) {
* err.must.be.a.number()
* err.must.be.truthy()
* return result // the resulting promise will be fulfilled
* })
* Promise.reject(42).must.betray(function(err) {
* err.must.be.a.number()
* err.must.be.truthy()
* throw result // the resulting promise will be rejected
* })
* Promise.reject(42).must.betray(function(err) {
* err.must.not.be.a.number() // fails
* err.must.be.truthy()
* return result
* })
* Promise.resolve(42).must.betray(function(err) { // betray fails, callback is not executed
* err.must.not.be.a.number()
* err.must.be.truthy()
* return result
* })
*
* @method betray
* @param catchCondition
*/
Must.prototype.betray = function(catchCondition) {
var must = this
must.assert(isPromise(this.actual), isPromiseMsg, {actual: this.actual})
return must.actual.then(
function(result) {
must.assert(
false,
"reject, but got fulfilled with \'" + stringify(result) + "\'",
{actual: must.actual}
)
},
catchCondition
? catchCondition
: function(err) { throw err }
)
}

/**
* Assert a string starts with the given string.
*
Expand Down Expand Up @@ -1285,4 +1428,10 @@ function messageFromError(err) {

function isFn(fn) { return typeof fn === "function" }
function isNumber(n) { return typeof n === "number" || n instanceof Number }

var isPromiseMsg = "be a promise (i.e., have a \'then\' and a \'catch\' function)"
function isPromise(p) {
return p && typeof p.then === "function" && typeof p.catch === "function"
}

function passthrough() { return this }
66 changes: 66 additions & 0 deletions test/must/_failing_promise_tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
var Must = require("../..")
var assert = require("./assert")
var assertionErrorTest = require("./_assertion_error_test")

function dummy () {}

var thenNoCatch = {then: dummy}
var catchNoThen = {catch: dummy}
var catchAndThen = {then: dummy, catch: dummy}

module.exports = function(callToTest) {
it("must fail given null", function () {
assert.fail(function () { callToTest(Must(null)) })
})

it("must fail given undefined", function () {
assert.fail(function () { callToTest(Must(undefined)) })
})

it("must fail given boolean primitive", function () {
assert.fail(function () { callToTest(Must(true)) })
assert.fail(function () { callToTest(Must(false)) })
})

it("must fail given number primitive", function () {
assert.fail(function () { callToTest(Must(42)) })
})

it("must fail given string primitive", function () {
assert.fail(function () { callToTest(Must("")) })
})

it("must fail given array", function () {
assert.fail(function () { callToTest(Must([])) })
})

it("must fail given object", function () {
assert.fail(function () { callToTest(Must({})) })
})

it("must fail given an object with a then function, but not a catch function", function () {
assert.fail(function () { callToTest(Must(thenNoCatch)) })
})

it("must fail given an object with a catch function, but not a then function", function () {
assert.fail(function () { callToTest(Must(catchNoThen)) })
})

assertionErrorTest(
function() { callToTest(Must(catchNoThen)) },
{
actual: catchNoThen,
message: "{} must be a promise (i.e., have a \'then\' and a \'catch\' function)"
}
)

describe(".not", function() {
it("must invert the assertion", function() {
assert.fail(function() { callToTest(Must(catchAndThen).not) })
})
})
}

module.exports.thenNoCatch = thenNoCatch
module.exports.catchNoThen = catchNoThen
module.exports.catchAndThen = catchAndThen
Loading