diff --git a/lib/chai-things.js b/lib/chai-things.js index b1388c3..e8eaa8d 100644 --- a/lib/chai-things.js +++ b/lib/chai-things.js @@ -21,9 +21,6 @@ // Handles the `something` chain property function chainSomething() { - // `include` or `contains` should have been called before - if (!utils.flag(this, "contains")) - 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")) @@ -42,22 +39,31 @@ // Performs the `something()` assertion function assertSomething() { - // Undo the flags set by the `something` chain property var somethingFlags = utils.flag(this, "something"); + + // `include` or `contains` should have been called before + if (!utils.flag(this, "contains") && !utils.flag(somethingFlags, "contains")) + throw new Error("cannot use something without include or contains"); + + // Undo the flags set by the `something` chain property utils.flag(this, "something", false); if (somethingFlags) utils.transferFlags(somethingFlags, this, true); - // The assertion's object for `something` should be array-like + // The assertion's object for `something` should be enumerable and non-empty var object = utils.flag(this, "object"); - expect(object).to.have.property("length"); - expect(object.length).to.be.a("number", "something object length"); - - // The object should contain something - this.assert(object.length > 0, - "expected #{this} to contain something", - "expected #{this} not to contain something" - ); + if (object.hasOwnProperty("length")) { + expect(object.length).to.be.a("number", "something object length"); + this.assert(object.length, + "expected #{this} to contain something", + "expected #{this} not to contain something"); + } else { + expect(object).is.an("object", "since length property unavailable"); + this.assert(Object.keys(object).length, + "expected #{this} to contain something", + "expected #{this} not to contain something"); + } + } // Handles the `all` chain property @@ -91,17 +97,43 @@ // 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 var somethingFlags = utils.flag(this, "something"); + + // Passthrough keys assertion + if(methodName.match(/keys?/) && somethingFlags) { + if (utils.flag(this, "negate") || utils.flag(somethingFlags, "negate")) + utils.flag(this, "negate", true); + if (utils.flag(this, "contains") || utils.flag(somethingFlags, "contains")) + utils.flag(this, "contains", true); + + utils.flag(this, "any", true); + utils.flag(this, "something", false); + utils.flag(this, "all", false); + + return _super.apply(this, arguments); + } + // Return if no `something` has been used in the chain if (!somethingFlags) return _super.apply(this, arguments); + + // `include` or `contains` should have been called before + if (!utils.flag(somethingFlags, "contains")) + throw new Error("cannot use something without include or contains"); + // Use the nested `something` flag as the new `something` flag for this. utils.flag(this, "something", utils.flag(somethingFlags, "something")); // The assertion's object for `something` should be array-like var arrayObject = utils.flag(this, "object"); - expect(arrayObject).to.have.property("length"); - var length = arrayObject.length; + + var length; + if (!arrayObject.hasOwnProperty("length")) { + expect(arrayObject).is.an("object", "since length property unavailable"); + length = Object.keys(arrayObject).length; + } else { + length = arrayObject.length; + } + expect(length).to.be.a("number", "something object length"); // In the negative case, an empty array means success @@ -114,7 +146,7 @@ // Try the assertion on every array element var assertionError; - for (var i = 0; i < length; i++) { + for (var i in arrayObject) { // Test whether the element satisfies the assertion var item = arrayObject[i], itemAssertion = copyAssertion(this, item, somethingAssert); @@ -159,8 +191,22 @@ // 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"); + + // passthrough keys assertion + if(methodName.match(/keys?/) && allFlags) { + if (utils.flag(this, "negate") || utils.flag(allFlags, "negate")) + utils.flag(this, "negate", true); + if (utils.flag(this, "contains") || utils.flag(allFlags, "contains")) + utils.flag(this, "contains", true); + + utils.flag(this, "all", true); + utils.flag(this, "any", false); + + return _super.apply(this, arguments); + } + + // Return if no `all` has been used in the chain if (!allFlags) return _super.apply(this, arguments); // Use the nested `all` flag as the new `all` flag for this. @@ -168,10 +214,17 @@ // 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; + var length; + if (!arrayObject.hasOwnProperty("length")) { + expect(arrayObject).is.an("object", "since length property unavailable"); + length = Object.keys(arrayObject).length; + } else { + expect(arrayObject).to.have.property("length"); + 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) @@ -179,7 +232,7 @@ // Try the assertion on every array element var assertionError, item, itemAssertion; - for (var i = 0; i < length; i++) { + for (var i in arrayObject) { // Test whether the element satisfies the assertion item = arrayObject[i]; itemAssertion = copyAssertion(this, item, allAssert); @@ -235,9 +288,10 @@ // Define the `something` chainable assertion method and its variants ["something", "thing", "item", "one", "some", "any"].forEach(function (methodName) { - if (!(methodName in assertionPrototype)) + if (!(methodName in assertionPrototype) || methodName == "any") // Always overwrite now Assertion.addChainableMethod(methodName, assertSomething, chainSomething); }); // Define the `all` chainable assertion method Assertion.addChainableMethod("all", null, chainAll); + Assertion.addChainableMethod("each", null, chainAll); })); diff --git a/test/all.coffee b/test/all.coffee index 7689492..670f31d 100644 --- a/test/all.coffee +++ b/test/all.coffee @@ -3,16 +3,14 @@ describe "using all()", -> describe "an object without length", -> - nonLengthObject = {} + nonLengthObject = 1 - 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", -> + it "fails to have all elements equal to assertion", -> (() -> nonLengthObject.should.all.equal 1). - should.throw "expected {} to have a property 'length'" - + should.throw "since length property unavailable: expected 1 to be an object" + it "fails not to have all elements equal to assertion", -> + (() -> nonLengthObject.should.not.all.equal 1). + should.throw "since length property unavailable: expected 1 to be an object" describe "an object without numeric length", -> nonNumLengthObject = { length: 'a' } @@ -35,6 +33,15 @@ describe "an empty array's elements", -> it "should trivially all not equal 1", -> emptyArray.should.all.not.equal(1) +describe "an empty object's elements", -> + emptyObject = {} + + it "should trivially all equal 1", -> + emptyObject.should.all.equal(1) + + it "should trivially all not equal 1", -> + emptyObject.should.all.not.equal(1) + describe "the array [1, 1]'s elements", -> array = [1, 1] @@ -67,6 +74,37 @@ describe "the array [1, 1]'s elements", -> (() -> array.should.not.all.not.equal 2). should.throw "expected not all elements of [ 1, 1 ] to not equal 2" +describe "the object {first: 1, second: 1}'s elements", -> + obj = {leading: 1, trailing: 1} + + it "should all equal 1", -> + obj.should.all.equal 1 + + it "should all not equal 2", -> + obj.should.all.not.equal 2 + + it "should not all equal 2", -> + obj.should.not.all.equal 2 + + it "should not all not equal 1", -> + obj.should.not.all.not.equal 1 + + it "do not all equal 2", -> + (() -> obj.should.all.equal 2). + should.throw "expected all elements of { leading: 1, trailing: 1 } to equal 2" + + it "do not all *not* equal 1", -> + (() -> obj.should.all.not.equal 1). + should.throw "expected all elements of { leading: 1, trailing: 1 } to not equal 1" + + it "do not *not* all equal 1", -> + (() -> obj.should.not.all.equal 1). + should.throw "expected not all elements of { leading: 1, trailing: 1 } to equal 1" + + it "do not *not* all not equal 2", -> + (() -> obj.should.not.all.not.equal 2). + should.throw "expected not all elements of { leading: 1, trailing: 1 } to not equal 2" + describe "the array [1, 2]'s elements", -> array = [1, 2] @@ -116,3 +154,4 @@ describe "the array [{ a: 'b' }, { a: 'c' }]'s elements", -> it.skip "should not all have property 'a' not equal 'c'", -> (() -> array.should.all.have.property("a").not.equal("c")). should.throw "expected all elements of [ { a: 'b' }, { a: 'c' } ] to satisfy the assertion" + diff --git a/test/something.coffee b/test/something.coffee index de4be6e..03c3e70 100644 --- a/test/something.coffee +++ b/test/something.coffee @@ -17,23 +17,23 @@ describe "using something()", -> describe "an object without length", -> - nonLengthObject = {} + nonLengthObject = 1 it "fails to include something", -> (() -> nonLengthObject.should.include.something()). - should.throw "expected {} to have a property 'length'" + should.throw "since length property unavailable: expected 1 to be an object" it "fails not to include something", -> (() -> nonLengthObject.should.not.include.something()). - should.throw "expected {} to have a property 'length'" + should.throw "since length property unavailable: expected 1 to be an object" it "fails to include something that equals 1", -> (() -> nonLengthObject.should.include.something.that.equals 1). - should.throw "expected {} to have a property 'length'" + should.throw "since length property unavailable: expected 1 to be an object" it "fails not to include something that equals 1", -> (() -> nonLengthObject.should.not.include.something.that.equals 1). - should.throw "expected {} to have a property 'length'" + should.throw "since length property unavailable: expected 1 to be an object" describe "an object without numeric length", -> @@ -115,6 +115,47 @@ describe "the array [{ a: 1 }, { b: 2 }]", -> it "should not include something with a property b of 3", -> array.should.not.include.something.with.property('b', 3) +describe "the object {primary: { a: 1 }, secondary: { b: 2 }}", -> + obj = { primary: { a: 1 }, secondary: { b: 2 }} + + it "should include something", -> + obj.should.include.something() + + it "does not *not* include something", -> + (() -> obj.should.not.include.something()). + should.throw + + it "should include something that deep equals { b: 2 }", -> + obj.should.include.something.that.deep.equals { b: 2 } + + it "should include something that not deep equals { b: 2 }", -> + obj.should.include.something.that.not.deep.equals { b: 2 } + + it "does not include something that deep equals { c: 3 }", -> + (() -> obj.should.include.something.that.deep.equals { c: 3 }). + should.throw "expected an element of { Object (primary, secondary) } to deeply equal { c: 3 }" + + it "should not include something that deep equals { c : 3 }", -> + obj.should.not.include.something.that.deep.equals { c: 3 } + + it "should include something that not deep equals { c: 3 }", -> + obj.should.include.something.that.not.deep.equals { c: 3 } + + it "does not *not* include something that deep equals { b: 2 }", -> + (() -> obj.should.not.include.something.that.deep.equals { b: 2 }). + should.throw "expected no element of { Object (primary, secondary) } to deeply equal { b: 2 }" + + it "should include something with a property b of 2", -> + obj.should.include.something.with.property('b', 2) + + it "does not include something with a property b of 3", -> + (() -> obj.should.include.something.with.property('b', 3)). + should.throw "expected an element of { Object (primary, secondary) }" + + " to have a property 'b' of 3, but got 2" + + it "should not include something with a property b of 3", -> + obj.should.not.include.something.with.property('b', 3) + describe "the array [{ a: 1 }, { a: 1 }]", -> array = [{ a: 1 }, { a: 1 }] diff --git a/test/versions.coffee b/test/versions.coffee new file mode 100644 index 0000000..5f2eb79 --- /dev/null +++ b/test/versions.coffee @@ -0,0 +1,65 @@ +# This file describes the behavior of the `all` property + +chai_vers = process.env.npm_package_devDependencies_chai +chai_major_vers = (chai_vers.split ".").shift() + +describe "regression testing keys()", -> + console.log chai_major_vers + (if chai_major_vers == '1' then describe else describe.skip) "1.4 assertion keys", -> + describe "out of the box", -> + obj = { first: 1, second: 2, third: 3} + + it "should succeed all keys, if match inclusive", -> + obj.should.be.all.keys("first", "second", "third") + + it "should succeed all keys, fail if match subset", -> + obj.should.contain.all.keys("second", "third") + + it "should fail not all keys, if mismatch", -> + obj.should.not.have.all.keys("first", "second", "third", "fourth") + + it "should succeed all keys, if match subset", -> + obj.should.contain.all.keys("second") + + it "should fail not any keys, if mismatch", -> + obj.should.not.include.any.keys("fourth") + + describe "polyfilling any", -> + ob_nested = {first: { foo: 1 }, second: { bar: 2 }, third: {}} + + it "should succeed some keys, if match subset", -> + ob_nested.should.contain.any.keys("first") + + it "should succeed any keys, if match inclusive", -> + ob_nested.should.not.include.any.keys("second", "third", "bar") + + (if chai_major_vers != '1' then describe else describe.skip) "3+ assertion keys", -> + obj = { first: 1, second: 2, third: 3} + + it "should succeed all keys, if match inclusive", -> + obj.should.be.all.keys("first", "second", "third") + + it "should succeed all keys, fail if match subset", -> + obj.should.contain.all.keys("second", "third") + + it "should throw on all keys, if match subset", -> + (() -> obj.should.not.be.not.all.keys("first", "second")). + should.throw + + it "should fail on all keys, if match subset", -> + obj.should.not.be.not.all.keys("fourth", "eigth", "sixteenth") + + it "should succeed any keys, if match inclusive", -> + obj.should.include.any.keys("second", "third", "fourth") + + it "should fail not all keys, if mismatch", -> + obj.should.not.have.all.keys("first", "second", "third", "fourth") + + it "should succeed some keys, if match subset", -> + obj.should.contain.any.key("second") + + it "should succeed all keys, if match subset", -> + obj.should.contain.all.keys("second") + + it "should fail not any keys, if mismatch", -> + obj.should.not.include.any.keys("fourth")