Skip to content

Commit

Permalink
Transfer flags around better. Fixes GH-5.
Browse files Browse the repository at this point in the history
This resulted in a somewhat-extensive rewrite of the core `eventually`-extender logic. In the end the code ends up being much the same, but with slightly clearer flow and many more comments.

Uses Chai 1.0.2's new `transferFlags` utility.
  • Loading branch information
domenic committed May 27, 2012
1 parent 2610c9c commit 4bc1d6b
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 64 deletions.
144 changes: 81 additions & 63 deletions lib/chai-as-promised.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
}.bind(this)
);

return promiseWithAsserters(transformedPromise, this);
return makeAssertionPromise(transformedPromise, this);
};

var rejectedAsserter = function () {
Expand Down Expand Up @@ -218,110 +218,128 @@
}
}.bind(this);

return promiseWithAsserters(transformedPromise.then(onTransformedFulfilled, onTransformedRejected), this);
return makeAssertionPromise(transformedPromise.then(onTransformedFulfilled, onTransformedRejected), this);
}.bind(this);

var transformedPromise = promiseWithAsserters(this._obj.then(onOriginalFulfilled, onOriginalRejected), this);
var transformedPromise = makeAssertionPromise(this._obj.then(onOriginalFulfilled, onOriginalRejected), this);
Object.defineProperty(transformedPromise, "with", { enumerable: true, configurable: true, value: withMethod });

return transformedPromise;
};

