Use custom object formatters for any part of a diff, not just leaf nodes

This commit is contained in:
Steve Gravrock
2020-02-01 18:49:06 -08:00
committed by Steve Gravrock
parent 25816a6e77
commit 873d1c2945
15 changed files with 669 additions and 141 deletions

View File

@@ -1,5 +1,5 @@
/*
Copyright (c) 2008-2019 Pivotal Labs
Copyright (c) 2008-2020 Pivotal Labs
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -106,6 +106,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) {
j$.DiffBuilder = jRequire.DiffBuilder(j$);
j$.NullDiffBuilder = jRequire.NullDiffBuilder(j$);
j$.ObjectPath = jRequire.ObjectPath(j$);
j$.MismatchTree = jRequire.MismatchTree(j$);
j$.GlobalErrors = jRequire.GlobalErrors(j$);
j$.Truthy = jRequire.Truthy(j$);
@@ -4187,20 +4188,54 @@ getJasmineRequireObj().toBeResolvedTo = function(j$) {
};
};
getJasmineRequireObj().DiffBuilder = function(j$) {
getJasmineRequireObj().DiffBuilder = function (j$) {
return function DiffBuilder(config) {
var path = new j$.ObjectPath(),
mismatches = [],
prettyPrinter = (config || {}).prettyPrinter || j$.makePrettyPrinter();
var prettyPrinter = (config || {}).prettyPrinter || j$.makePrettyPrinter(),
mismatches = new j$.MismatchTree(),
path = new j$.ObjectPath(),
actualRoot = undefined,
expectedRoot = undefined;
return {
record: function (actual, expected, formatter) {
formatter = formatter || defaultFormatter;
mismatches.push(formatter(actual, expected, path, prettyPrinter));
setRoots: function (actual, expected) {
actualRoot = actual;
expectedRoot = expected;
},
recordMismatch: function (formatter) {
mismatches.add(path, formatter);
},
getMessage: function () {
return mismatches.join('\n');
var messages = [];
mismatches.traverse(function (path, isLeaf, formatter) {
var actualCustom, expectedCustom, useCustom,
actual = path.dereference(actualRoot),
expected = path.dereference(expectedRoot);
if (formatter) {
messages.push(formatter(actual, expected, path, prettyPrinter));
return true;
}
actualCustom = prettyPrinter.customFormat_(actual);
expectedCustom = prettyPrinter.customFormat_(expected);
useCustom = !(j$.util.isUndefined(actualCustom) && j$.util.isUndefined(expectedCustom));
if (useCustom) {
messages.push(wrapPrettyPrinted(actualCustom, expectedCustom, path));
return false; // don't recurse further
}
if (isLeaf) {
messages.push(defaultFormatter(actual, expected, path, prettyPrinter));
}
return true;
});
return messages.join('\n');
},
withPath: function (pathComponent, block) {
@@ -4211,12 +4246,16 @@ getJasmineRequireObj().DiffBuilder = function(j$) {
}
};
function defaultFormatter (actual, expected, path, prettyPrinter) {
function defaultFormatter(actual, expected, path, prettyPrinter) {
return wrapPrettyPrinted(prettyPrinter(actual), prettyPrinter(expected), path);
}
function wrapPrettyPrinted(actual, expected, path) {
return 'Expected ' +
path + (path.depth() ? ' = ' : '') +
prettyPrinter(actual) +
actual +
' to equal ' +
prettyPrinter(expected) +
expected +
'.';
}
};
@@ -4293,7 +4332,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
if (asymmetricA) {
result = a.asymmetricMatch(b, shim);
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
}
@@ -4301,7 +4340,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
if (asymmetricB) {
result = b.asymmetricMatch(a, shim);
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
}
@@ -4319,6 +4358,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
customTesters = customTesters || this.customTesters_;
diffBuilder = diffBuilder || j$.NullDiffBuilder();
diffBuilder.setRoots(a, b);
return this.eq_(a, b, [], [], customTesters, diffBuilder);
};
@@ -4337,7 +4377,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
var customTesterResult = customTesters[i](a, b);
if (!j$.util.isUndefined(customTesterResult)) {
if (!customTesterResult) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return customTesterResult;
}
@@ -4346,7 +4386,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
if (a instanceof Error && b instanceof Error) {
result = a.message == b.message;
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
}
@@ -4356,7 +4396,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
if (a === b) {
result = a !== 0 || 1 / a == 1 / b;
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
}
@@ -4364,13 +4404,13 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
if (a === null || b === null) {
result = a === b;
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
}
var className = Object.prototype.toString.call(a);
if (className != Object.prototype.toString.call(b)) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
switch (className) {
@@ -4380,7 +4420,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
// equivalent to `new String("5")`.
result = a == String(b);
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
case '[object Number]':
@@ -4388,7 +4428,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
// other numeric values.
result = a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b);
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
case '[object Date]':
@@ -4398,7 +4438,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
// of `NaN` are not equivalent.
result = +a == +b;
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
// RegExps are compared by their source patterns and flags.
@@ -4409,7 +4449,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
a.ignoreCase == b.ignoreCase;
}
if (typeof a != 'object' || typeof b != 'object') {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
@@ -4419,12 +4459,12 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
// At first try to use DOM3 method isEqualNode
result = a.isEqualNode(b);
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
}
if (aIsDomNode || bIsDomNode) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
@@ -4454,7 +4494,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
diffBuilder.withPath('length', function() {
if (aLength !== bLength) {
diffBuilder.record(aLength, bLength);
diffBuilder.recordMismatch();
result = false;
}
});
@@ -4462,7 +4502,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
for (i = 0; i < aLength || i < bLength; i++) {
diffBuilder.withPath(i, function() {
if (i >= bLength) {
diffBuilder.record(a[i], void 0, actualArrayIsLongerFormatter.bind(null, self.pp));
diffBuilder.recordMismatch(actualArrayIsLongerFormatter.bind(null, self.pp));
result = false;
} else {
result = self.eq_(i < aLength ? a[i] : void 0, i < bLength ? b[i] : void 0, aStack, bStack, customTesters, diffBuilder) && result;
@@ -4474,7 +4514,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
}
} else if (j$.isMap(a) && j$.isMap(b)) {
if (a.size != b.size) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
@@ -4516,12 +4556,12 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
}
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
} else if (j$.isSet(a) && j$.isSet(b)) {
if (a.size != b.size) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
@@ -4566,7 +4606,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
}
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
} else {
@@ -4579,7 +4619,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
a instanceof aCtor && b instanceof bCtor &&
!(aCtor instanceof aCtor && bCtor instanceof bCtor)) {
diffBuilder.record(a, b, constructorsAreDifferentFormatter.bind(null, this.pp));
diffBuilder.recordMismatch(constructorsAreDifferentFormatter.bind(null, this.pp));
return false;
}
}
@@ -4590,7 +4630,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
// Ensure that both objects contain the same number of properties before comparing deep equality.
if (keys(b, className == '[object Array]').length !== size) {
diffBuilder.record(a, b, objectKeysAreDifferentFormatter.bind(null, this.pp));
diffBuilder.recordMismatch(objectKeysAreDifferentFormatter.bind(null, this.pp));
return false;
}
@@ -4598,7 +4638,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
key = aKeys[i];
// Deep compare each member
if (!j$.util.has(b, key)) {
diffBuilder.record(a, b, objectKeysAreDifferentFormatter.bind(null, this.pp));
diffBuilder.recordMismatch(objectKeysAreDifferentFormatter.bind(null, this.pp));
result = false;
continue;
}
@@ -4704,12 +4744,75 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
}
function isDiffBuilder(obj) {
return obj && typeof obj.record === 'function';
return obj && typeof obj.recordMismatch === 'function';
}
return MatchersUtil;
};
getJasmineRequireObj().MismatchTree = function (j$) {
/*
To be able to apply custom object formatters at all possible levels of an
object graph, DiffBuilder needs to be able to know not just where the
mismatch occurred but also all ancestors of the mismatched value in both
the expected and actual object graphs. MismatchTree maintains that context
and provides it via the traverse method.
*/
function MismatchTree(path) {
this.path = path || new j$.ObjectPath([]);
this.formatter = undefined;
this.children = [];
this.isMismatch = false;
}
MismatchTree.prototype.add = function (path, formatter) {
var key, child;
if (path.depth() === 0) {
this.formatter = formatter;
this.isMismatch = true;
} else {
key = path.components[0];
path = path.shift();
child = this.child(key);
if (!child) {
child = new MismatchTree(this.path.add(key));
this.children.push(child);
}
child.add(path, formatter);
}
};
MismatchTree.prototype.traverse = function (visit) {
var i, hasChildren = this.children.length > 0;
if (this.isMismatch || hasChildren) {
if (visit(this.path, !hasChildren, this.formatter)) {
for (i = 0; i < this.children.length; i++) {
this.children[i].traverse(visit);
}
}
}
};
MismatchTree.prototype.child = function(key) {
var i, pathEls;
for (i = 0; i < this.children.length; i++) {
pathEls = this.children[i].path.components;
if (pathEls[pathEls.length - 1] === key) {
return this.children[i];
}
}
};
return MismatchTree;
};
getJasmineRequireObj().nothing = function() {
/**
* {@link expect} nothing explicitly.
@@ -4738,7 +4841,8 @@ getJasmineRequireObj().NullDiffBuilder = function(j$) {
withPath: function(_, block) {
block();
},
record: function() {}
setRoots: function() {},
recordMismatch: function() {}
};
};
};
@@ -4756,10 +4860,24 @@ getJasmineRequireObj().ObjectPath = function(j$) {
}
};
ObjectPath.prototype.dereference = function(obj) {
var i;
for (i = 0; i < this.components.length; i++) {
obj = obj[this.components[i]];
}
return obj;
};
ObjectPath.prototype.add = function(component) {
return new ObjectPath(this.components.concat([component]));
};
ObjectPath.prototype.shift = function() {
return new ObjectPath(this.components.slice(1));
};
ObjectPath.prototype.depth = function() {
return this.components.length;
};
@@ -6109,15 +6227,7 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) {
};
SinglePrettyPrintRun.prototype.applyCustomFormatters_ = function(value) {
var i, result;
for (i = 0; i < this.customObjectFormatters_.length; i++) {
result = this.customObjectFormatters_[i](value);
if (result !== undefined) {
return result;
}
}
return customFormat(value, this.customObjectFormatters_);
};
SinglePrettyPrintRun.prototype.iterateObject = function(obj, fn) {
@@ -6390,16 +6500,31 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) {
return extraKeys;
}
function customFormat(value, customObjectFormatters) {
var i, result;
for (i = 0; i < customObjectFormatters.length; i++) {
result = customObjectFormatters[i](value);
if (result !== undefined) {
return result;
}
}
}
return function(customObjectFormatters) {
customObjectFormatters = customObjectFormatters || [];
var pp = function(value) {
var prettyPrinter = new SinglePrettyPrintRun(
customObjectFormatters || [],
pp
);
var prettyPrinter = new SinglePrettyPrintRun(customObjectFormatters, pp);
prettyPrinter.format(value);
return prettyPrinter.stringParts.join('');
};
pp.customFormat_ = function(value) {
return customFormat(value, customObjectFormatters);
};
return pp;
};
};

View File

@@ -505,7 +505,7 @@ describe('PrettyPrinter', function() {
pp = jasmineUnderTest.makePrettyPrinter(customObjectFormatters),
obj = { foo: 'bar' };
expect(pp(obj, customObjectFormatters)).toEqual('2nd: bar');
expect(pp(obj)).toEqual('2nd: bar');
});
it('should fall back to built in logic if all custom object formatters return undefined', function() {
@@ -517,7 +517,39 @@ describe('PrettyPrinter', function() {
pp = jasmineUnderTest.makePrettyPrinter(customObjectFormatters),
obj = { foo: 'bar' };
expect(pp(obj, customObjectFormatters)).toEqual("Object({ foo: 'bar' })");
expect(pp(obj)).toEqual("Object({ foo: 'bar' })");
});
});
describe('#customFormat_', function() {
it('should use the first custom object formatter that does not return undefined', function() {
var customObjectFormatters = [
function(obj) {
return undefined;
},
function(obj) {
return '2nd: ' + obj.foo;
},
function(obj) {
return '3rd: ' + obj.foo;
}
],
pp = jasmineUnderTest.makePrettyPrinter(customObjectFormatters),
obj = { foo: 'bar' };
expect(pp.customFormat_(obj)).toEqual('2nd: bar');
});
it('should return undefined if all custom object formatters return undefined', function() {
var customObjectFormatters = [
function(obj) {
return undefined;
}
],
pp = jasmineUnderTest.makePrettyPrinter(customObjectFormatters),
obj = { foo: 'bar' };
expect(pp.customFormat_(obj)).toBeUndefined();
});
});
});

View File

@@ -1,73 +1,136 @@
describe("DiffBuilder", function() {
it("records the actual and expected objects", function() {
describe("DiffBuilder", function () {
it("records the actual and expected objects", function () {
var diffBuilder = jasmineUnderTest.DiffBuilder();
diffBuilder.record({x: 'actual'}, {x: 'expected'});
diffBuilder.setRoots({x: 'actual'}, {x: 'expected'});
diffBuilder.recordMismatch();
expect(diffBuilder.getMessage()).toEqual("Expected Object({ x: 'actual' }) to equal Object({ x: 'expected' }).");
});
it("prints the path at which the difference was found", function() {
it("prints the path at which the difference was found", function () {
var diffBuilder = jasmineUnderTest.DiffBuilder();
diffBuilder.setRoots({foo: {x: 'actual'}}, {foo: {x: 'expected'}});
diffBuilder.withPath('foo', function() {
diffBuilder.record({x: 'actual'}, {x: 'expected'});
diffBuilder.withPath('foo', function () {
diffBuilder.recordMismatch();
});
expect(diffBuilder.getMessage()).toEqual("Expected $.foo = Object({ x: 'actual' }) to equal Object({ x: 'expected' }).");
});
it("prints multiple messages, separated by newlines", function() {
it("prints multiple messages, separated by newlines", function () {
var diffBuilder = jasmineUnderTest.DiffBuilder();
diffBuilder.setRoots({foo: 1, bar: 3}, {foo: 2, bar: 4});
diffBuilder.withPath('foo', function() {
diffBuilder.record(1, 2);
diffBuilder.withPath('foo', function () {
diffBuilder.recordMismatch();
});
diffBuilder.withPath('bar', function () {
diffBuilder.recordMismatch();
});
var message =
"Expected $.foo = 1 to equal 2.\n" +
"Expected 3 to equal 4.";
"Expected $.bar = 3 to equal 4.";
diffBuilder.record(3, 4);
expect(diffBuilder.getMessage()).toEqual(message);
});
it("allows customization of the message", function() {
it("allows customization of the message", function () {
var diffBuilder = jasmineUnderTest.DiffBuilder();
diffBuilder.setRoots({x: 'bar'}, {x: 'foo'});
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);
diffBuilder.withPath('x', function () {
diffBuilder.recordMismatch(darthVaderFormatter);
});
expect(diffBuilder.getMessage()).toEqual("I find your lack of foo disturbing. (was bar, at $.x)");
});
it("uses the injected pretty-printer", function() {
var prettyPrinter = function(val) {
it("uses the injected pretty-printer", function () {
var prettyPrinter = function (val) {
return '|' + val + '|';
},
diffBuilder = jasmineUnderTest.DiffBuilder({prettyPrinter: prettyPrinter});
prettyPrinter.customFormat_ = function () {
};
diffBuilder.withPath('foo', function() {
diffBuilder.record('actual', 'expected');
diffBuilder.setRoots({foo: 'actual'}, {foo: 'expected'});
diffBuilder.withPath('foo', function () {
diffBuilder.recordMismatch();
});
expect(diffBuilder.getMessage()).toEqual("Expected $.foo = |actual| to equal |expected|.");
});
it("passes the injected pretty-printer to the diff formatter", function() {
it("passes the injected pretty-printer to the diff formatter", function () {
var diffFormatter = jasmine.createSpy('diffFormatter'),
prettyPrinter = function() {},
prettyPrinter = function () {
},
diffBuilder = jasmineUnderTest.DiffBuilder({prettyPrinter: prettyPrinter});
prettyPrinter.customFormat_ = function () {
};
diffBuilder.withPath('x', function() {
diffBuilder.record('bar', 'foo', diffFormatter);
diffBuilder.setRoots({x: 'bar'}, {x: 'foo'});
diffBuilder.withPath('x', function () {
diffBuilder.recordMismatch(diffFormatter);
});
diffBuilder.getMessage();
expect(diffFormatter).toHaveBeenCalledWith('bar', 'foo', jasmine.anything(), prettyPrinter);
});
it("uses custom object formatters on leaf nodes", function() {
var formatter = function(x) {
if (typeof x === 'number') {
return '[number:' + x + ']';
}
};
prettyPrinter = jasmineUnderTest.makePrettyPrinter([formatter]);
var diffBuilder = new jasmineUnderTest.DiffBuilder({prettyPrinter: prettyPrinter});
diffBuilder.setRoots(5, 4);
diffBuilder.recordMismatch();
expect(diffBuilder.getMessage()).toEqual('Expected [number:5] to equal [number:4].');
});
it("uses custom object formatters on non leaf nodes", function () {
var formatter = function (x) {
if (x.hasOwnProperty('a')) {
return '[thing with a=' + x.a + ', b=' + JSON.stringify(x.b) + ']';
}
};
prettyPrinter = jasmineUnderTest.makePrettyPrinter([formatter]);
var diffBuilder = new jasmineUnderTest.DiffBuilder({prettyPrinter: prettyPrinter});
var expectedMsg = 'Expected $[0].foo = [thing with a=1, b={"x":42}] to equal [thing with a=1, b={"x":43}].\n' +
"Expected $[0].bar = 'yes' to equal 'no'.";
diffBuilder.setRoots(
[{foo: {a: 1, b: {x: 42}}, bar: 'yes'}],
[{foo: {a: 1, b: {x: 43}}, bar: 'no'}]
);
diffBuilder.withPath(0, function () {
diffBuilder.withPath('foo', function () {
diffBuilder.withPath('b', function () {
diffBuilder.withPath('x', function () {
diffBuilder.recordMismatch();
});
});
});
diffBuilder.withPath('bar', function () {
diffBuilder.recordMismatch();
});
});
expect(diffBuilder.getMessage()).toEqual(expectedMsg);
});
});

View File

@@ -0,0 +1,136 @@
describe('MismatchTree', function () {
describe('#add', function () {
describe('When the path is empty', function () {
it('flags the root node as mismatched', function () {
var tree = new jasmineUnderTest.MismatchTree();
tree.add(new jasmineUnderTest.ObjectPath([]));
expect(tree.isMismatch).toBe(true);
});
});
describe('When the path is not empty', function () {
it('flags the node as mismatched', function () {
var tree = new jasmineUnderTest.MismatchTree();
tree.add(new jasmineUnderTest.ObjectPath(['a', 'b']));
expect(tree.child('a').child('b').isMismatch).toBe(true);
});
it('does not flag ancestors as mismatched', function () {
var tree = new jasmineUnderTest.MismatchTree();
tree.add(new jasmineUnderTest.ObjectPath(['a', 'b']));
expect(tree.isMismatch).toBe(false);
expect(tree.child('a').isMismatch).toBe(false);
});
});
it('stores the formatter on only the target node', function () {
var tree = new jasmineUnderTest.MismatchTree();
tree.add(new jasmineUnderTest.ObjectPath(['a', 'b']), formatter);
expect(tree.formatter).toBeFalsy();
expect(tree.child('a').formatter).toBeFalsy();
expect(tree.child('a').child('b').formatter).toBe(formatter);
});
it('stores the path to the node', function () {
var tree = new jasmineUnderTest.MismatchTree();
tree.add(new jasmineUnderTest.ObjectPath(['a', 'b']), formatter);
expect(tree.child('a').child('b').path.components).toEqual(['a', 'b']);
});
});
describe('#traverse', function () {
it('calls the callback for all nodes that are or contain mismatches', function () {
var tree = new jasmineUnderTest.MismatchTree();
tree.add(new jasmineUnderTest.ObjectPath(['a', 'b']), formatter);
tree.add(new jasmineUnderTest.ObjectPath(['c']));
var visit = jasmine.createSpy('visit').and.returnValue(true);
tree.traverse(visit);
expect(visit).toHaveBeenCalledWith(
new jasmineUnderTest.ObjectPath([]), false, undefined
);
expect(visit).toHaveBeenCalledWith(
new jasmineUnderTest.ObjectPath(['a']), false, undefined
);
expect(visit).toHaveBeenCalledWith(
new jasmineUnderTest.ObjectPath(['a', 'b']), true, formatter
);
expect(visit).toHaveBeenCalledWith(
new jasmineUnderTest.ObjectPath(['c']), true, undefined
);
});
it('does not call the callback if there are no mismatches', function () {
var tree = new jasmineUnderTest.MismatchTree();
var visit = jasmine.createSpy('visit');
tree.traverse(visit);
expect(visit).not.toHaveBeenCalled();
});
it('visits parents before children', function () {
var tree = new jasmineUnderTest.MismatchTree();
tree.add(new jasmineUnderTest.ObjectPath(['a', 'b']));
var visited = [];
tree.traverse(function (path) {
visited.push(path);
return true;
});
expect(visited).toEqual([
new jasmineUnderTest.ObjectPath([]),
new jasmineUnderTest.ObjectPath(['a']),
new jasmineUnderTest.ObjectPath(['a', 'b'])
]);
});
it('visits children in the order they were recorded', function() {
var tree = new jasmineUnderTest.MismatchTree();
tree.add(new jasmineUnderTest.ObjectPath(['length']));
tree.add(new jasmineUnderTest.ObjectPath([1]));
var visited = [];
tree.traverse(function (path) {
visited.push(path);
return true;
});
expect(visited).toEqual([
new jasmineUnderTest.ObjectPath([]),
new jasmineUnderTest.ObjectPath(['length']),
new jasmineUnderTest.ObjectPath([1])
]);
});
it('does not visit children if the callback returns falsy', function() {
var tree = new jasmineUnderTest.MismatchTree();
tree.add(new jasmineUnderTest.ObjectPath(['a', 'b']));
var visited = [];
tree.traverse(function (path) {
visited.push(path);
return path.depth() === 0;
});
expect(visited).toEqual([
new jasmineUnderTest.ObjectPath([]),
new jasmineUnderTest.ObjectPath(['a'])
]);
});
});
function formatter() {
}
});

View File

@@ -1,13 +1,8 @@
describe('NullDiffBuilder', function() {
it('responds to withPath() by calling the passed function', function() {
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();
})
});

View File

@@ -39,5 +39,13 @@ describe('ObjectPath', function() {
expect(path.toString()).toEqual('$.foo');
expect(root.toString()).toEqual('');
})
});
describe('#dereference', function() {
it('returns the value corresponding to the path', function () {
var path = new ObjectPath().add('foo').add(1).add('bar');
var obj = {foo: ['', {bar: 42}]};
expect(path.dereference(obj)).toEqual(42);
});
});
});

View File

@@ -730,26 +730,26 @@ describe("matchersUtil", function() {
var diffBuilder = new jasmineUnderTest.DiffBuilder(),
matchersUtil = new jasmineUnderTest.MatchersUtil();
spyOn(diffBuilder, 'record');
spyOn(diffBuilder, 'recordMismatch');
spyOn(diffBuilder, 'withPath').and.callThrough();
matchersUtil.equals([1], [2], [], diffBuilder);
expect(diffBuilder.withPath).toHaveBeenCalledWith('length', jasmine.any(Function));
expect(diffBuilder.withPath).toHaveBeenCalledWith(0, jasmine.any(Function));
expect(diffBuilder.record).toHaveBeenCalledWith(1, 2);
expect(diffBuilder.recordMismatch).toHaveBeenCalledWith();
});
it('uses a diffBuilder if one is provided as the third argument', function() {
var diffBuilder = new jasmineUnderTest.DiffBuilder(),
matchersUtil = new jasmineUnderTest.MatchersUtil();
spyOn(diffBuilder, 'record');
spyOn(diffBuilder, 'recordMismatch');
spyOn(diffBuilder, 'withPath').and.callThrough();
matchersUtil.equals([1], [2], diffBuilder);
expect(diffBuilder.withPath).toHaveBeenCalledWith('length', jasmine.any(Function));
expect(diffBuilder.withPath).toHaveBeenCalledWith(0, jasmine.any(Function));
expect(diffBuilder.record).toHaveBeenCalledWith(1, 2);
expect(diffBuilder.recordMismatch).toHaveBeenCalled();
});
});

View File

@@ -97,8 +97,12 @@ describe("toEqual", function() {
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("uses custom object formatters to pretty-print properties", function() {
function formatter(x) { return '|' + x + '|'; }
it("uses custom object formatters to pretty-print simple properties", function() {
function formatter(x) {
if (typeof x === 'number') {
return '|' + x + '|';
}
}
var actual = {x: {y: 1, z: 2, f: 4}},
expected = {x: {y: 1, z: 2, g: 3}},
@@ -114,8 +118,12 @@ describe("toEqual", function() {
expect(matcher.compare(actual, expected).message).toEqual(message);
});
it("uses custom object formatters to build diffs", function() {
function formatter(x) { return '|' + x + '|'; }
it("uses custom object formatters to show simple values in diffs", function() {
function formatter(x) {
if (typeof x === 'number') {
return '|' + x + '|';
}
}
var actual = [{foo: 4}],
expected = [{foo: 5}],
@@ -127,6 +135,30 @@ describe("toEqual", function() {
expect(matcher.compare(actual, expected).message).toEqual(message);
});
it("uses custom object formatters to show more complex objects diffs", function() {
function formatter(x) {
if (x.hasOwnProperty('a')) {
return '[thing with a=' + x.a + ', b=' + x.b + ']';
}
}
var actual = [{
foo: {a: 1, b: 2},
bar: 'should not be pretty printed'
}],
expected = [{
foo: {a: 5, b: 2},
bar: "shouldn't be pretty printed"
}],
prettyPrinter = jasmineUnderTest.makePrettyPrinter([formatter]),
util = new jasmineUnderTest.MatchersUtil({pp: prettyPrinter}),
matcher = jasmineUnderTest.matchers.toEqual(util),
message = "Expected $[0].foo = [thing with a=1, b=2] to equal [thing with a=5, b=2].\n" +
"Expected $[0].bar = 'should not be pretty printed' to equal 'shouldn't be pretty printed'.";
expect(matcher.compare(actual, expected).message).toEqual(message);
});
it("reports extra and missing properties of the root-level object", function() {
var actual = {x: 1},
expected = {a: 1},
@@ -303,10 +335,14 @@ describe("toEqual", function() {
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("uses custom object formatters to report objects with different constructors", function () {
it("uses custom object formatters for the value but not the type when reporting objects with different constructors", function () {
function Foo() {}
function Bar() {}
function formatter(x) { return '|' + x + '|'; }
function formatter(x) {
if (x instanceof Foo || x instanceof Bar) {
return '|' + x + '|';
}
}
var actual = {x: new Foo()},
expected = {x: new Bar()},
@@ -329,6 +365,11 @@ describe("toEqual", function() {
expect(compareEquals(actual, expected).message).toEqual(message);
});
it("reports value mismatches at the root level", function() {
expect(compareEquals(1, 2).message).toEqual("Expected 1 to equal 2.");
});
it("reports mismatches between objects with their own constructor property", function () {
function Foo() {}
function Bar() {}
@@ -839,7 +880,11 @@ describe("toEqual", function() {
});
it("uses custom object formatters when the actual array is longer", function() {
function formatter(x) { return '|' + x + '|'; }
function formatter(x) {
if (typeof x === 'number') {
return '|' + x + '|';
}
}
var actual = [1, 1, 2, 3, 5],
expected = [1, 1, 2, 3],

View File

@@ -102,15 +102,7 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) {
};
SinglePrettyPrintRun.prototype.applyCustomFormatters_ = function(value) {
var i, result;
for (i = 0; i < this.customObjectFormatters_.length; i++) {
result = this.customObjectFormatters_[i](value);
if (result !== undefined) {
return result;
}
}
return customFormat(value, this.customObjectFormatters_);
};
SinglePrettyPrintRun.prototype.iterateObject = function(obj, fn) {
@@ -383,16 +375,31 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) {
return extraKeys;
}
function customFormat(value, customObjectFormatters) {
var i, result;
for (i = 0; i < customObjectFormatters.length; i++) {
result = customObjectFormatters[i](value);
if (result !== undefined) {
return result;
}
}
}
return function(customObjectFormatters) {
customObjectFormatters = customObjectFormatters || [];
var pp = function(value) {
var prettyPrinter = new SinglePrettyPrintRun(
customObjectFormatters || [],
pp
);
var prettyPrinter = new SinglePrettyPrintRun(customObjectFormatters, pp);
prettyPrinter.format(value);
return prettyPrinter.stringParts.join('');
};
pp.customFormat_ = function(value) {
return customFormat(value, customObjectFormatters);
};
return pp;
};
};

View File

@@ -1,17 +1,51 @@
getJasmineRequireObj().DiffBuilder = function(j$) {
getJasmineRequireObj().DiffBuilder = function (j$) {
return function DiffBuilder(config) {
var path = new j$.ObjectPath(),
mismatches = [],
prettyPrinter = (config || {}).prettyPrinter || j$.makePrettyPrinter();
var prettyPrinter = (config || {}).prettyPrinter || j$.makePrettyPrinter(),
mismatches = new j$.MismatchTree(),
path = new j$.ObjectPath(),
actualRoot = undefined,
expectedRoot = undefined;
return {
record: function (actual, expected, formatter) {
formatter = formatter || defaultFormatter;
mismatches.push(formatter(actual, expected, path, prettyPrinter));
setRoots: function (actual, expected) {
actualRoot = actual;
expectedRoot = expected;
},
recordMismatch: function (formatter) {
mismatches.add(path, formatter);
},
getMessage: function () {
return mismatches.join('\n');
var messages = [];
mismatches.traverse(function (path, isLeaf, formatter) {
var actualCustom, expectedCustom, useCustom,
actual = path.dereference(actualRoot),
expected = path.dereference(expectedRoot);
if (formatter) {
messages.push(formatter(actual, expected, path, prettyPrinter));
return true;
}
actualCustom = prettyPrinter.customFormat_(actual);
expectedCustom = prettyPrinter.customFormat_(expected);
useCustom = !(j$.util.isUndefined(actualCustom) && j$.util.isUndefined(expectedCustom));
if (useCustom) {
messages.push(wrapPrettyPrinted(actualCustom, expectedCustom, path));
return false; // don't recurse further
}
if (isLeaf) {
messages.push(defaultFormatter(actual, expected, path, prettyPrinter));
}
return true;
});
return messages.join('\n');
},
withPath: function (pathComponent, block) {
@@ -22,12 +56,16 @@ getJasmineRequireObj().DiffBuilder = function(j$) {
}
};
function defaultFormatter (actual, expected, path, prettyPrinter) {
function defaultFormatter(actual, expected, path, prettyPrinter) {
return wrapPrettyPrinted(prettyPrinter(actual), prettyPrinter(expected), path);
}
function wrapPrettyPrinted(actual, expected, path) {
return 'Expected ' +
path + (path.depth() ? ' = ' : '') +
prettyPrinter(actual) +
actual +
' to equal ' +
prettyPrinter(expected) +
expected +
'.';
}
};

View File

@@ -0,0 +1,62 @@
getJasmineRequireObj().MismatchTree = function (j$) {
/*
To be able to apply custom object formatters at all possible levels of an
object graph, DiffBuilder needs to be able to know not just where the
mismatch occurred but also all ancestors of the mismatched value in both
the expected and actual object graphs. MismatchTree maintains that context
and provides it via the traverse method.
*/
function MismatchTree(path) {
this.path = path || new j$.ObjectPath([]);
this.formatter = undefined;
this.children = [];
this.isMismatch = false;
}
MismatchTree.prototype.add = function (path, formatter) {
var key, child;
if (path.depth() === 0) {
this.formatter = formatter;
this.isMismatch = true;
} else {
key = path.components[0];
path = path.shift();
child = this.child(key);
if (!child) {
child = new MismatchTree(this.path.add(key));
this.children.push(child);
}
child.add(path, formatter);
}
};
MismatchTree.prototype.traverse = function (visit) {
var i, hasChildren = this.children.length > 0;
if (this.isMismatch || hasChildren) {
if (visit(this.path, !hasChildren, this.formatter)) {
for (i = 0; i < this.children.length; i++) {
this.children[i].traverse(visit);
}
}
}
};
MismatchTree.prototype.child = function(key) {
var i, pathEls;
for (i = 0; i < this.children.length; i++) {
pathEls = this.children[i].path.components;
if (pathEls[pathEls.length - 1] === key) {
return this.children[i];
}
}
};
return MismatchTree;
};

