Permalink
Browse files

Enable making assertions over all elements of an array.

Implements #2.
  • Loading branch information...
1 parent 6ebcb06 commit 318ac1830396551c02bf2d3382f0f8d851577d07 @RubenVerborgh RubenVerborgh committed Apr 5, 2013
Showing with 215 additions and 7 deletions.
  1. +21 −1 README.md
  2. +94 −5 lib/chai-things.js
  3. +1 −1 package.json
  4. +99 −0 test/all.coffee
View
@@ -1,7 +1,9 @@
# Chai Things
Chai Things adds support to [Chai](http://chaijs.com/) for assertions on array elements.
-## Example
+## Examples
+
+### Something
Use the `something` property on an array to test whether the assertion holds for one of its elements.
```javascript
@@ -24,6 +26,24 @@ You are free to choose the syntactic variant you like most:
[{ a: 'cat' }, { a: 'dog' }].should.contain.an.item.with.property('a', 'dog')
```
+### All
+Use the `all` property on an array to test whether the assertion holds for all its elements.
+
+```javascript
+// All items are below 20
+[4, 11, 15].should.all.be.below(20)
+// All items have a property 'a'
+[{ a: 'cat' }, { a: 'dog' }].should.all.have.property('a')
+// If the test fails, we get a descriptive message
+[4, 11, 15].should.all.be.above(20)
+/* expected all elements of [ 4, 11, 15 ] to be above 20 */
+[{ a: 'cat' }, { a: 'dog' }].should.all.have.property('a', 'cat')
+/* expected all elements of [ { a: 'cat' }, { a: 'dog' } ] to have a property 'a' of 'cat', but got 'dog' */
+```
+
+There are currently no syntactic variants for `all`. Let me know if you need them.
+
+
## Installation and usage
```bash
$ npm install chai-things
View
@@ -26,9 +26,8 @@
throw new Error("cannot use something without include or contains");
// Flag that this is a `something` chain
var lastSomething = this, newSomething = {};
- while (utils.flag(lastSomething, "something")) {
+ while (utils.flag(lastSomething, "something"))
lastSomething = utils.flag(lastSomething, "something");
- }
// Transfer all the flags to the new `something` and remove them from `this`
utils.transferFlags(this, newSomething, false);
for (var prop in this.__flags)
@@ -61,15 +60,35 @@
);
}
+ // Handles the `all` chain property
+ function chainAll() {
+ // Flag that this is an `all` chain
+ var lastAll = this, newAll = {};
+ while (utils.flag(lastAll, "all"))
+ lastAll = utils.flag(lastAll, "all");
+ // Transfer all the flags to the new `all` and remove them from `this`
+ utils.transferFlags(this, newAll, false);
+ for (var prop in this.__flags)
+ if (!/^(?:all|object|ssfi|message)$/.test(prop))
+ delete this.__flags[prop];
+
+ // Add the `newAll` to the `lastAll` in the chain.
+ utils.flag(lastAll, "all", newAll);
+ // Clear the `all` flag from `newAll`.
+ utils.flag(newAll, "all", false);
+ }
+
// Find all assertion methods
var methodNames = Object.getOwnPropertyNames(assertionPrototype)
.filter(function (propertyName) {
var property = Object.getOwnPropertyDescriptor(assertionPrototype, propertyName);
return typeof property.value === "function";
});
- // Override all methods to react on a possible `something` in the chain
+ // Override all assertion methods
methodNames.forEach(function (methodName) {
+
+ // Override the method to react on a possible `something` in the chain
Assertion.overwriteMethod(methodName, function (_super) {
return function somethingMethod() {
// Return if no `something` has been used in the chain
@@ -108,11 +127,13 @@
// means the base assertion ("no element must satisfy x") fails
if (negate) {
if (!utils.flag(somethingFlags, "something")) {
- // If we have no child `something`s then assert the negated item assertion, which should fail and throw an error
+ // If we have no child `something`s then assert the negated item assertion,
+ // which should fail and throw an error
var negItemAssertion = copyAssertion(this, item, somethingAssert, true);
somethingMethod.apply(negItemAssertion, arguments);
}
- // Throw here if we have a child `something`, or, for some reason, the negated item assertion didn't throw
+ // Throw here if we have a child `something`,
+ // or if the negated item assertion didn't throw for some reason
new Assertion(arrayObject).assert(false,
"expected no element of #{this} to satisfy the assertion");
}
@@ -134,6 +155,72 @@
throw assertionError;
};
});
+
+ // Override the method to react on a possible `all` in the chain
+ Assertion.overwriteMethod(methodName, function (_super) {
+ return function allMethod() {
+ // Return if no `all` has been used in the chain
+ var allFlags = utils.flag(this, "all");
+ if (!allFlags)
+ return _super.apply(this, arguments);
+ // Use the nested `all` flag as the new `all` flag for this.
+ utils.flag(this, "all", utils.flag(allFlags, "all"));
+
+ // The assertion's object for `all` should be array-like
+ var arrayObject = utils.flag(this, "object");
+ expect(arrayObject).to.have.property("length");
+ var length = arrayObject.length;
+ expect(length).to.be.a("number", "all object length");
+
+ // In the positive case, an empty array means success
+ var negate = utils.flag(allFlags, "negate");
+ if (!negate && !length)
+ return;
+
+ // Try the assertion on every array element
+ var assertionError, item, itemAssertion;
+ for (var i = 0; i < length; i++) {
+ // Test whether the element satisfies the assertion
+ item = arrayObject[i];
+ itemAssertion = copyAssertion(this, item, allAssert);
+ assertionError = null;
+ try { allMethod.apply(itemAssertion, arguments); }
+ catch (error) { assertionError = error; }
+ // If the element does not satisfy the assertion
+ if (assertionError) {
+ // In the positive case, this means the assertion has failed
+ if (!negate) {
+ // If we have no child `all`s then throw the item's assertion error
+ if (!utils.flag(allFlags, "all"))
+ throw assertionError;
+ // Throw here if we have a child `all`,
+ new Assertion(arrayObject).assert(false,
+ "expected all elements of #{this} to satisfy the assertion");
+ }
+ // In the negative case, a failing element means the assertion holds
+ return;
+ }
+ }
+ // Changes the assertion message to an array viewpoint
+ function allAssert(test, positive, negative, expected, actual) {
+ var replacement = (negate ? "not " : "") + "all elements of #{this}";
+ utils.flag(this, "object", arrayObject);
+ assertionPrototype.assert.call(this, test,
+ (negate ? negative : positive).replace("#{this}", replacement),
+ (negate ? positive : negative).replace("#{this}", replacement),
+ expected, actual);
+ }
+ // If we reach this point, no failing element has been found
+ if (negate) {
+ // Assert the negated item assertion which should fail and throw an error
+ var negItemAssertion = copyAssertion(this, item, allAssert, true);
+ allMethod.apply(negItemAssertion, arguments);
+ // Throw here if the negated item assertion didn't throw for some reason
+ new Assertion(arrayObject).assert(false,
+ "expected not all elements of #{this} to satisfy the assertion");
+ }
+ };
+ });
});
// Copies an assertion to another item, using the specified assert function
@@ -151,4 +238,6 @@
if (!(methodName in assertionPrototype))
Assertion.addChainableMethod(methodName, assertSomething, chainSomething);
});
+ // Define the `all` chainable assertion method
+ Assertion.addChainableMethod("all", null, chainAll);
}));
View
@@ -1,6 +1,6 @@
{
"name": "chai-things",
- "version": "0.1.2",
+ "version": "0.2.0",
"author": "Ruben Verborgh <ruben.verborgh@gmail.com> (http://ruben.verborgh.org/)",
"main": "./lib/chai-things.js",
"license": "MIT",
View
@@ -0,0 +1,99 @@
+# This file describes the behavior of the `all` property
+
+describe "using all()", ->
+
+describe "an object without length", ->
+ nonLengthObject = {}
+
+ it "fails to have all elements equal to 1", ->
+ (() -> nonLengthObject.should.all.equal 1).
+ should.throw "expected {} to have a property 'length'"
+
+ it "fails not to have all elements equal to 1", ->
+ (() -> nonLengthObject.should.all.equal 1).
+ should.throw "expected {} to have a property 'length'"
+
+
+describe "an object without numeric length", ->
+ nonNumLengthObject = { length: 'a' }
+
+ it "fails to have all elements equal to 1", ->
+ (() -> nonNumLengthObject.should.all.equal 1).
+ should.throw "all object length: expected 'a' to be a number"
+
+ it "fails not to have all elements equal to 1", ->
+ (() -> nonNumLengthObject.should.all.equal 1).
+ should.throw "all object length: expected 'a' to be a number"
+
+
+describe "an empty array's elements", ->
+ emptyArray = []
+
+ it "should trivially all equal 1", ->
+ emptyArray.should.all.equal(1)
+
+ it "should trivially all not equal 1", ->
+ emptyArray.should.all.not.equal(1)
+
+
+describe "the array [1, 1]'s elements", ->
+ array = [1, 1]
+
+ it "should all equal 1", ->
+ array.should.all.equal 1
+
+ it "should all not equal 2", ->
+ array.should.all.not.equal 2
+
+ it "should not all equal 2", ->
+ array.should.not.all.equal 2
+
+ it "should not all not equal 1", ->
+ array.should.not.all.not.equal 1
+
+ it "do not all equal 2", ->
+ (() -> array.should.all.equal 2).
+ should.throw "expected all elements of [ 1, 1 ] to equal 2"
+
+ it "do not all *not* equal 1", ->
+ (() -> array.should.all.not.equal 1).
+ should.throw "expected all elements of [ 1, 1 ] to not equal 1"
+
+ it "do not *not* all equal 1", ->
+ (() -> array.should.not.all.equal 1).
+ should.throw "expected not all elements of [ 1, 1 ] to equal 1"
+
+ it "do not *not* all not equal 2", ->
+ (() -> array.should.not.all.not.equal 2).
+ should.throw "expected not all elements of [ 1, 1 ] to not equal 2"
+
+describe "the array [1, 2]'s elements", ->
+ array = [1, 2]
+
+ it "should not all equal 1", ->
+ array.should.not.all.equal 1
+
+ it "should not all equal 2", ->
+ array.should.not.all.equal 2
+
+ it "should not all not equal 1", ->
+ array.should.not.all.not.equal 1
+
+ it "should not all not equal 2", ->
+ array.should.not.all.not.equal 2
+
+ it "do not all equal 1", ->
+ (() -> array.should.all.equal 1).
+ should.throw "expected all elements of [ 1, 2 ] to equal 1"
+
+ it "do not all equal 2", ->
+ (() -> array.should.all.equal 2).
+ should.throw "expected all elements of [ 1, 2 ] to equal 2"
+
+ it "do not all not equal 1", ->
+ (() -> array.should.all.not.equal 1).
+ should.throw "expected all elements of [ 1, 2 ] to not equal 1"
+
+ it "do not all not equal 2", ->
+ (() -> array.should.all.not.equal 2).
+ should.throw "expected all elements of [ 1, 2 ] to not equal 2"

0 comments on commit 318ac18

Please sign in to comment.