function promiseWithAsserters(promise, originalAssertion) {
function makeAssertion(fulfillmentValue) {
var newAssertion = null;
function isChaiAsPromisedAsserter(asserterName) {
return ["fulfilled", "rejected", "broken", "eventually", "become"].indexOf(asserterName) !== -1;
}

if (fulfillmentValue instanceof Assertion) {
newAssertion = new Assertion(fulfillmentValue._obj);
utils.flag(newAssertion, "negate", utils.flag(fulfillmentValue, "negate"));
} else {
newAssertion = new Assertion(fulfillmentValue);
utils.flag(newAssertion, "negate", utils.flag(originalAssertion, "negate"));
}
function makeAssertionPromiseToDoAsserter(currentAssertionPromise, previousAssertionPromise, doAsserter) {
var promiseToDoAsserter = currentAssertionPromise.then(function (fulfillmentValue) {
// The previous assertion promise might have picked up some flags while waiting for fulfillment.
utils.transferFlags(previousAssertionPromise, currentAssertionPromise);

return newAssertion;
}
// Replace the object flag with the fulfillment value, so that doAsserter can operate properly.
utils.flag(currentAssertionPromise, "object", fulfillmentValue);

function promiseToDoAsserter(doAsserter) {
var promiseForAssertion = promise.then(makeAssertion);
var basicPromiseToDoAsserter = promiseForAssertion.then(doAsserter);
var promiseToDoAsserterWithAsserters = promiseWithAsserters(basicPromiseToDoAsserter, originalAssertion);
// Perform the actual asserter action.
doAsserter(currentAssertionPromise);

return promiseToDoAsserterWithAsserters;
}
// Return the fulfillmentValue for future assertions of the new assertion-promise to act upon.
return fulfillmentValue;
});
return makeAssertionPromise(promiseToDoAsserter, currentAssertionPromise);
}

function makeAssertionPromise(promise, baseAssertion) {
// An assertion-promise is an (extensible!) promise with the following additions:
var assertionPromise = Object.create(promise);

// 1. A `notify` method.
addNotifyMethod(assertionPromise);

// 2. An `assert` method that acts exactly as it would on an assertion. This is called by promisified
// asserters after the promise fulfills.
assertionPromise.assert = function () {
return Assertion.prototype.assert.apply(assertionPromise, arguments);
};

// Use `Object.create` to ensure we get an extensible promise, so we can add `with`. Q promises, for example,
// are non-extensible.
var augmentedPromise = Object.create(promise);
// 3. Chai asserters, which act upon the promise's fulfillment value.
var asserterNames = Object.getOwnPropertyNames(Assertion.prototype);
asserterNames.forEach(function (asserterName) {
if (["fulfilled", "rejected", "broken", "eventually", "become"].indexOf(asserterName) !== -1) {
Object.defineProperty(augmentedPromise, asserterName, {
get: function () {
throw new Error("Cannot use Chai as Promised asserters more than once in an assertion.");
}
// We already added `notify` and `assert`; don't mess with those.
if (asserterName === "notify" || asserterName === "assert") {
return;
}

// Only add asserters for other libraries; poison-pill Chai as Promised ones.
if (isChaiAsPromisedAsserter(asserterName)) {
utils.addProperty(assertionPromise, asserterName, function () {
throw new Error("Cannot use Chai as Promised asserters more than once in an assertion.");
});
return;
}

// The asserter will need to be added differently depending on its type. In all cases we use
// `makeAssertionPromiseToDoAsserter`, which, given this current `assertionPromise` we are going to
// return, plus the `baseAssertion` we are basing it off of, will return a new assertion-promise that
// builds off of `assertionPromise` and `baseAssertion` to perform the actual asserter action upon
// fulfillment.
var propertyDescriptor = Object.getOwnPropertyDescriptor(Assertion.prototype, asserterName);

if (propertyDescriptor.value) {
propertyDescriptor.value = function () {
if (typeof propertyDescriptor.value === "function") {
// Case 1: simple method asserters
utils.addMethod(assertionPromise, asserterName, function () {
var args = arguments;

return promiseToDoAsserter(function (assertion) {
return assertion[asserterName].apply(assertion, args);
return makeAssertionPromiseToDoAsserter(assertionPromise, baseAssertion, function () {
propertyDescriptor.value.apply(assertionPromise, args);
});
};
} else if (propertyDescriptor.get) {
// This case is complicated by asserters that can be used as both chainers and as terminators, e.g.
// (5).should.not.be.an.instanceOf(Object) vs. (5).should.not.be.an("object"). In such cases the getter
// will return a function with its [[Prototype]] set to the assertion, with the function used in the
// terminator case and the assertion [[Prototype]] used in the chainer case.
var getterIsFunction = false;
});
} else if (typeof propertyDescriptor.get === "function") {
// Case 2: property asserters. These break down into two subcases: chainable methods, and pure
// properties. An example of the former is `a`/`an`: `.should.be.an.instanceOf` vs.
// `should.be.an("object")`.
var isChainableMethod = false;
try {
getterIsFunction = typeof propertyDescriptor.get.call({}) === "function";
isChainableMethod = typeof propertyDescriptor.get.call({}) === "function";
} catch (e) { }

if (getterIsFunction) {
// If we've detected we're in the special case described above, we need to handle it specially:
// The getter should be for a function that calls the original function, and the function should
// have its [[Prototype]] set (via `__proto__`, ugh) to the augmented promise, which you can chain
// off of just like an assertion (but better).
propertyDescriptor.get = function () {
/*jshint proto:true */
function funcToBeGotten() {
if (isChainableMethod) {
// Case 2A: chainable methods. Recreate the chainable method, but operating on the augmented
// promise. We need to copy both the assertion behavior and the chaining behavior, since the
// chaining behavior might for example set flags on the object.
utils.addChainableMethod(
assertionPromise,
asserterName,
function () {
var args = arguments;
return promiseToDoAsserter(function (assertion) {
return Function.prototype.apply.call(assertion[asserterName], assertion, args);

return makeAssertionPromiseToDoAsserter(assertionPromise, baseAssertion, function () {
propertyDescriptor.get().apply(assertionPromise, args);
});
},
function () {
propertyDescriptor.get.call(assertionPromise);
}
funcToBeGotten.__proto__ = augmentedPromise;
return funcToBeGotten;
};
);
} else {
propertyDescriptor.get = function () {
return promiseToDoAsserter(function (assertion) {
return assertion[asserterName];
// Case 2B: pure property case
utils.addProperty(assertionPromise, asserterName, function () {
return makeAssertionPromiseToDoAsserter(assertionPromise, baseAssertion, function () {
propertyDescriptor.get.call(assertionPromise);
});
};
});
}
}

Object.defineProperty(augmentedPromise, asserterName, propertyDescriptor);
});

addNotifyMethod(augmentedPromise);
return augmentedPromise;
return assertionPromise;
}

property("fulfilled", fulfilledAsserter);
property("rejected", rejectedAsserter);
property("broken", rejectedAsserter);

property("eventually", function () {
return promiseWithAsserters(this._obj, this);
return makeAssertionPromise(this._obj, this);
});

method("become", function (value) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"devDependencies": {
"coffee-script": "1",
"mocha": "1",
"chai": ">= 1.0.1",
"chai": ">= 1.0.2",
"cover": "*",
"jshint": "*",
"q": "*",
Expand Down

0 comments on commit 4bc1d6b

Please sign in to comment.