View File

@@ -4,7 +4,8 @@ getJasmineRequireObj().NullDiffBuilder = function(j$) {
withPath: function(_, block) {
block();
},
record: function() {}
setRoots: function() {},
recordMismatch: function() {}
};
};
};

View File

@@ -11,10 +11,24 @@ getJasmineRequireObj().ObjectPath = function(j$) {
}
};
ObjectPath.prototype.dereference = function(obj) {
var i;
for (i = 0; i < this.components.length; i++) {
obj = obj[this.components[i]];
}
return obj;
};
ObjectPath.prototype.add = function(component) {
return new ObjectPath(this.components.concat([component]));
};
ObjectPath.prototype.shift = function() {
return new ObjectPath(this.components.slice(1));
};
ObjectPath.prototype.depth = function() {
return this.components.length;
};

View File

@@ -69,7 +69,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
if (asymmetricA) {
result = a.asymmetricMatch(b, shim);
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
}
@@ -77,7 +77,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
if (asymmetricB) {
result = b.asymmetricMatch(a, shim);
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
}
@@ -95,6 +95,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
customTesters = customTesters || this.customTesters_;
diffBuilder = diffBuilder || j$.NullDiffBuilder();
diffBuilder.setRoots(a, b);
return this.eq_(a, b, [], [], customTesters, diffBuilder);
};
@@ -113,7 +114,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
var customTesterResult = customTesters[i](a, b);
if (!j$.util.isUndefined(customTesterResult)) {
if (!customTesterResult) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return customTesterResult;
}
@@ -122,7 +123,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
if (a instanceof Error && b instanceof Error) {
result = a.message == b.message;
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
}
@@ -132,7 +133,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
if (a === b) {
result = a !== 0 || 1 / a == 1 / b;
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
}
@@ -140,13 +141,13 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
if (a === null || b === null) {
result = a === b;
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
}
var className = Object.prototype.toString.call(a);
if (className != Object.prototype.toString.call(b)) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
switch (className) {
@@ -156,7 +157,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
// equivalent to `new String("5")`.
result = a == String(b);
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
case '[object Number]':
@@ -164,7 +165,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
// other numeric values.
result = a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b);
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
case '[object Date]':
@@ -174,7 +175,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
// of `NaN` are not equivalent.
result = +a == +b;
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
// RegExps are compared by their source patterns and flags.
@@ -185,7 +186,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
a.ignoreCase == b.ignoreCase;
}
if (typeof a != 'object' || typeof b != 'object') {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
@@ -195,12 +196,12 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
// At first try to use DOM3 method isEqualNode
result = a.isEqualNode(b);
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
}
return result;
}
if (aIsDomNode || bIsDomNode) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
@@ -230,7 +231,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
diffBuilder.withPath('length', function() {
if (aLength !== bLength) {
diffBuilder.record(aLength, bLength);
diffBuilder.recordMismatch();
result = false;
}
});
@@ -238,7 +239,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
for (i = 0; i < aLength || i < bLength; i++) {
diffBuilder.withPath(i, function() {
if (i >= bLength) {
diffBuilder.record(a[i], void 0, actualArrayIsLongerFormatter.bind(null, self.pp));
diffBuilder.recordMismatch(actualArrayIsLongerFormatter.bind(null, self.pp));
result = false;
} else {
result = self.eq_(i < aLength ? a[i] : void 0, i < bLength ? b[i] : void 0, aStack, bStack, customTesters, diffBuilder) && result;
@@ -250,7 +251,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
}
} else if (j$.isMap(a) && j$.isMap(b)) {
if (a.size != b.size) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
@@ -292,12 +293,12 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
}
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
} else if (j$.isSet(a) && j$.isSet(b)) {
if (a.size != b.size) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
@@ -342,7 +343,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
}
if (!result) {
diffBuilder.record(a, b);
diffBuilder.recordMismatch();
return false;
}
} else {
@@ -355,7 +356,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
a instanceof aCtor && b instanceof bCtor &&
!(aCtor instanceof aCtor && bCtor instanceof bCtor)) {
diffBuilder.record(a, b, constructorsAreDifferentFormatter.bind(null, this.pp));
diffBuilder.recordMismatch(constructorsAreDifferentFormatter.bind(null, this.pp));
return false;
}
}
@@ -366,7 +367,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
// Ensure that both objects contain the same number of properties before comparing deep equality.
if (keys(b, className == '[object Array]').length !== size) {
diffBuilder.record(a, b, objectKeysAreDifferentFormatter.bind(null, this.pp));
diffBuilder.recordMismatch(objectKeysAreDifferentFormatter.bind(null, this.pp));
return false;
}
@@ -374,7 +375,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
key = aKeys[i];
// Deep compare each member
if (!j$.util.has(b, key)) {
diffBuilder.record(a, b, objectKeysAreDifferentFormatter.bind(null, this.pp));
diffBuilder.recordMismatch(objectKeysAreDifferentFormatter.bind(null, this.pp));
result = false;
continue;
}
@@ -480,7 +481,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
}
function isDiffBuilder(obj) {
return obj && typeof obj.record === 'function';
return obj && typeof obj.recordMismatch === 'function';
}
return MatchersUtil;

View File

@@ -84,6 +84,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) {
j$.DiffBuilder = jRequire.DiffBuilder(j$);
j$.NullDiffBuilder = jRequire.NullDiffBuilder(j$);
j$.ObjectPath = jRequire.ObjectPath(j$);
j$.MismatchTree = jRequire.MismatchTree(j$);
j$.GlobalErrors = jRequire.GlobalErrors(j$);
j$.Truthy = jRequire.Truthy(j$);