diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 8bba9262..f1099d17 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.objectContaining = function (sample) { + return new jasmine.Matchers.ObjectContaining(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.ObjectContaining) { + return a.matches(b); + } + + if (b instanceof jasmine.Matchers.ObjectContaining) { + 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.ObjectContaining = function (sample) { + this.sample = sample; +}; + +jasmine.Matchers.ObjectContaining.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.ObjectContaining.prototype.toString = function () { + return ""; +}; /** * @constructor */ @@ -2472,5 +2524,5 @@ jasmine.version_= { "major": 1, "minor": 1, "build": 0, - "revision": 1320442951 + "revision": 1299963843 }; diff --git a/pages b/pages index a9d577eb..d08ce2de 160000 --- a/pages +++ b/pages @@ -1 +1 @@ -Subproject commit a9d577eb45346c088c857af5bb301540c2a121f4 +Subproject commit d08ce2de245d6782c09c652045a12e93af0dc7ec diff --git a/spec/core/MatchersSpec.js b/spec/core/MatchersSpec.js index cb58208d..a8e965ac 100644 --- a/spec/core/MatchersSpec.js +++ b/spec/core/MatchersSpec.js @@ -352,6 +352,33 @@ describe("jasmine.Matchers", function() { }]).toEqual(["a", jasmine.any(Function)])).toPass(); }); + describe("toEqual with an object implementing jasmineMatches", function () { + var matcher; + beforeEach(function () { + matcher = { + jasmineMatches: jasmine.createSpy("jasmineMatches") + }; + }); + + describe("on the left side", function () { + it("uses the jasmineMatches function", function () { + matcher.jasmineMatches.andReturn(false); + expect(match(matcher).toEqual("foo")).toFail(); + matcher.jasmineMatches.andReturn(true); + expect(match(matcher).toEqual("foo")).toPass(); + }); + }); + + describe("on the right side", function () { + it("uses the jasmineMatches function", function () { + matcher.jasmineMatches.andReturn(false); + expect(match("foo").toEqual(matcher)).toFail(); + matcher.jasmineMatches.andReturn(true); + expect(match("foo").toEqual(matcher)).toPass(); + }); + }); + }); + it("toEqual handles circular objects ok", function() { expect(match({foo: "bar", baz: jasmine.undefined}).toEqual({foo: "bar", baz: jasmine.undefined})).toPass(); expect(match({foo:['bar','baz','quux']}).toEqual({foo:['bar','baz','quux']})).toPass(); @@ -830,6 +857,252 @@ describe("jasmine.Matchers", function() { }); }); + describe("ObjectContaining", function () { + describe("with an empty object", function () { + var containing; + beforeEach(function () { + containing = new jasmine.Matchers.ObjectContaining({}); + }); + it("matches everything", function () { + expect(containing.jasmineMatches("foo", [], [])).toBe(true); + }); + + it("says it didn't expect to contain anything", function () { + expect(containing.jasmineToString()).toEqual(""); + }); + }); + + describe("with an object with items in it", function () { + var containing, mismatchKeys, mismatchValues; + beforeEach(function () { + mismatchKeys = []; + mismatchValues = []; + containing = new jasmine.Matchers.ObjectContaining({foo: "fooVal", bar: "barVal"}); + }); + + it("doesn't match an empty object", function () { + expect(containing.jasmineMatches({}, mismatchKeys, mismatchValues)).toBe(false); + }); + + it("doesn't match an object with none of the specified options", function () { + expect(containing.jasmineMatches({baz:"stuff"}, mismatchKeys, mismatchValues)).toBe(false); + }); + + it("adds a message for each missing key", function () { + containing.jasmineMatches({foo: "fooVal"}, mismatchKeys, mismatchValues); + expect(mismatchKeys.length).toEqual(1); + }); + + it("doesn't match an object when the values are different", function () { + expect(containing.jasmineMatches({foo:"notFoo", bar:"notBar"}, mismatchKeys, mismatchValues)).toBe(false); + }); + + it("adds a message when values don't match", function () { + containing.jasmineMatches({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.jasmineMatches({foo:"notFoo", bar:"barVal"}, mismatchKeys, mismatchValues)).toBe(false); + }); + + it("matches when all the values are the same", function () { + expect(containing.jasmineMatches({foo: "fooVal", bar: "barVal"}, mismatchKeys, mismatchValues)).toBe(true); + }); + + it("matches when there are additional values", function () { + expect(containing.jasmineMatches({foo: "fooVal", bar: "barVal", baz: "bazVal"}, mismatchKeys, mismatchValues)).toBe(true); + }); + + it("doesn't modify missingKeys or missingValues when match is successful", function () { + containing.jasmineMatches({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.jasmineToString()).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.objectContaining({a:"b"})); + }); + + it("works correctly for negative matches", function () { + expect(method).not.toHaveBeenCalledWith(jasmine.objectContaining({z:"x"})); + }); + }); + }); + + describe("Matchers.Any", function () { + var any; + describe(".jasmineToString", function () { + describe("with Object", function () { + it("says it's looking for an object", function () { + any = jasmine.any(Object); + expect(any.jasmineToString()).toMatch(//); + }); + }); + + describe("with Function", function () { + it("says it's looking for a function", function () { + any = jasmine.any(Function); + expect(any.jasmineToString()).toMatch(//); + }); + }); + + describe("with String", function () { + it("says it's looking for a string", function () { + any = jasmine.any(String); + expect(any.jasmineToString()).toMatch(//); + }); + }); + + describe("with Number", function () { + it("says it's looking for a number", function () { + any = jasmine.any(Number); + expect(any.jasmineToString()).toMatch(//); + }); + }); + + describe("with some other defined 'class'", function () { + it("says it's looking for an object", function () { + function MyClass () {} + any = jasmine.any(MyClass); + expect(any.jasmineToString()).toMatch(//); + }); + }); + }); + + describe(".jasmineMatches", function () { + describe("with Object", function () { + beforeEach(function () { + any = jasmine.any(Object); + }); + + it("matches an empty object", function () { + expect(any.jasmineMatches({})).toEqual(true); + }); + + it("matches a newed up object", function () { + expect(any.jasmineMatches(new Object())).toEqual(true); + }); + + it("doesn't match a string", function () { + expect(any.jasmineMatches("")).toEqual(false); + }); + + it("doesn't match a number", function () { + expect(any.jasmineMatches(123)).toEqual(false); + }); + + it("doesn't match a function", function () { + expect(any.jasmineMatches(function () {})).toEqual(false); + }); + }); + + describe("with Function", function () { + beforeEach(function () { + any = jasmine.any(Function); + }); + + it("doesn't match an object", function () { + expect(any.jasmineMatches({})).toEqual(false); + }); + + it("doesn't match a string", function () { + expect(any.jasmineMatches("")).toEqual(false); + }); + + it("doesn't match a number", function () { + expect(any.jasmineMatches(123)).toEqual(false); + }); + + it("matches a function", function () { + expect(any.jasmineMatches(function () {})).toEqual(true); + }); + }); + + describe("with Number", function () { + beforeEach(function () { + any = jasmine.any(Number); + }); + + it("doesn't match an object", function () { + expect(any.jasmineMatches({})).toEqual(false); + }); + + it("doesn't match a string", function () { + expect(any.jasmineMatches("")).toEqual(false); + }); + + it("matches a number", function () { + expect(any.jasmineMatches(123)).toEqual(true); + }); + + it("doesn't match a function", function () { + expect(any.jasmineMatches(function () {})).toEqual(false); + }); + }); + + describe("with String", function () { + beforeEach(function () { + any = jasmine.any(String); + }); + + it("doesn't match an object", function () { + expect(any.jasmineMatches({})).toEqual(false); + }); + + it("matches a string", function () { + expect(any.jasmineMatches("")).toEqual(true); + }); + + it("doesn't match a number", function () { + expect(any.jasmineMatches(123)).toEqual(false); + }); + + it("doesn't match a function", function () { + expect(any.jasmineMatches(function () {})).toEqual(false); + }); + }); + + describe("with some defined 'class'", function () { + function MyClass () {} + beforeEach(function () { + any = jasmine.any(MyClass); + }); + + it("doesn't match an object", function () { + expect(any.jasmineMatches({})).toEqual(false); + }); + + it("doesn't match a string", function () { + expect(any.jasmineMatches("")).toEqual(false); + }); + + it("doesn't match a number", function () { + expect(any.jasmineMatches(123)).toEqual(false); + }); + + it("doesn't match a function", function () { + expect(any.jasmineMatches(function () {})).toEqual(false); + }); + + it("matches an instance of the defined class", function () { + expect(any.jasmineMatches(new MyClass())).toEqual(true); + }); + }); + }); + }); + 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/spec/core/PrettyPrintSpec.js b/spec/core/PrettyPrintSpec.js index 37f71041..becf8a61 100644 --- a/spec/core/PrettyPrintSpec.js +++ b/spec/core/PrettyPrintSpec.js @@ -83,5 +83,12 @@ describe("jasmine.pp", function () { expect(jasmine.pp(jasmine.createSpy("something"))).toEqual("spy on something"); }); + it("should stringify objects that implement jasmineToString", function () { + var obj = { + jasmineToString: function () { return "strung"; } + }; + + expect(jasmine.pp(obj)).toEqual("strung"); + }); }); diff --git a/src/core/Env.js b/src/core/Env.js index 1ba67597..4ebc0963 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -230,11 +230,19 @@ jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { return a.getTime() == b.getTime(); } - if (a instanceof jasmine.Matchers.Any) { + if (a.jasmineMatches) { + return a.jasmineMatches(b); + } + + if (b.jasmineMatches) { + return b.jasmineMatches(a); + } + + if (a instanceof jasmine.Matchers.ObjectContaining) { return a.matches(b); } - if (b instanceof jasmine.Matchers.Any) { + if (b instanceof jasmine.Matchers.ObjectContaining) { return b.matches(a); } diff --git a/src/core/Matchers.js b/src/core/Matchers.js index f3bf0579..4f078f3b 100644 --- a/src/core/Matchers.js +++ b/src/core/Matchers.js @@ -345,7 +345,7 @@ jasmine.Matchers.Any = function(expectedClass) { this.expectedClass = expectedClass; }; -jasmine.Matchers.Any.prototype.matches = function(other) { +jasmine.Matchers.Any.prototype.jasmineMatches = function(other) { if (this.expectedClass == String) { return typeof other == 'string' || other instanceof String; } @@ -365,7 +365,36 @@ jasmine.Matchers.Any.prototype.matches = function(other) { return other instanceof this.expectedClass; }; -jasmine.Matchers.Any.prototype.toString = function() { +jasmine.Matchers.Any.prototype.jasmineToString = function() { return ''; }; +jasmine.Matchers.ObjectContaining = function (sample) { + this.sample = sample; +}; + +jasmine.Matchers.ObjectContaining.prototype.jasmineMatches = 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.ObjectContaining.prototype.jasmineToString = function () { + return ""; +}; diff --git a/src/core/PrettyPrinter.js b/src/core/PrettyPrinter.js index 7d890c02..a7d283b6 100644 --- a/src/core/PrettyPrinter.js +++ b/src/core/PrettyPrinter.js @@ -23,8 +23,8 @@ jasmine.PrettyPrinter.prototype.format = function(value) { this.emitScalar('null'); } else if (value === jasmine.getGlobal()) { this.emitScalar(''); - } else if (value instanceof jasmine.Matchers.Any) { - this.emitScalar(value.toString()); + } else if (value.jasmineToString) { + this.emitScalar(value.jasmineToString()); } else if (typeof value === 'string') { this.emitString(value); } else if (jasmine.isSpy(value)) { diff --git a/src/core/base.js b/src/core/base.js index bb0c5550..3a61b3d8 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 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.objectContaining({foo: "bar"}); + * + * @param sample {Object} sample + * @returns matchable object for the sample + */ +jasmine.objectContaining = function (sample) { + return new jasmine.Matchers.ObjectContaining(sample); +}; + /** * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. * diff --git a/tasks/spec.rb b/tasks/spec.rb index 30ef2f7b..2760e00d 100644 --- a/tasks/spec.rb +++ b/tasks/spec.rb @@ -31,7 +31,7 @@ end def count_specs_in(files) files.inject(0) do |count, file| - File.read(file).scan(/\sit\(/) {|s| count += 1} + File.read(file).scan(/\sit\s*\(/) {|s| count += 1} count end end