Don't require matchers and asymmetric equality testers to pass custom object formatters back to Jasmine

This makes it easier to write high quality matchers and asymmetric equality
testers, and is also a step toward supporting custom object formatters.

Previously, Jasmine passed custom object formatters as the second argument
to matcher factories and as and the second argument to asymmetric equality
testers' `asymmetricMatch` method. Matchers and asymmetric equality testers
were responsible for passing the custom object formatters to methods like
`matchersUtil#equals`:

  function toEqual(util, customEqualityTesters) {
    return {
      compare: function(actual, expected) {
        // ...
        result.pass = util.equals(actual, expected, customEqualityTesters, diffBuilder);

And:

  ArrayContaining.prototype.asymmetricMatch = function(other, customTesters) {
    // ...
    for (var i = 0; i < this.sample.length; i++) {
      var item = this.sample[i];
      if (!j$.matchersUtil.contains(other, item, customTesters)) {
        return false;
      }
    }

With this change, that is no longer necessary. Matchers and asymmetric
equality testers can ignore the existence of custom equality testers and
still fully support them:

  function toEqual(util) {
    return {
      compare: function(actual, expected) {
        // ...
        result.pass = util.equals(actual, expected, diffBuilder);

And:

  ArrayContaining.prototype.asymmetricMatch = function(other, matchersUtil) {
    // ...
    for (var i = 0; i < this.sample.length; i++) {
      var item = this.sample[i];
      if (!matchersUtil.contains(other, item)) {
        return false;
      }
    }

The old interfaces are still supported, for now, but will be deprecated
in a future commit and removed in the next major release after that.

In addition to making matchers and custom equality testers simpler,
this change sets the stage for adding support for custom object
formatters. Those will be architecturally similar to custom equality
testers, and by injecting a `MatchersUtil` instance everywhere we can
add them without requiring user code to pass them around as used to be
the case with custom object formatters.
This commit is contained in:
Steve Gravrock
2019-09-21 10:58:09 -07:00
committed by Steve Gravrock
parent a00f995c68
commit dec67bd535
43 changed files with 1214 additions and 455 deletions

View File

@@ -11,7 +11,7 @@ getJasmineRequireObj().toBeRejectedWith = function(j$) {
* @example
* return expectAsync(aPromise).toBeRejectedWith({prop: 'value'});
*/
return function toBeRejectedWith(util, customEqualityTesters) {
return function toBeRejectedWith(util) {
return {
compare: function(actualPromise, expectedValue) {
if (!j$.isPromiseLike(actualPromise)) {
@@ -32,7 +32,7 @@ getJasmineRequireObj().toBeRejectedWith = function(j$) {
};
},
function(actualValue) {
if (util.equals(actualValue, expectedValue, customEqualityTesters)) {
if (util.equals(actualValue, expectedValue)) {
return {
pass: true,
message: prefix(true) + '.'

View File

@@ -11,7 +11,7 @@ getJasmineRequireObj().toBeResolvedTo = function(j$) {
* @example
* return expectAsync(aPromise).toBeResolvedTo({prop: 'value'});
*/
return function toBeResolvedTo(util, customEqualityTesters) {
return function toBeResolvedTo(util) {
return {
compare: function(actualPromise, expectedValue) {
if (!j$.isPromiseLike(actualPromise)) {
@@ -26,7 +26,7 @@ getJasmineRequireObj().toBeResolvedTo = function(j$) {
return actualPromise.then(
function(actualValue) {
if (util.equals(actualValue, expectedValue, customEqualityTesters)) {
if (util.equals(actualValue, expectedValue)) {
return {
pass: true,
message: prefix(true) + '.'

View File

@@ -1,63 +1,67 @@
getJasmineRequireObj().matchersUtil = function(j$) {
getJasmineRequireObj().MatchersUtil = function(j$) {
// TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter?
return {
equals: equals,
function MatchersUtil(options) {
options = options || {};
this.customTesters_ = options.customTesters || [];
contains: function(haystack, needle, customTesters) {
customTesters = customTesters || [];
if (j$.isSet(haystack)) {
return haystack.has(needle);
}
if ((Object.prototype.toString.apply(haystack) === '[object Array]') ||
(!!haystack && !haystack.indexOf))
{
for (var i = 0; i < haystack.length; i++) {
if (equals(haystack[i], needle, customTesters)) {
return true;
}
}
return false;
}
return !!haystack && haystack.indexOf(needle) >= 0;
},
buildFailureMessage: function() {
var args = Array.prototype.slice.call(arguments, 0),
matcherName = args[0],
isNot = args[1],
actual = args[2],
expected = args.slice(3),
englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
var message = 'Expected ' +
j$.pp(actual) +
(isNot ? ' not ' : ' ') +
englishyPredicate;
if (expected.length > 0) {
for (var i = 0; i < expected.length; i++) {
if (i > 0) {
message += ',';
}
message += ' ' + j$.pp(expected[i]);
}
}
return message + '.';
if (!j$.isArray_(this.customTesters_)) {
throw new Error("MatchersUtil requires custom equality testers");
}
}
MatchersUtil.prototype.contains = function(haystack, needle, customTesters) {
if (j$.isSet(haystack)) {
return haystack.has(needle);
}
if ((Object.prototype.toString.apply(haystack) === '[object Array]') ||
(!!haystack && !haystack.indexOf))
{
for (var i = 0; i < haystack.length; i++) {
if (this.equals(haystack[i], needle, customTesters)) {
return true;
}
}
return false;
}
return !!haystack && haystack.indexOf(needle) >= 0;
};
MatchersUtil.prototype.buildFailureMessage = function() {
var args = Array.prototype.slice.call(arguments, 0),
matcherName = args[0],
isNot = args[1],
actual = args[2],
expected = args.slice(3),
englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
var message = 'Expected ' +
j$.pp(actual) +
(isNot ? ' not ' : ' ') +
englishyPredicate;
if (expected.length > 0) {
for (var i = 0; i < expected.length; i++) {
if (i > 0) {
message += ',';
}
message += ' ' + j$.pp(expected[i]);
}
}
return message + '.';
};
function isAsymmetric(obj) {
return obj && j$.isA_('Function', obj.asymmetricMatch);
}
function asymmetricMatch(a, b, customTesters, diffBuilder) {
MatchersUtil.prototype.asymmetricMatch_ = function(a, b, customTesters, diffBuilder) {
var asymmetricA = isAsymmetric(a),
asymmetricB = isAsymmetric(b),
shim = j$.asymmetricEqualityTesterArgCompatShim(this, customTesters),
result;
if (asymmetricA && asymmetricB) {
@@ -65,7 +69,7 @@ getJasmineRequireObj().matchersUtil = function(j$) {
}
if (asymmetricA) {
result = a.asymmetricMatch(b, customTesters);
result = a.asymmetricMatch(b, shim);
if (!result) {
diffBuilder.record(a, b);
}
@@ -73,27 +77,36 @@ getJasmineRequireObj().matchersUtil = function(j$) {
}
if (asymmetricB) {
result = b.asymmetricMatch(a, customTesters);
result = b.asymmetricMatch(a, shim);
if (!result) {
diffBuilder.record(a, b);
}
return result;
}
}
};
function equals(a, b, customTesters, diffBuilder) {
customTesters = customTesters || [];
MatchersUtil.prototype.equals = function(a, b, customTestersOrDiffBuilder, diffBuilderOrNothing) {
var customTesters, diffBuilder;
if (isDiffBuilder(customTestersOrDiffBuilder)) {
diffBuilder = customTestersOrDiffBuilder;
} else {
customTesters = customTestersOrDiffBuilder;
diffBuilder = diffBuilderOrNothing;
}
customTesters = customTesters || this.customTesters_;
diffBuilder = diffBuilder || j$.NullDiffBuilder();
return eq(a, b, [], [], customTesters, diffBuilder);
}
return this.eq_(a, b, [], [], customTesters, diffBuilder);
};
// Equality function lovingly adapted from isEqual in
// [Underscore](http://underscorejs.org)
function eq(a, b, aStack, bStack, customTesters, diffBuilder) {
var result = true, i;
MatchersUtil.prototype.eq_ = function(a, b, aStack, bStack, customTesters, diffBuilder) {
var result = true, self = this, i;
var asymmetricResult = asymmetricMatch(a, b, customTesters, diffBuilder);
var asymmetricResult = this.asymmetricMatch_(a, b, customTesters, diffBuilder);
if (!j$.util.isUndefined(asymmetricResult)) {
return asymmetricResult;
}
@@ -230,7 +243,7 @@ getJasmineRequireObj().matchersUtil = function(j$) {
diffBuilder.record(a[i], void 0, actualArrayIsLongerFormatter);
result = false;
} else {
result = eq(i < aLength ? a[i] : void 0, i < bLength ? b[i] : void 0, aStack, bStack, customTesters, diffBuilder) && result;
result = self.eq_(i < aLength ? a[i] : void 0, i < bLength ? b[i] : void 0, aStack, bStack, customTesters, diffBuilder) && result;
}
});
}
@@ -271,12 +284,12 @@ getJasmineRequireObj().matchersUtil = function(j$) {
// otherwise explicitly look up the mapKey in the other Map since we want keys with unique
// obj identity (that are otherwise equal) to not match.
if (isAsymmetric(mapKey) || isAsymmetric(cmpKey) &&
eq(mapKey, cmpKey, aStack, bStack, customTesters, j$.NullDiffBuilder())) {
this.eq_(mapKey, cmpKey, aStack, bStack, customTesters, j$.NullDiffBuilder())) {
mapValueB = b.get(cmpKey);
} else {
mapValueB = b.get(mapKey);
}
result = eq(mapValueA, mapValueB, aStack, bStack, customTesters, j$.NullDiffBuilder());
result = this.eq_(mapValueA, mapValueB, aStack, bStack, customTesters, j$.NullDiffBuilder());
}
}
@@ -320,7 +333,7 @@ getJasmineRequireObj().matchersUtil = function(j$) {
otherValue = otherValues[l];
prevStackSize = baseStack.length;
// compare by value equality
found = eq(baseValue, otherValue, baseStack, otherStack, customTesters, j$.NullDiffBuilder());
found = this.eq_(baseValue, otherValue, baseStack, otherStack, customTesters, j$.NullDiffBuilder());
if (!found && prevStackSize !== baseStack.length) {
baseStack.splice(prevStackSize);
otherStack.splice(prevStackSize);
@@ -369,7 +382,7 @@ getJasmineRequireObj().matchersUtil = function(j$) {
}
diffBuilder.withPath(key, function() {
if(!eq(a[key], b[key], aStack, bStack, customTesters, diffBuilder)) {
if(!self.eq_(a[key], b[key], aStack, bStack, customTesters, diffBuilder)) {
result = false;
}
});
@@ -384,7 +397,7 @@ getJasmineRequireObj().matchersUtil = function(j$) {
bStack.pop();
return result;
}
};
function keys(obj, isArray) {
var allKeys = Object.keys ? Object.keys(obj) :
@@ -467,4 +480,10 @@ getJasmineRequireObj().matchersUtil = function(j$) {
}
return formatted;
}
function isDiffBuilder(obj) {
return obj && typeof obj.record === 'function';
}
return MatchersUtil;
};

View File

@@ -12,7 +12,7 @@ getJasmineRequireObj().toBeInstanceOf = function(j$) {
* expect(3).toBeInstanceOf(Number);
* expect(new Error()).toBeInstanceOf(Error);
*/
function toBeInstanceOf(util, customEqualityTesters) {
function toBeInstanceOf() {
return {
compare: function(actual, expected) {
var actualType = actual && actual.constructor ? j$.fnNameFor(actual.constructor) : j$.pp(actual),

View File

@@ -9,14 +9,12 @@ getJasmineRequireObj().toContain = function() {
* expect(array).toContain(anElement);
* expect(string).toContain(substring);
*/
function toContain(util, customEqualityTesters) {
customEqualityTesters = customEqualityTesters || [];
function toContain(util) {
return {
compare: function(actual, expected) {
return {
pass: util.contains(actual, expected, customEqualityTesters)
pass: util.contains(actual, expected)
};
}
};

View File

@@ -8,9 +8,7 @@ getJasmineRequireObj().toEqual = function(j$) {
* @example
* expect(bigObject).toEqual({"foo": ['bar', 'baz']});
*/
function toEqual(util, customEqualityTesters) {
customEqualityTesters = customEqualityTesters || [];
function toEqual(util) {
return {
compare: function(actual, expected) {
var result = {
@@ -18,7 +16,7 @@ getJasmineRequireObj().toEqual = function(j$) {
},
diffBuilder = j$.DiffBuilder();
result.pass = util.equals(actual, expected, customEqualityTesters, diffBuilder);
result.pass = util.equals(actual, expected, diffBuilder);
// TODO: only set error message if test fails
result.message = diffBuilder.getMessage();

View File

@@ -11,7 +11,7 @@ getJasmineRequireObj().toHaveBeenCalledWith = function(j$) {
* @example
* expect(mySpy).toHaveBeenCalledWith('foo', 'bar', 2);
*/
function toHaveBeenCalledWith(util, customEqualityTesters) {
function toHaveBeenCalledWith(util) {
return {
compare: function() {
var args = Array.prototype.slice.call(arguments, 0),
@@ -32,7 +32,7 @@ getJasmineRequireObj().toHaveBeenCalledWith = function(j$) {
return result;
}
if (util.contains(actual.calls.allArgs(), expectedArgs, customEqualityTesters)) {
if (util.contains(actual.calls.allArgs(), expectedArgs)) {
result.pass = true;
result.message = function() {
return 'Expected spy ' + actual.and.identity + ' not to have been called with:\n' +
@@ -47,7 +47,7 @@ getJasmineRequireObj().toHaveBeenCalledWith = function(j$) {
var diffs = actual.calls.allArgs().map(function(argsForCall, callIx) {
var diffBuilder = new j$.DiffBuilder();
util.equals(argsForCall, expectedArgs, customEqualityTesters, diffBuilder);
util.equals(argsForCall, expectedArgs, diffBuilder);
return 'Call ' + callIx + ':\n' +
diffBuilder.getMessage().replace(/^/mg, ' ');
});

View File

@@ -10,7 +10,7 @@ getJasmineRequireObj().toHaveClass = function(j$) {
* el.className = 'foo bar baz';
* expect(el).toHaveClass('bar');
*/
function toHaveClass(util, customEqualityTesters) {
function toHaveClass() {
return {
compare: function(actual, expected) {
if (!isElement(actual)) {