diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js
index 3577abe5..c462536f 100644
--- a/lib/jasmine-core/jasmine.js
+++ b/lib/jasmine-core/jasmine.js
@@ -76,6 +76,9 @@ var getJasmineRequireObj = (function (jasmineGlobal) {
j$.TreeProcessor = jRequire.TreeProcessor();
j$.version = jRequire.version();
j$.Order = jRequire.Order();
+ j$.DiffBuilder = jRequire.DiffBuilder(j$);
+ j$.NullDiffBuilder = jRequire.NullDiffBuilder(j$);
+ j$.ObjectPath = jRequire.ObjectPath(j$);
j$.matchers = jRequire.requireMatchers(jRequire, j$);
@@ -309,6 +312,22 @@ getJasmineRequireObj().util = function() {
return descriptor;
};
+ util.objectDifference = function(obj, toRemove) {
+ var diff = {};
+
+ for (var key in obj) {
+ if (util.has(obj, key) && !util.has(toRemove, key)) {
+ diff[key] = obj[key];
+ }
+ }
+
+ return diff;
+ };
+
+ util.has = function(obj, key) {
+ return Object.prototype.hasOwnProperty.call(obj, key);
+ };
+
return util;
};
@@ -1918,7 +1937,13 @@ getJasmineRequireObj().pp = function(j$) {
};
StringPrettyPrinter.prototype.emitObject = function(obj) {
- var constructorName = obj.constructor ? j$.fnNameFor(obj.constructor) : 'null';
+ var ctor = obj.constructor,
+ constructorName;
+
+ constructorName = typeof ctor === 'function' && obj instanceof ctor ?
+ j$.fnNameFor(obj.constructor) :
+ 'null';
+
this.append(constructorName);
if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) {
@@ -2961,15 +2986,104 @@ getJasmineRequireObj().formatErrorMsg = function() {
return generateErrorMsg;
};
+getJasmineRequireObj().DiffBuilder = function(j$) {
+ return function DiffBuilder() {
+ var path = new j$.ObjectPath(),
+ mismatches = [];
+
+ return {
+ record: function (actual, expected, formatter) {
+ formatter = formatter || defaultFormatter;
+ mismatches.push(formatter(actual, expected, path));
+ },
+
+ getMessage: function () {
+ return mismatches.join('\n');
+ },
+
+ withPath: function (pathComponent, block) {
+ var oldPath = path;
+ path = path.add(pathComponent);
+ block();
+ path = oldPath;
+ }
+ };
+
+ function defaultFormatter (actual, expected, path) {
+ return 'Expected ' +
+ path + (path.depth() ? ' = ' : '') +
+ j$.pp(actual) +
+ ' to equal ' +
+ j$.pp(expected) +
+ '.';
+ }
+ };
+};
+
+getJasmineRequireObj().NullDiffBuilder = function(j$) {
+ return function() {
+ return {
+ withPath: function(_, block) {
+ block();
+ },
+ record: function() {}
+ };
+ };
+};
+
+getJasmineRequireObj().ObjectPath = function(j$) {
+ function ObjectPath(components) {
+ this.components = components || [];
+ }
+
+ ObjectPath.prototype.toString = function() {
+ if (this.components.length) {
+ return '$' + map(this.components, formatPropertyAccess).join('');
+ } else {
+ return '';
+ }
+ };
+
+ ObjectPath.prototype.add = function(component) {
+ return new ObjectPath(this.components.concat([component]));
+ };
+
+ ObjectPath.prototype.depth = function() {
+ return this.components.length;
+ };
+
+ function formatPropertyAccess(prop) {
+ if (typeof prop === 'number') {
+ return '[' + prop + ']';
+ }
+
+ if (isValidIdentifier(prop)) {
+ return '.' + prop;
+ }
+
+ return '[\'' + prop + '\']';
+ }
+
+ function map(array, fn) {
+ var results = [];
+ for (var i = 0; i < array.length; i++) {
+ results.push(fn(array[i]));
+ }
+ return results;
+ }
+
+ function isValidIdentifier(string) {
+ return /^[A-Za-z\$_][A-Za-z0-9\$_]*$/.test(string);
+ }
+
+ return ObjectPath;
+};
+
getJasmineRequireObj().matchersUtil = function(j$) {
// TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter?
return {
- equals: function(a, b, customTesters) {
- customTesters = customTesters || [];
-
- return eq(a, b, [], [], customTesters);
- },
+ equals: equals,
contains: function(haystack, needle, customTesters) {
customTesters = customTesters || [];
@@ -2982,7 +3096,7 @@ getJasmineRequireObj().matchersUtil = function(j$) {
(!!haystack && !haystack.indexOf))
{
for (var i = 0; i < haystack.length; i++) {
- if (eq(haystack[i], needle, [], [], customTesters)) {
+ if (equals(haystack[i], needle, customTesters)) {
return true;
}
}
@@ -3022,67 +3136,113 @@ getJasmineRequireObj().matchersUtil = function(j$) {
return obj && j$.isA_('Function', obj.asymmetricMatch);
}
- function asymmetricMatch(a, b, customTesters) {
+ function asymmetricMatch(a, b, customTesters, diffBuilder) {
var asymmetricA = isAsymmetric(a),
- asymmetricB = isAsymmetric(b);
+ asymmetricB = isAsymmetric(b),
+ result;
if (asymmetricA && asymmetricB) {
return undefined;
}
if (asymmetricA) {
- return a.asymmetricMatch(b, customTesters);
+ result = a.asymmetricMatch(b, customTesters);
+ diffBuilder.record(a, b);
+ return result;
}
if (asymmetricB) {
- return b.asymmetricMatch(a, customTesters);
+ result = b.asymmetricMatch(a, customTesters);
+ diffBuilder.record(a, b);
+ return result;
}
}
+ function equals(a, b, customTesters, diffBuilder) {
+ customTesters = customTesters || [];
+ diffBuilder = diffBuilder || j$.NullDiffBuilder();
+
+ return eq(a, b, [], [], customTesters, diffBuilder);
+ }
+
// Equality function lovingly adapted from isEqual in
// [Underscore](http://underscorejs.org)
- function eq(a, b, aStack, bStack, customTesters) {
- var result = true;
+ function eq(a, b, aStack, bStack, customTesters, diffBuilder) {
+ var result = true, i;
- var asymmetricResult = asymmetricMatch(a, b, customTesters);
+ var asymmetricResult = asymmetricMatch(a, b, customTesters, diffBuilder);
if (!j$.util.isUndefined(asymmetricResult)) {
return asymmetricResult;
}
- for (var i = 0; i < customTesters.length; i++) {
+ for (i = 0; i < customTesters.length; i++) {
var customTesterResult = customTesters[i](a, b);
if (!j$.util.isUndefined(customTesterResult)) {
+ if (!customTesterResult) {
+ diffBuilder.record(a, b);
+ }
return customTesterResult;
}
}
if (a instanceof Error && b instanceof Error) {
- return a.message == b.message;
+ result = a.message == b.message;
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
}
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
- if (a === b) { return a !== 0 || 1 / a == 1 / b; }
+ if (a === b) {
+ result = a !== 0 || 1 / a == 1 / b;
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
+ }
// A strict comparison is necessary because `null == undefined`.
- if (a === null || b === null) { return a === b; }
+ if (a === null || b === null) {
+ result = a === b;
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
+ }
var className = Object.prototype.toString.call(a);
- if (className != Object.prototype.toString.call(b)) { return false; }
+ if (className != Object.prototype.toString.call(b)) {
+ diffBuilder.record(a, b);
+ return false;
+ }
switch (className) {
// Strings, numbers, dates, and booleans are compared by value.
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
- return a == String(b);
+ result = a == String(b);
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
// other numeric values.
- return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b);
+ result = a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b);
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
- return +a == +b;
+ result = +a == +b;
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
// RegExps are compared by their source patterns and flags.
case '[object RegExp]':
return a.source == b.source &&
@@ -3090,14 +3250,21 @@ getJasmineRequireObj().matchersUtil = function(j$) {
a.multiline == b.multiline &&
a.ignoreCase == b.ignoreCase;
}
- if (typeof a != 'object' || typeof b != 'object') { return false; }
+ if (typeof a != 'object' || typeof b != 'object') {
+ diffBuilder.record(a, b);
+ return false;
+ }
var aIsDomNode = j$.isDomNode(a);
var bIsDomNode = j$.isDomNode(b);
if (aIsDomNode && bIsDomNode) {
// At first try to use DOM3 method isEqualNode
if (a.isEqualNode) {
- return a.isEqualNode(b);
+ result = a.isEqualNode(b);
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
}
// IE8 doesn't support isEqualNode, try to use outerHTML && innerText
var aIsElement = a instanceof Element;
@@ -3131,17 +3298,21 @@ getJasmineRequireObj().matchersUtil = function(j$) {
if (className == '[object Array]') {
size = a.length;
if (size !== b.length) {
+ diffBuilder.record(a, b);
return false;
}
- while (size--) {
- result = eq(a[size], b[size], aStack, bStack, customTesters);
- if (!result) {
- return false;
- }
+ for (i = 0; i < size; i++) {
+ diffBuilder.withPath(i, function() {
+ result = eq(a[i], b[i], aStack, bStack, customTesters, diffBuilder) && result;
+ });
+ }
+ if (!result) {
+ return false;
}
} else if (className == '[object Set]') {
if (a.size != b.size) {
+ diffBuilder.record(a, b);
return false;
}
var iterA = a.values(), iterB = b.values();
@@ -3149,7 +3320,8 @@ getJasmineRequireObj().matchersUtil = function(j$) {
do {
valA = iterA.next();
valB = iterB.next();
- if (!eq(valA.value, valB.value, aStack, bStack, customTesters)) {
+ if (!eq(valA.value, valB.value, aStack, bStack, customTesters, j$.NullDiffBuilder())) {
+ diffBuilder.record(a, b);
return false;
}
} while (!valA.done && !valB.done);
@@ -3158,8 +3330,12 @@ getJasmineRequireObj().matchersUtil = function(j$) {
// Objects with different constructors are not equivalent, but `Object`s
// or `Array`s from different frames are.
var aCtor = a.constructor, bCtor = b.constructor;
- if (aCtor !== bCtor && !(isObjectConstructor(aCtor) &&
- isObjectConstructor(bCtor))) {
+ if (aCtor !== bCtor &&
+ isFunction(aCtor) && isFunction(bCtor) &&
+ a instanceof aCtor && b instanceof bCtor &&
+ !(aCtor instanceof aCtor && bCtor instanceof bCtor)) {
+
+ diffBuilder.record(a, b, constructorsAreDifferentFormatter);
return false;
}
}
@@ -3169,52 +3345,66 @@ getJasmineRequireObj().matchersUtil = function(j$) {
size = aKeys.length;
// Ensure that both objects contain the same number of properties before comparing deep equality.
- if (keys(b, className == '[object Array]').length !== size) { return false; }
-
- while (size--) {
- key = aKeys[size];
- // Deep compare each member
- result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters);
-
- if (!result) {
- return false;
- }
+ if (keys(b, className == '[object Array]').length !== size) {
+ diffBuilder.record(a, b, objectKeysAreDifferentFormatter);
+ return false;
}
+
+ for (i = 0; i < size; i++) {
+ key = aKeys[i];
+ // Deep compare each member
+ if (!j$.util.has(b, key)) {
+ diffBuilder.record(a, b, objectKeysAreDifferentFormatter);
+ result = false;
+ continue;
+ }
+
+ diffBuilder.withPath(key, function() {
+ if(!eq(a[key], b[key], aStack, bStack, customTesters, diffBuilder)) {
+ result = false;
+ }
+ });
+ }
+
+ if (!result) {
+ return false;
+ }
+
// Remove the first object from the stack of traversed objects.
aStack.pop();
bStack.pop();
return result;
+ }
- function keys(obj, isArray) {
- var allKeys = Object.keys ? Object.keys(obj) :
- (function(o) {
- var keys = [];
- for (var key in o) {
- if (has(o, key)) {
- keys.push(key);
- }
- }
- return keys;
- })(obj);
-
- if (!isArray) {
- return allKeys;
- }
-
- var extraKeys = [];
- if (allKeys.length === 0) {
- return allKeys;
- }
-
- for (var x = 0; x < allKeys.length; x++) {
- if (!allKeys[x].match(/^[0-9]+$/)) {
- extraKeys.push(allKeys[x]);
+ function keys(obj, isArray) {
+ var allKeys = Object.keys ? Object.keys(obj) :
+ (function(o) {
+ var keys = [];
+ for (var key in o) {
+ if (j$.util.has(o, key)) {
+ keys.push(key);
+ }
}
- }
+ return keys;
+ })(obj);
- return extraKeys;
+ if (!isArray) {
+ return allKeys;
}
+
+ if (allKeys.length === 0) {
+ return allKeys;
+ }
+
+ var extraKeys = [];
+ for (var i in allKeys) {
+ if (!allKeys[i].match(/^[0-9]+$/)) {
+ extraKeys.push(allKeys[i]);
+ }
+ }
+
+ return extraKeys;
}
function has(obj, key) {
@@ -3225,12 +3415,45 @@ getJasmineRequireObj().matchersUtil = function(j$) {
return typeof obj === 'function';
}
- function isObjectConstructor(ctor) {
- // aCtor instanceof aCtor is true for the Object and Function
- // constructors (since a constructor is-a Function and a function is-a
- // Object). We don't just compare ctor === Object because the constructor
- // might come from a different frame with different globals.
- return isFunction(ctor) && ctor instanceof ctor;
+ function objectKeysAreDifferentFormatter(actual, expected, path) {
+ var missingProperties = j$.util.objectDifference(expected, actual),
+ extraProperties = j$.util.objectDifference(actual, expected),
+ missingPropertiesMessage = formatKeyValuePairs(missingProperties),
+ extraPropertiesMessage = formatKeyValuePairs(extraProperties),
+ messages = [];
+
+ if (!path.depth()) {
+ path = 'object';
+ }
+
+ if (missingPropertiesMessage.length) {
+ messages.push('Expected ' + path + ' to have properties' + missingPropertiesMessage);
+ }
+
+ if (extraPropertiesMessage.length) {
+ messages.push('Expected ' + path + ' not to have properties' + extraPropertiesMessage);
+ }
+
+ return messages.join('\n');
+ }
+
+ function constructorsAreDifferentFormatter(actual, expected, path) {
+ if (!path.depth()) {
+ path = 'object';
+ }
+
+ return 'Expected ' +
+ path + ' to be a kind of ' +
+ expected.constructor.name +
+ ', but was ' + j$.pp(actual) + '.';
+ }
+
+ function formatKeyValuePairs(obj) {
+ var formatted = '';
+ for (var key in obj) {
+ formatted += '\n ' + key + ': ' + j$.pp(obj[key]);
+ }
+ return formatted;
}
};
@@ -3440,7 +3663,7 @@ getJasmineRequireObj().toContain = function() {
return toContain;
};
-getJasmineRequireObj().toEqual = function() {
+getJasmineRequireObj().toEqual = function(j$) {
function toEqual(util, customEqualityTesters) {
customEqualityTesters = customEqualityTesters || [];
@@ -3448,10 +3671,14 @@ getJasmineRequireObj().toEqual = function() {
return {
compare: function(actual, expected) {
var result = {
- pass: false
- };
+ pass: false
+ },
+ diffBuilder = j$.DiffBuilder();
- result.pass = util.equals(actual, expected, customEqualityTesters);
+ result.pass = util.equals(actual, expected, customEqualityTesters, diffBuilder);
+
+ // TODO: only set error message if test fails
+ result.message = diffBuilder.getMessage();
return result;
}
diff --git a/spec/core/PrettyPrintSpec.js b/spec/core/PrettyPrintSpec.js
index 301ba80e..3965ea73 100644
--- a/spec/core/PrettyPrintSpec.js
+++ b/spec/core/PrettyPrintSpec.js
@@ -99,6 +99,11 @@ describe("jasmineUnderTest.pp", function () {
}, bar: [1, 2, 3]})).toEqual("Object({ foo: Function, bar: [ 1, 2, 3 ] })");
});
+ it("should print 'null' as the constructor of an object with its own constructor property", function() {
+ expect(jasmineUnderTest.pp({constructor: function() {}})).toEqual("null({ constructor: Function })");
+ expect(jasmineUnderTest.pp({constructor: 'foo'})).toEqual("null({ constructor: 'foo' })");
+ });
+
it("should not include inherited properties when stringifying an object", function() {
var SomeClass = function SomeClass() {};
SomeClass.prototype.foo = "inherited foo";
@@ -164,7 +169,6 @@ describe("jasmineUnderTest.pp", function () {
}
});
-
it('should not do HTML escaping of strings', function() {
expect(jasmineUnderTest.pp('some html string &', false)).toEqual('\'some html string &\'');
});
diff --git a/spec/core/UtilSpec.js b/spec/core/UtilSpec.js
index e547edf1..faaca2c2 100644
--- a/spec/core/UtilSpec.js
+++ b/spec/core/UtilSpec.js
@@ -63,4 +63,37 @@ describe("jasmineUnderTest.util", function() {
expect(actual).toEqual(expected);
});
});
+
+ describe("objectDifference", function() {
+ it("given two objects A and B, returns the properties in A not present in B", function() {
+ var a = {
+ foo: 3,
+ bar: 4,
+ baz: 5
+ };
+
+ var b = {
+ bar: 6,
+ quux: 7
+ };
+
+ expect(jasmineUnderTest.util.objectDifference(a, b)).toEqual({foo: 3, baz: 5})
+ });
+
+ it("only looks at own properties of both objects", function() {
+ function Foo() {}
+
+ Foo.prototype.x = 1;
+ Foo.prototype.y = 2;
+
+ var a = new Foo();
+ a.x = 1;
+
+ var b = new Foo();
+ b.y = 2;
+
+ expect(jasmineUnderTest.util.objectDifference(a, b)).toEqual({x: 1});
+ expect(jasmineUnderTest.util.objectDifference(b, a)).toEqual({y: 2});
+ })
+ })
});
diff --git a/spec/core/integration/CustomMatchersSpec.js b/spec/core/integration/CustomMatchersSpec.js
index ad73bbb4..0b0ee92c 100644
--- a/spec/core/integration/CustomMatchersSpec.js
+++ b/spec/core/integration/CustomMatchersSpec.js
@@ -79,6 +79,30 @@ describe("Custom Matchers (Integration)", function() {
env.execute();
});
+ it("displays an appropriate failure message if a custom equality matcher fails", function(done) {
+ env.it("spec using custom equality matcher", function() {
+ var customEqualityFn = function(a, b) {
+ // "foo" is not equal to anything
+ if (a === 'foo' || b === 'foo') {
+ return false;
+ }
+ };
+
+ env.addCustomEqualityTester(customEqualityFn);
+ env.expect({foo: 'foo'}).toEqual({foo: 'foo'});
+ });
+
+ var specExpectations = function(result) {
+ expect(result.status).toEqual('failed');
+ expect(result.failedExpectations[0].message).toEqual(
+ "Expected $.foo = 'foo' to equal 'foo'."
+ );
+ };
+
+ env.addReporter({ specDone: specExpectations, jasmineDone: done });
+ env.execute();
+ });
+
it("uses the negative compare function for a negative comparison, if provided", function(done) {
env.it("spec with custom negative comparison matcher", function() {
env.addMatchers({
diff --git a/spec/core/matchers/DiffBuilderSpec.js b/spec/core/matchers/DiffBuilderSpec.js
new file mode 100644
index 00000000..2fe8f1bf
--- /dev/null
+++ b/spec/core/matchers/DiffBuilderSpec.js
@@ -0,0 +1,47 @@
+describe("DiffBuilder", function() {
+ it("records the actual and expected objects", function() {
+ var diffBuilder = jasmineUnderTest.DiffBuilder();
+ diffBuilder.record({x: 'actual'}, {x: 'expected'});
+
+ expect(diffBuilder.getMessage()).toEqual("Expected Object({ x: 'actual' }) to equal Object({ x: 'expected' }).");
+ });
+
+ it("prints the path at which the difference was found", function() {
+ var diffBuilder = jasmineUnderTest.DiffBuilder();
+
+ diffBuilder.withPath('foo', function() {
+ diffBuilder.record({x: 'actual'}, {x: 'expected'});
+ });
+
+ expect(diffBuilder.getMessage()).toEqual("Expected $.foo = Object({ x: 'actual' }) to equal Object({ x: 'expected' }).");
+ });
+
+ it("prints multiple messages, separated by newlines", function() {
+ var diffBuilder = jasmineUnderTest.DiffBuilder();
+
+ diffBuilder.withPath('foo', function() {
+ diffBuilder.record(1, 2);
+ });
+
+ var message =
+ "Expected $.foo = 1 to equal 2.\n" +
+ "Expected 3 to equal 4.";
+
+ diffBuilder.record(3, 4);
+ expect(diffBuilder.getMessage()).toEqual(message);
+ });
+
+ it("allows customization of the message", function() {
+ var diffBuilder = jasmineUnderTest.DiffBuilder();
+
+ function darthVaderFormatter(actual, expected, path) {
+ return "I find your lack of " + expected + " disturbing. (was " + actual + ", at " + path + ")"
+ }
+
+ diffBuilder.withPath('x', function() {
+ diffBuilder.record('bar', 'foo', darthVaderFormatter);
+ });
+
+ expect(diffBuilder.getMessage()).toEqual("I find your lack of foo disturbing. (was bar, at $.x)");
+ });
+});
diff --git a/spec/core/matchers/NullDiffBuilderSpec.js b/spec/core/matchers/NullDiffBuilderSpec.js
new file mode 100644
index 00000000..a9aac3db
--- /dev/null
+++ b/spec/core/matchers/NullDiffBuilderSpec.js
@@ -0,0 +1,13 @@
+describe('NullDiffBuilder', function() {
+ it('responds to withPath() by calling the passed function', function() {
+ var spy = jasmine.createSpy('callback');
+ jasmineUnderTest.NullDiffBuilder().withPath('does not matter', spy);
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('responds to record()', function() {
+ expect(function() {
+ jasmineUnderTest.NullDiffBuilder().record('does not matter');
+ }).not.toThrow();
+ })
+});
diff --git a/spec/core/matchers/ObjectPathSpec.js b/spec/core/matchers/ObjectPathSpec.js
new file mode 100644
index 00000000..88b023d1
--- /dev/null
+++ b/spec/core/matchers/ObjectPathSpec.js
@@ -0,0 +1,43 @@
+describe('ObjectPath', function() {
+ var ObjectPath = jasmineUnderTest.ObjectPath;
+
+ it('represents the path to a node in an object tree', function() {
+ expect(new ObjectPath(['foo', 'bar']).toString()).toEqual('$.foo.bar');
+ });
+
+ it('has a depth', function() {
+ expect(new ObjectPath().depth()).toEqual(0);
+ expect(new ObjectPath(['foo']).depth()).toEqual(1);
+ });
+
+ it('renders numbers as array access', function() {
+ expect(new ObjectPath(['foo', 0]).toString()).toEqual('$.foo[0]');
+ });
+
+ it('renders properties that are valid identifiers with dot notation', function() {
+ expect(new ObjectPath(['foo123']).toString()).toEqual('$.foo123');
+ expect(new ObjectPath(['x_y']).toString()).toEqual('$.x_y');
+ expect(new ObjectPath(['A$B']).toString()).toEqual('$.A$B');
+ });
+
+ it('renders properties with non-identifier-safe characters with square bracket notation', function() {
+ expect(new ObjectPath(['a b c']).toString()).toEqual("$['a b c']");
+ expect(new ObjectPath(['1hello']).toString()).toEqual("$['1hello']");
+ });
+
+ it('renders as the empty string when empty', function() {
+ expect(new ObjectPath().toString()).toEqual('');
+ });
+
+ it('stringifies properties that are not strings or numbers', function() {
+ expect(new ObjectPath([{}]).toString()).toEqual("$['[object Object]']");
+ });
+
+ it('can be created based on another path', function() {
+ var root = new ObjectPath();
+ var path = root.add('foo');
+
+ expect(path.toString()).toEqual('$.foo');
+ expect(root.toString()).toEqual('');
+ })
+});
diff --git a/spec/core/matchers/matchersUtilSpec.js b/spec/core/matchers/matchersUtilSpec.js
index d5c9c200..3a8054a7 100644
--- a/spec/core/matchers/matchersUtilSpec.js
+++ b/spec/core/matchers/matchersUtilSpec.js
@@ -61,10 +61,14 @@ describe("matchersUtil", function() {
expect(jasmineUnderTest.matchersUtil.equals(foo, [undefined])).toBe(true);
});
- it("fails for Arrays that are not equivalent", function() {
+ it("fails for Arrays that have different lengths", function() {
expect(jasmineUnderTest.matchersUtil.equals([1, 2], [1, 2, 3])).toBe(false);
});
+ it("fails for Arrays that have different elements", function() {
+ expect(jasmineUnderTest.matchersUtil.equals([1, 2, 3], [1, 5, 3])).toBe(false);
+ });
+
it("fails for Arrays whose contents are equivalent, but have differing properties", function() {
var one = [1,2,3],
two = [1,2,3];
@@ -136,6 +140,13 @@ describe("matchersUtil", function() {
expect(jasmineUnderTest.matchersUtil.equals(actual, expected)).toBe(false);
});
+ it("fails for Objects that have the same number of keys, but different keys/values", function () {
+ var expected = { a: undefined },
+ actual = { b: 1 };
+
+ expect(jasmineUnderTest.matchersUtil.equals(actual, expected)).toBe(false);
+ })
+
it("fails when comparing an empty object to an empty array (issue #114)", function() {
var emptyObject = {},
emptyArray = [];
diff --git a/spec/core/matchers/toEqualSpec.js b/spec/core/matchers/toEqualSpec.js
index f038f9e9..2ac0a367 100644
--- a/spec/core/matchers/toEqualSpec.js
+++ b/spec/core/matchers/toEqualSpec.js
@@ -1,20 +1,39 @@
describe("toEqual", function() {
+ "use strict";
+
+ function compareEquals(actual, expected) {
+ var util = jasmineUnderTest.matchersUtil,
+ matcher = jasmineUnderTest.matchers.toEqual(util);
+
+ var result = matcher.compare(actual, expected);
+
+ return result;
+ }
+
it("delegates to equals function", function() {
var util = {
- equals: jasmine.createSpy('delegated-equals').and.returnValue(true)
+ equals: jasmine.createSpy('delegated-equals').and.returnValue(true),
+ buildFailureMessage: function() {
+ return 'does not matter'
+ },
+ DiffBuilder: jasmineUnderTest.matchersUtil.DiffBuilder
},
matcher = jasmineUnderTest.matchers.toEqual(util),
result;
result = matcher.compare(1, 1);
- expect(util.equals).toHaveBeenCalledWith(1, 1, []);
+ expect(util.equals).toHaveBeenCalledWith(1, 1, [], jasmine.anything());
expect(result.pass).toBe(true);
});
it("delegates custom equality testers, if present", function() {
var util = {
- equals: jasmine.createSpy('delegated-equals').and.returnValue(true)
+ equals: jasmine.createSpy('delegated-equals').and.returnValue(true),
+ buildFailureMessage: function() {
+ return 'does not matter'
+ },
+ DiffBuilder: jasmineUnderTest.matchersUtil.DiffBuilder
},
customEqualityTesters = ['a', 'b'],
matcher = jasmineUnderTest.matchers.toEqual(util, customEqualityTesters),
@@ -22,7 +41,457 @@ describe("toEqual", function() {
result = matcher.compare(1, 1);
- expect(util.equals).toHaveBeenCalledWith(1, 1, ['a', 'b']);
+ expect(util.equals).toHaveBeenCalledWith(1, 1, ['a', 'b'], jasmine.anything());
expect(result.pass).toBe(true);
});
+
+ it("reports the difference between objects that are not equal", function() {
+ var actual = {x: 1, y: 3},
+ expected = {x: 2, y: 3},
+ message = "Expected $.x = 1 to equal 2.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports the difference between nested objects that are not equal", function() {
+ var actual = {x: {y: 1}},
+ expected = {x: {y: 2}},
+ message = "Expected $.x.y = 1 to equal 2.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("formats property access so that it's valid JavaScript", function() {
+ var actual = {'my prop': 1},
+ expected = {'my prop': 2},
+ message = "Expected $['my prop'] = 1 to equal 2.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports missing properties", function() {
+ var actual = {x: {}},
+ expected = {x: {y: 1}},
+ message =
+ "Expected $.x to have properties\n" +
+ " y: 1";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports extra properties", function() {
+ var actual = {x: {y: 1, z: 2}},
+ expected = {x: {}},
+ message =
+ "Expected $.x not to have properties\n" +
+ " y: 1\n" +
+ " z: 2";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("pretty-prints properties", function() {
+ var actual = {x: {y: 'foo bar'}},
+ expected = {x: {}},
+ message =
+ "Expected $.x not to have properties\n" +
+ " y: 'foo bar'"
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports extra and missing properties together", function() {
+ var actual = {x: {y: 1, z: 2, f: 4}},
+ expected = {x: {y: 1, z: 2, g: 3}},
+ message =
+ "Expected $.x to have properties\n" +
+ " g: 3\n" +
+ "Expected $.x not to have properties\n" +
+ " f: 4";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports extra and missing properties of the root-level object", function() {
+ var actual = {x: 1},
+ expected = {a: 1},
+ message =
+ "Expected object to have properties\n" +
+ " a: 1\n" +
+ "Expected object not to have properties\n" +
+ " x: 1";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports multiple incorrect values", function() {
+ var actual = {x: 1, y: 2},
+ expected = {x: 3, y: 4},
+ message =
+ "Expected $.x = 1 to equal 3.\n" +
+ "Expected $.y = 2 to equal 4.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatch between actual child object and expected child number", function() {
+ var actual = {x: {y: 2}},
+ expected = {x: 1},
+ message = "Expected $.x = Object({ y: 2 }) to equal 1.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("uses the default failure message if actual is not an object", function() {
+ var actual = 1,
+ expected = {x: {}},
+ message = "Expected 1 to equal Object({ x: Object({ }) }).";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("uses the default failure message if expected is not an object", function() {
+ var actual = {x: {}},
+ expected = 1,
+ message = "Expected Object({ x: Object({ }) }) to equal 1.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("uses the default failure message given arrays with different lengths", function() {
+ var actual = [1, 2],
+ expected = [1, 2, 3],
+ message =
+ "Expected [ 1, 2 ] to equal [ 1, 2, 3 ].";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports a mismatch between elements of equal-length arrays", function() {
+ var actual = [1, 2, 5],
+ expected = [1, 2, 3],
+ message = "Expected $[2] = 5 to equal 3.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports a mismatch between multiple array elements", function() {
+ var actual = [2, 2, 5],
+ expected = [1, 2, 3],
+ message =
+ "Expected $[0] = 2 to equal 1.\n" +
+ "Expected $[2] = 5 to equal 3.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports a mismatch between properties of objects in arrays", function() {
+ var actual = [{x: 1}],
+ expected = [{x: 2}],
+ message = "Expected $[0].x = 1 to equal 2.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports a mismatch between arrays in objects", function() {
+ var actual = {x: [1]},
+ expected = {x: [2]},
+ message =
+ "Expected $.x[0] = 1 to equal 2.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches between nested arrays", function() {
+ var actual = [[1]],
+ expected = [[2]],
+ message =
+ "Expected $[0][0] = 1 to equal 2.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches involving NaN", function() {
+ var actual = {x: 0},
+ expected = {x: 0/0},
+ message = "Expected $.x = 0 to equal NaN.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches involving regular expressions", function() {
+ var actual = {x: '1'},
+ expected = {x: /1/},
+ message = "Expected $.x = '1' to equal /1/.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches involving infinities", function() {
+ var actual = {x: 0},
+ expected = {x: 1/0},
+ message = "Expected $.x = 0 to equal Infinity.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches involving booleans", function() {
+ var actual = {x: false},
+ expected = {x: true},
+ message = "Expected $.x = false to equal true.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches involving strings", function() {
+ var actual = {x: 'foo'},
+ expected = {x: 'bar'},
+ message = "Expected $.x = 'foo' to equal 'bar'.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches involving undefined", function() {
+ var actual = {x: void 0},
+ expected = {x: 0},
+ message = "Expected $.x = undefined to equal 0.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches involving null", function() {
+ var actual = {x: null},
+ expected = {x: 0},
+ message = "Expected $.x = null to equal 0.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches between objects with different constructors", function () {
+ function Foo() {}
+ function Bar() {}
+
+ var actual = {x: new Foo()},
+ expected = {x: new Bar()},
+ message = "Expected $.x to be a kind of Bar, but was Foo({ }).";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports type mismatches at the root level", function () {
+ function Foo() {}
+ function Bar() {}
+
+ var actual = new Foo(),
+ expected = new Bar(),
+ message = "Expected object to be a kind of Bar, but was Foo({ }).";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches between objects with their own constructor property", function () {
+ function Foo() {}
+ function Bar() {}
+
+ var actual = {x: {constructor: 'blerf'}},
+ expected = {x: {constructor: 'ftarrh'}},
+ message = "Expected $.x.constructor = 'blerf' to equal 'ftarrh'.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches between an object with a real constructor and one with its own constructor property", function () {
+ function Foo() {}
+ function Bar() {}
+
+ var actual = {x: {}},
+ expected = {x: {constructor: 'ftarrh'}},
+ message =
+ "Expected $.x to have properties\n" +
+ " constructor: 'ftarrh'";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ expect(compareEquals(expected, actual).message).toEqual(
+ "Expected $.x not to have properties\n constructor: 'ftarrh'"
+ );
+ });
+
+ it("reports mismatches between 0 and -0", function() {
+ var actual = {x: 0},
+ expected = {x: -0},
+ message = "Expected $.x = 0 to equal -0.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches between Errors", function() {
+ var actual = {x: new Error("the error you got")},
+ expected = {x: new Error("the error you want")},
+ message = "Expected $.x = Error: the error you got to equal Error: the error you want.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches between Functions", function() {
+ var actual = {x: function() {}},
+ expected = {x: function() {}},
+ message = "Expected $.x = Function to equal Function.";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("does not report deep mismatches within Sets", function() {
+ // TODO: implement deep comparison of set elements
+ jasmine.getEnv().requireFunctioningSets();
+
+ var actual = new Set([1]),
+ expected = new Set([2]),
+ message = 'Expected Set( 1 ) to equal Set( 2 ).';
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches between Sets nested in objects", function() {
+ jasmine.getEnv().requireFunctioningSets();
+
+ var actual = {sets: [new Set([1])]},
+ expected = {sets: [new Set([2])]},
+ message = "Expected $.sets[0] = Set( 1 ) to equal Set( 2 ).";
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports mismatches between Sets of different lengths", function() {
+ jasmine.getEnv().requireFunctioningSets();
+
+ var actual = new Set([1, 2]),
+ expected = new Set([2]),
+ message = 'Expected Set( 1, 2 ) to equal Set( 2 ).';
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ function isNotRunningInBrowser() {
+ return typeof document === 'undefined'
+ }
+
+ it("reports mismatches between DOM nodes with different tags", function() {
+ if(isNotRunningInBrowser()) {
+ return;
+ }
+
+ var actual = {a: document.createElement('div')},
+ expected = {a: document.createElement('p')},
+ message = 'Expected $.a = HTMLNode to equal HTMLNode.';
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it('reports mismatches between DOM nodes with different content', function() {
+ if(isNotRunningInBrowser()) {
+ return;
+ }
+
+ var nodeA = document.createElement('div'),
+ nodeB = document.createElement('div');
+
+ nodeA.innerText = 'foo'
+ nodeB.innerText = 'bar'
+
+ var actual = {a: nodeA},
+ expected = {a: nodeB},
+ message = 'Expected $.a = HTMLNode to equal HTMLNode.';
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ })
+
+ it("reports mismatches between a DOM node and a bare Object", function() {
+ if(isNotRunningInBrowser()) {
+ return;
+ }
+
+ var actual = {a: document.createElement('div')},
+ expected = {a: {}},
+ message = 'Expected $.a = HTMLNode to equal Object({ }).';
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ });
+
+ it("reports asymmetric mismatches", function() {
+ var actual = {a: 1},
+ expected = {a: jasmineUnderTest.any(String)},
+ message = 'Expected $.a = 1 to equal .';
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ expect(compareEquals(actual, expected).pass).toBe(false)
+ });
+
+ it("reports asymmetric mismatches when the asymmetric comparand is the actual value", function() {
+ var actual = {a: jasmineUnderTest.any(String)},
+ expected = {a: 1},
+ message = 'Expected $.a = to equal 1.';
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ expect(compareEquals(actual, expected).pass).toBe(false)
+ });
+
+ it("does not report a mismatch when asymmetric matchers are satisfied", function() {
+ var actual = {a: 'a'},
+ expected = {a: jasmineUnderTest.any(String)},
+ message = 'Expected $.a = 1 to equal .';
+
+ expect(compareEquals(actual, expected).pass).toBe(true)
+ });
+
+ it("works on big complex stuff", function() {
+ var actual = {
+ foo: [
+ {bar: 1, things: ['a', 'b']},
+ {bar: 2, things: ['a', 'b']}
+ ],
+ baz: [
+ {a: {b: 1}}
+ ],
+ quux: 1,
+ nan: 0,
+ aRegexp: 'hi',
+ inf: -1/0,
+ boolean: false,
+ notDefined: 0,
+ aNull: void 0
+ }
+
+ var expected = {
+ foo: [
+ {bar: 2, things: ['a', 'b', 'c']},
+ {bar: 2, things: ['a', 'd']}
+ ],
+ baz: [
+ {a: {b: 1, c: 1}}
+ ],
+ quux: [],
+ nan: 0/0,
+ aRegexp: /hi/,
+ inf: 1/0,
+ boolean: true,
+ notDefined: void 0,
+ aNull: null
+ }
+
+ var message =
+ 'Expected $.foo[0].bar = 1 to equal 2.\n' +
+ "Expected $.foo[0].things = [ 'a', 'b' ] to equal [ 'a', 'b', 'c' ].\n" +
+ "Expected $.foo[1].things[1] = 'b' to equal 'd'.\n" +
+ 'Expected $.baz[0].a to have properties\n' +
+ ' c: 1\n' +
+ 'Expected $.quux = 1 to equal [ ].\n' +
+ 'Expected $.nan = 0 to equal NaN.\n' +
+ "Expected $.aRegexp = 'hi' to equal /hi/.\n" +
+ 'Expected $.inf = -Infinity to equal Infinity.\n' +
+ 'Expected $.boolean = false to equal true.\n' +
+ 'Expected $.notDefined = 0 to equal undefined.\n' +
+ 'Expected $.aNull = undefined to equal null.'
+
+ expect(compareEquals(actual, expected).message).toEqual(message);
+ })
});
diff --git a/src/core/PrettyPrinter.js b/src/core/PrettyPrinter.js
index 4aebd96f..d945d0c7 100644
--- a/src/core/PrettyPrinter.js
+++ b/src/core/PrettyPrinter.js
@@ -145,7 +145,13 @@ getJasmineRequireObj().pp = function(j$) {
};
StringPrettyPrinter.prototype.emitObject = function(obj) {
- var constructorName = obj.constructor ? j$.fnNameFor(obj.constructor) : 'null';
+ var ctor = obj.constructor,
+ constructorName;
+
+ constructorName = typeof ctor === 'function' && obj instanceof ctor ?
+ j$.fnNameFor(obj.constructor) :
+ 'null';
+
this.append(constructorName);
if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) {
diff --git a/src/core/matchers/DiffBuilder.js b/src/core/matchers/DiffBuilder.js
new file mode 100644
index 00000000..131b5b92
--- /dev/null
+++ b/src/core/matchers/DiffBuilder.js
@@ -0,0 +1,33 @@
+getJasmineRequireObj().DiffBuilder = function(j$) {
+ return function DiffBuilder() {
+ var path = new j$.ObjectPath(),
+ mismatches = [];
+
+ return {
+ record: function (actual, expected, formatter) {
+ formatter = formatter || defaultFormatter;
+ mismatches.push(formatter(actual, expected, path));
+ },
+
+ getMessage: function () {
+ return mismatches.join('\n');
+ },
+
+ withPath: function (pathComponent, block) {
+ var oldPath = path;
+ path = path.add(pathComponent);
+ block();
+ path = oldPath;
+ }
+ };
+
+ function defaultFormatter (actual, expected, path) {
+ return 'Expected ' +
+ path + (path.depth() ? ' = ' : '') +
+ j$.pp(actual) +
+ ' to equal ' +
+ j$.pp(expected) +
+ '.';
+ }
+ };
+};
diff --git a/src/core/matchers/NullDiffBuilder.js b/src/core/matchers/NullDiffBuilder.js
new file mode 100644
index 00000000..de7d3464
--- /dev/null
+++ b/src/core/matchers/NullDiffBuilder.js
@@ -0,0 +1,10 @@
+getJasmineRequireObj().NullDiffBuilder = function(j$) {
+ return function() {
+ return {
+ withPath: function(_, block) {
+ block();
+ },
+ record: function() {}
+ };
+ };
+};
diff --git a/src/core/matchers/ObjectPath.js b/src/core/matchers/ObjectPath.js
new file mode 100644
index 00000000..cd1629e2
--- /dev/null
+++ b/src/core/matchers/ObjectPath.js
@@ -0,0 +1,47 @@
+getJasmineRequireObj().ObjectPath = function(j$) {
+ function ObjectPath(components) {
+ this.components = components || [];
+ }
+
+ ObjectPath.prototype.toString = function() {
+ if (this.components.length) {
+ return '$' + map(this.components, formatPropertyAccess).join('');
+ } else {
+ return '';
+ }
+ };
+
+ ObjectPath.prototype.add = function(component) {
+ return new ObjectPath(this.components.concat([component]));
+ };
+
+ ObjectPath.prototype.depth = function() {
+ return this.components.length;
+ };
+
+ function formatPropertyAccess(prop) {
+ if (typeof prop === 'number') {
+ return '[' + prop + ']';
+ }
+
+ if (isValidIdentifier(prop)) {
+ return '.' + prop;
+ }
+
+ return '[\'' + prop + '\']';
+ }
+
+ function map(array, fn) {
+ var results = [];
+ for (var i = 0; i < array.length; i++) {
+ results.push(fn(array[i]));
+ }
+ return results;
+ }
+
+ function isValidIdentifier(string) {
+ return /^[A-Za-z\$_][A-Za-z0-9\$_]*$/.test(string);
+ }
+
+ return ObjectPath;
+};
diff --git a/src/core/matchers/matchersUtil.js b/src/core/matchers/matchersUtil.js
index 52dfaece..5998c393 100644
--- a/src/core/matchers/matchersUtil.js
+++ b/src/core/matchers/matchersUtil.js
@@ -2,11 +2,7 @@ getJasmineRequireObj().matchersUtil = function(j$) {
// TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter?
return {
- equals: function(a, b, customTesters) {
- customTesters = customTesters || [];
-
- return eq(a, b, [], [], customTesters);
- },
+ equals: equals,
contains: function(haystack, needle, customTesters) {
customTesters = customTesters || [];
@@ -19,7 +15,7 @@ getJasmineRequireObj().matchersUtil = function(j$) {
(!!haystack && !haystack.indexOf))
{
for (var i = 0; i < haystack.length; i++) {
- if (eq(haystack[i], needle, [], [], customTesters)) {
+ if (equals(haystack[i], needle, customTesters)) {
return true;
}
}
@@ -59,67 +55,113 @@ getJasmineRequireObj().matchersUtil = function(j$) {
return obj && j$.isA_('Function', obj.asymmetricMatch);
}
- function asymmetricMatch(a, b, customTesters) {
+ function asymmetricMatch(a, b, customTesters, diffBuilder) {
var asymmetricA = isAsymmetric(a),
- asymmetricB = isAsymmetric(b);
+ asymmetricB = isAsymmetric(b),
+ result;
if (asymmetricA && asymmetricB) {
return undefined;
}
if (asymmetricA) {
- return a.asymmetricMatch(b, customTesters);
+ result = a.asymmetricMatch(b, customTesters);
+ diffBuilder.record(a, b);
+ return result;
}
if (asymmetricB) {
- return b.asymmetricMatch(a, customTesters);
+ result = b.asymmetricMatch(a, customTesters);
+ diffBuilder.record(a, b);
+ return result;
}
}
+ function equals(a, b, customTesters, diffBuilder) {
+ customTesters = customTesters || [];
+ diffBuilder = diffBuilder || j$.NullDiffBuilder();
+
+ return eq(a, b, [], [], customTesters, diffBuilder);
+ }
+
// Equality function lovingly adapted from isEqual in
// [Underscore](http://underscorejs.org)
- function eq(a, b, aStack, bStack, customTesters) {
- var result = true;
+ function eq(a, b, aStack, bStack, customTesters, diffBuilder) {
+ var result = true, i;
- var asymmetricResult = asymmetricMatch(a, b, customTesters);
+ var asymmetricResult = asymmetricMatch(a, b, customTesters, diffBuilder);
if (!j$.util.isUndefined(asymmetricResult)) {
return asymmetricResult;
}
- for (var i = 0; i < customTesters.length; i++) {
+ for (i = 0; i < customTesters.length; i++) {
var customTesterResult = customTesters[i](a, b);
if (!j$.util.isUndefined(customTesterResult)) {
+ if (!customTesterResult) {
+ diffBuilder.record(a, b);
+ }
return customTesterResult;
}
}
if (a instanceof Error && b instanceof Error) {
- return a.message == b.message;
+ result = a.message == b.message;
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
}
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
- if (a === b) { return a !== 0 || 1 / a == 1 / b; }
+ if (a === b) {
+ result = a !== 0 || 1 / a == 1 / b;
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
+ }
// A strict comparison is necessary because `null == undefined`.
- if (a === null || b === null) { return a === b; }
+ if (a === null || b === null) {
+ result = a === b;
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
+ }
var className = Object.prototype.toString.call(a);
- if (className != Object.prototype.toString.call(b)) { return false; }
+ if (className != Object.prototype.toString.call(b)) {
+ diffBuilder.record(a, b);
+ return false;
+ }
switch (className) {
// Strings, numbers, dates, and booleans are compared by value.
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
- return a == String(b);
+ result = a == String(b);
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
// other numeric values.
- return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b);
+ result = a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b);
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
- return +a == +b;
+ result = +a == +b;
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
// RegExps are compared by their source patterns and flags.
case '[object RegExp]':
return a.source == b.source &&
@@ -127,14 +169,21 @@ getJasmineRequireObj().matchersUtil = function(j$) {
a.multiline == b.multiline &&
a.ignoreCase == b.ignoreCase;
}
- if (typeof a != 'object' || typeof b != 'object') { return false; }
+ if (typeof a != 'object' || typeof b != 'object') {
+ diffBuilder.record(a, b);
+ return false;
+ }
var aIsDomNode = j$.isDomNode(a);
var bIsDomNode = j$.isDomNode(b);
if (aIsDomNode && bIsDomNode) {
// At first try to use DOM3 method isEqualNode
if (a.isEqualNode) {
- return a.isEqualNode(b);
+ result = a.isEqualNode(b);
+ if (!result) {
+ diffBuilder.record(a, b);
+ }
+ return result;
}
// IE8 doesn't support isEqualNode, try to use outerHTML && innerText
var aIsElement = a instanceof Element;
@@ -168,17 +217,21 @@ getJasmineRequireObj().matchersUtil = function(j$) {
if (className == '[object Array]') {
size = a.length;
if (size !== b.length) {
+ diffBuilder.record(a, b);
return false;
}
- while (size--) {
- result = eq(a[size], b[size], aStack, bStack, customTesters);
- if (!result) {
- return false;
- }
+ for (i = 0; i < size; i++) {
+ diffBuilder.withPath(i, function() {
+ result = eq(a[i], b[i], aStack, bStack, customTesters, diffBuilder) && result;
+ });
+ }
+ if (!result) {
+ return false;
}
} else if (className == '[object Set]') {
if (a.size != b.size) {
+ diffBuilder.record(a, b);
return false;
}
var iterA = a.values(), iterB = b.values();
@@ -186,7 +239,8 @@ getJasmineRequireObj().matchersUtil = function(j$) {
do {
valA = iterA.next();
valB = iterB.next();
- if (!eq(valA.value, valB.value, aStack, bStack, customTesters)) {
+ if (!eq(valA.value, valB.value, aStack, bStack, customTesters, j$.NullDiffBuilder())) {
+ diffBuilder.record(a, b);
return false;
}
} while (!valA.done && !valB.done);
@@ -195,8 +249,12 @@ getJasmineRequireObj().matchersUtil = function(j$) {
// Objects with different constructors are not equivalent, but `Object`s
// or `Array`s from different frames are.
var aCtor = a.constructor, bCtor = b.constructor;
- if (aCtor !== bCtor && !(isObjectConstructor(aCtor) &&
- isObjectConstructor(bCtor))) {
+ if (aCtor !== bCtor &&
+ isFunction(aCtor) && isFunction(bCtor) &&
+ a instanceof aCtor && b instanceof bCtor &&
+ !(aCtor instanceof aCtor && bCtor instanceof bCtor)) {
+
+ diffBuilder.record(a, b, constructorsAreDifferentFormatter);
return false;
}
}
@@ -206,52 +264,66 @@ getJasmineRequireObj().matchersUtil = function(j$) {
size = aKeys.length;
// Ensure that both objects contain the same number of properties before comparing deep equality.
- if (keys(b, className == '[object Array]').length !== size) { return false; }
-
- while (size--) {
- key = aKeys[size];
- // Deep compare each member
- result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters);
-
- if (!result) {
- return false;
- }
+ if (keys(b, className == '[object Array]').length !== size) {
+ diffBuilder.record(a, b, objectKeysAreDifferentFormatter);
+ return false;
}
+
+ for (i = 0; i < size; i++) {
+ key = aKeys[i];
+ // Deep compare each member
+ if (!j$.util.has(b, key)) {
+ diffBuilder.record(a, b, objectKeysAreDifferentFormatter);
+ result = false;
+ continue;
+ }
+
+ diffBuilder.withPath(key, function() {
+ if(!eq(a[key], b[key], aStack, bStack, customTesters, diffBuilder)) {
+ result = false;
+ }
+ });
+ }
+
+ if (!result) {
+ return false;
+ }
+
// Remove the first object from the stack of traversed objects.
aStack.pop();
bStack.pop();
return result;
+ }
- function keys(obj, isArray) {
- var allKeys = Object.keys ? Object.keys(obj) :
- (function(o) {
- var keys = [];
- for (var key in o) {
- if (has(o, key)) {
- keys.push(key);
- }
- }
- return keys;
- })(obj);
-
- if (!isArray) {
- return allKeys;
- }
-
- var extraKeys = [];
- if (allKeys.length === 0) {
- return allKeys;
- }
-
- for (var x = 0; x < allKeys.length; x++) {
- if (!allKeys[x].match(/^[0-9]+$/)) {
- extraKeys.push(allKeys[x]);
+ function keys(obj, isArray) {
+ var allKeys = Object.keys ? Object.keys(obj) :
+ (function(o) {
+ var keys = [];
+ for (var key in o) {
+ if (j$.util.has(o, key)) {
+ keys.push(key);
+ }
}
- }
+ return keys;
+ })(obj);
- return extraKeys;
+ if (!isArray) {
+ return allKeys;
}
+
+ if (allKeys.length === 0) {
+ return allKeys;
+ }
+
+ var extraKeys = [];
+ for (var i in allKeys) {
+ if (!allKeys[i].match(/^[0-9]+$/)) {
+ extraKeys.push(allKeys[i]);
+ }
+ }
+
+ return extraKeys;
}
function has(obj, key) {
@@ -262,11 +334,44 @@ getJasmineRequireObj().matchersUtil = function(j$) {
return typeof obj === 'function';
}
- function isObjectConstructor(ctor) {
- // aCtor instanceof aCtor is true for the Object and Function
- // constructors (since a constructor is-a Function and a function is-a
- // Object). We don't just compare ctor === Object because the constructor
- // might come from a different frame with different globals.
- return isFunction(ctor) && ctor instanceof ctor;
+ function objectKeysAreDifferentFormatter(actual, expected, path) {
+ var missingProperties = j$.util.objectDifference(expected, actual),
+ extraProperties = j$.util.objectDifference(actual, expected),
+ missingPropertiesMessage = formatKeyValuePairs(missingProperties),
+ extraPropertiesMessage = formatKeyValuePairs(extraProperties),
+ messages = [];
+
+ if (!path.depth()) {
+ path = 'object';
+ }
+
+ if (missingPropertiesMessage.length) {
+ messages.push('Expected ' + path + ' to have properties' + missingPropertiesMessage);
+ }
+
+ if (extraPropertiesMessage.length) {
+ messages.push('Expected ' + path + ' not to have properties' + extraPropertiesMessage);
+ }
+
+ return messages.join('\n');
+ }
+
+ function constructorsAreDifferentFormatter(actual, expected, path) {
+ if (!path.depth()) {
+ path = 'object';
+ }
+
+ return 'Expected ' +
+ path + ' to be a kind of ' +
+ expected.constructor.name +
+ ', but was ' + j$.pp(actual) + '.';
+ }
+
+ function formatKeyValuePairs(obj) {
+ var formatted = '';
+ for (var key in obj) {
+ formatted += '\n ' + key + ': ' + j$.pp(obj[key]);
+ }
+ return formatted;
}
};
diff --git a/src/core/matchers/toEqual.js b/src/core/matchers/toEqual.js
index 653541bc..e6236457 100644
--- a/src/core/matchers/toEqual.js
+++ b/src/core/matchers/toEqual.js
@@ -1,4 +1,4 @@
-getJasmineRequireObj().toEqual = function() {
+getJasmineRequireObj().toEqual = function(j$) {
function toEqual(util, customEqualityTesters) {
customEqualityTesters = customEqualityTesters || [];
@@ -6,10 +6,14 @@ getJasmineRequireObj().toEqual = function() {
return {
compare: function(actual, expected) {
var result = {
- pass: false
- };
+ pass: false
+ },
+ diffBuilder = j$.DiffBuilder();
- result.pass = util.equals(actual, expected, customEqualityTesters);
+ result.pass = util.equals(actual, expected, customEqualityTesters, diffBuilder);
+
+ // TODO: only set error message if test fails
+ result.message = diffBuilder.getMessage();
return result;
}
diff --git a/src/core/requireCore.js b/src/core/requireCore.js
index 50d07359..b82d1aa1 100644
--- a/src/core/requireCore.js
+++ b/src/core/requireCore.js
@@ -54,6 +54,9 @@ var getJasmineRequireObj = (function (jasmineGlobal) {
j$.TreeProcessor = jRequire.TreeProcessor();
j$.version = jRequire.version();
j$.Order = jRequire.Order();
+ j$.DiffBuilder = jRequire.DiffBuilder(j$);
+ j$.NullDiffBuilder = jRequire.NullDiffBuilder(j$);
+ j$.ObjectPath = jRequire.ObjectPath(j$);
j$.matchers = jRequire.requireMatchers(jRequire, j$);
diff --git a/src/core/util.js b/src/core/util.js
index 5b53bf8d..121e3746 100644
--- a/src/core/util.js
+++ b/src/core/util.js
@@ -67,5 +67,21 @@ getJasmineRequireObj().util = function() {
return descriptor;
};
+ util.objectDifference = function(obj, toRemove) {
+ var diff = {};
+
+ for (var key in obj) {
+ if (util.has(obj, key) && !util.has(toRemove, key)) {
+ diff[key] = obj[key];
+ }
+ }
+
+ return diff;
+ };
+
+ util.has = function(obj, key) {
+ return Object.prototype.hasOwnProperty.call(obj, key);
+ };
+
return util;
};