From 992367dcbc7d114d95fc139f889c1c63043b073a Mon Sep 17 00:00:00 2001 From: gvanhove Date: Wed, 9 Mar 2011 18:56:28 -0800 Subject: [PATCH] New matcher "hashContaining" similar to rspec's hash_including --- lib/jasmine-core/jasmine.js | 52 +++++++++++++++++++++++ spec/core/MatchersSpec.js | 84 +++++++++++++++++++++++++++++++++++++ src/core/Env.js | 8 ++++ src/core/Matchers.js | 29 +++++++++++++ src/core/base.js | 15 +++++++ 5 files changed, 188 insertions(+) diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index e7c6ab33..2093f95d 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -196,6 +196,21 @@ jasmine.any = function(clazz) { return new jasmine.Matchers.Any(clazz); }; +/** + * Returns a matchable subset of a hash/JSON object. For use in expectations when you don't care about all of the + * attributes on the object. + * + * @example + * // don't care about any other attributes than foo. + * expect(mySpy).toHaveBeenCalledWith(jasmine.hashContaining({foo: "bar"}); + * + * @param sample {Object} sample + * @returns matchable object for the sample + */ +jasmine.hashContaining = function (sample) { + return new jasmine.Matchers.HashContaining(sample); +}; + /** * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. * @@ -922,6 +937,14 @@ jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { return b.matches(a); } + if (a instanceof jasmine.Matchers.HashContaining) { + return a.matches(b); + } + + if (b instanceof jasmine.Matchers.HashContaining) { + return b.matches(a); + } + if (jasmine.isString_(a) && jasmine.isString_(b)) { return (a == b); } @@ -1477,6 +1500,35 @@ jasmine.Matchers.Any.prototype.toString = function() { return ''; }; +jasmine.Matchers.HashContaining = function (sample) { + this.sample = sample; +}; + +jasmine.Matchers.HashContaining.prototype.matches = function(other, mismatchKeys, mismatchValues) { + mismatchKeys = mismatchKeys || []; + mismatchValues = mismatchValues || []; + + var env = jasmine.getEnv(); + + var hasKey = function(obj, keyName) { + return obj != null && obj[keyName] !== jasmine.undefined; + }; + + for (var property in this.sample) { + if (!hasKey(other, property) && hasKey(this.sample, property)) { + mismatchKeys.push("expected has key '" + property + "', but missing from actual."); + } + else if (!env.equals_(this.sample[property], other[property], mismatchKeys, mismatchValues)) { + mismatchValues.push("'" + property + "' was '" + (other[property] ? jasmine.util.htmlEscape(other[property].toString()) : other[property]) + "' in expected, but was '" + (this.sample[property] ? jasmine.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in actual."); + } + } + + return (mismatchKeys.length === 0 && mismatchValues.length === 0); +}; + +jasmine.Matchers.HashContaining.prototype.toString = function () { + return ""; +}; /** * @constructor */ diff --git a/spec/core/MatchersSpec.js b/spec/core/MatchersSpec.js index cb58208d..e655bf0a 100644 --- a/spec/core/MatchersSpec.js +++ b/spec/core/MatchersSpec.js @@ -830,6 +830,90 @@ describe("jasmine.Matchers", function() { }); }); + describe("HashContaining", function () { + describe("with an empty hash", function () { + var containing; + beforeEach(function () { + containing = new jasmine.Matchers.HashContaining({}); + }); + it("matches everything", function () { + expect(containing.matches("foo", [], [])).toBe(true); + }); + + it("says it didn't expect to contain anything", function () { + expect(containing.toString()).toEqual(""); + }); + }); + + describe("with a hash with items in it", function () { + var containing, mismatchKeys, mismatchValues; + beforeEach(function () { + mismatchKeys = []; + mismatchValues = []; + containing = new jasmine.Matchers.HashContaining({foo: "fooVal", bar: "barVal"}); + }); + + it("doesn't match an empty object", function () { + expect(containing.matches({}, mismatchKeys, mismatchValues)).toBe(false); + }); + + it("doesn't match an object with none of the specified options", function () { + expect(containing.matches({baz:"stuff"}, mismatchKeys, mismatchValues)).toBe(false); + }); + + it("adds a message for each missing key", function () { + containing.matches({foo: "fooVal"}, mismatchKeys, mismatchValues); + expect(mismatchKeys.length).toEqual(1); + }); + + it("doesn't match an object when the values are different", function () { + expect(containing.matches({foo:"notFoo", bar:"notBar"}, mismatchKeys, mismatchValues)).toBe(false); + }); + + it("adds a message when values don't match", function () { + containing.matches({foo: "fooVal", bar: "notBar"}, mismatchKeys, mismatchValues); + expect(mismatchValues.length).toEqual(1); + }); + + it("doesn't match an object with only one of the values matching", function () { + expect(containing.matches({foo:"notFoo", bar:"barVal"}, mismatchKeys, mismatchValues)).toBe(false); + }); + + it("matches when all the values are the same", function () { + expect(containing.matches({foo: "fooVal", bar: "barVal"}, mismatchKeys, mismatchValues)).toBe(true); + }); + + it("matches when there are additional values", function () { + expect(containing.matches({foo: "fooVal", bar: "barVal", baz: "bazVal"}, mismatchKeys, mismatchValues)).toBe(true); + }); + + it("doesn't modify missingKeys or missingValues when match is successful", function () { + containing.matches({foo: "fooVal", bar: "barVal"}, mismatchKeys, mismatchValues); + expect(mismatchKeys.length).toEqual(0); + expect(mismatchValues.length).toEqual(0); + }); + + it("says what it expects to contain", function () { + expect(containing.toString()).toEqual(""); + }); + }); + + describe("in real life", function () { + var method; + beforeEach(function () { + method = jasmine.createSpy("method"); + method({a:"b", c:"d"}); + }); + it("works correctly for positive matches", function () { + expect(method).toHaveBeenCalledWith(jasmine.hashContaining({a:"b"})); + }); + + it("works correctly for negative matches", function () { + expect(method).not.toHaveBeenCalledWith(jasmine.hashContaining({z:"x"})); + }); + }); + }); + describe("all matchers", function() { it("should return null, for future-proofing, since we might eventually allow matcher chaining", function() { expect(match(true).toBe(true)).toBeUndefined(); diff --git a/src/core/Env.js b/src/core/Env.js index 1ba67597..bf72c88c 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -238,6 +238,14 @@ jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { return b.matches(a); } + if (a instanceof jasmine.Matchers.HashContaining) { + return a.matches(b); + } + + if (b instanceof jasmine.Matchers.HashContaining) { + return b.matches(a); + } + if (jasmine.isString_(a) && jasmine.isString_(b)) { return (a == b); } diff --git a/src/core/Matchers.js b/src/core/Matchers.js index f3bf0579..122e32b4 100644 --- a/src/core/Matchers.js +++ b/src/core/Matchers.js @@ -369,3 +369,32 @@ jasmine.Matchers.Any.prototype.toString = function() { return ''; }; +jasmine.Matchers.HashContaining = function (sample) { + this.sample = sample; +}; + +jasmine.Matchers.HashContaining.prototype.matches = function(other, mismatchKeys, mismatchValues) { + mismatchKeys = mismatchKeys || []; + mismatchValues = mismatchValues || []; + + var env = jasmine.getEnv(); + + var hasKey = function(obj, keyName) { + return obj != null && obj[keyName] !== jasmine.undefined; + }; + + for (var property in this.sample) { + if (!hasKey(other, property) && hasKey(this.sample, property)) { + mismatchKeys.push("expected has key '" + property + "', but missing from actual."); + } + else if (!env.equals_(this.sample[property], other[property], mismatchKeys, mismatchValues)) { + mismatchValues.push("'" + property + "' was '" + (other[property] ? jasmine.util.htmlEscape(other[property].toString()) : other[property]) + "' in expected, but was '" + (this.sample[property] ? jasmine.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in actual."); + } + } + + return (mismatchKeys.length === 0 && mismatchValues.length === 0); +}; + +jasmine.Matchers.HashContaining.prototype.toString = function () { + return ""; +}; diff --git a/src/core/base.js b/src/core/base.js index bb0c5550..cf84c07d 100755 --- a/src/core/base.js +++ b/src/core/base.js @@ -196,6 +196,21 @@ jasmine.any = function(clazz) { return new jasmine.Matchers.Any(clazz); }; +/** + * Returns a matchable subset of a hash/JSON object. For use in expectations when you don't care about all of the + * attributes on the object. + * + * @example + * // don't care about any other attributes than foo. + * expect(mySpy).toHaveBeenCalledWith(jasmine.hashContaining({foo: "bar"}); + * + * @param sample {Object} sample + * @returns matchable object for the sample + */ +jasmine.hashContaining = function (sample) { + return new jasmine.Matchers.HashContaining(sample); +}; + /** * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. *