Merge branch 'add-map-set-matchers' of https://github.com/eventlistener/jasmine

* Merges #1741 from @eventlistener
This commit is contained in:
Steve Gravrock
2019-08-03 12:18:35 -07:00
9 changed files with 479 additions and 1 deletions

View File

@@ -0,0 +1,183 @@
describe('MapContaining', function() {
function MapI(iterable) { // for IE11
var map = new Map();
iterable.forEach(function(kv) {
map.set(kv[0], kv[1]);
});
return map;
}
beforeEach(function() {
jasmine.getEnv().requireFunctioningMaps();
});
it('matches any actual map to an empty map', function() {
var actualMap = new MapI([['foo', 'bar']]);
var containing = new jasmineUnderTest.MapContaining(new Map());
expect(containing.asymmetricMatch(actualMap)).toBe(true);
});
it('matches when all the key/value pairs in sample have matches in actual', function() {
var actualMap = new MapI([
['foo', [1, 2, 3]],
[{'foo': 'bar'}, 'baz'],
['other', 'any'],
]);
var containingMap = new MapI([
[{'foo': 'bar'}, 'baz'],
['foo', [1, 2, 3]],
]);
var containing = new jasmineUnderTest.MapContaining(containingMap);
expect(containing.asymmetricMatch(actualMap)).toBe(true);
});
it('does not match when a key is not in actual', function() {
var actualMap = new MapI([
['foo', [1, 2, 3]],
[{'foo': 'not a bar'}, 'baz'],
]);
var containingMap = new MapI([
[{'foo': 'bar'}, 'baz'],
['foo', [1, 2, 3]],
]);
var containing = new jasmineUnderTest.MapContaining(containingMap);
expect(containing.asymmetricMatch(actualMap)).toBe(false);
});
it('does not match when a value is not in actual', function() {
var actualMap = new MapI([
['foo', [1, 2, 3]],
[{'foo': 'bar'}, 'baz'],
]);
var containingMap = new MapI([
[{'foo': 'bar'}, 'baz'],
['foo', [1, 2]],
]);
var containing = new jasmineUnderTest.MapContaining(containingMap);
expect(containing.asymmetricMatch(actualMap)).toBe(false);
});
it('matches when all the key/value pairs in sample have asymmetric matches in actual', function() {
var actualMap = new MapI([
['foo1', 'not a bar'],
['foo2', 'bar'],
['baz', [1, 2, 3, 4]],
]);
var containingMap = new MapI([
[
jasmine.stringMatching(/^foo\d/),
'bar'
],
[
'baz',
jasmine.arrayContaining([2, 3])
],
]);
var containing = new jasmineUnderTest.MapContaining(containingMap);
expect(containing.asymmetricMatch(actualMap)).toBe(true);
});
it('does not match when a key in sample has no asymmetric matches in actual', function() {
var actualMap = new MapI([
['a-foo1', 'bar'],
['baz', [1, 2, 3, 4]],
]);
var containingMap = new MapI([
[
jasmine.stringMatching(/^foo\d/),
'bar'
],
[
'baz',
jasmine.arrayContaining([2, 3])
],
]);
var containing = new jasmineUnderTest.MapContaining(containingMap);
expect(containing.asymmetricMatch(actualMap)).toBe(false);
});
it('does not match when a value in sample has no asymmetric matches in actual', function() {
var actualMap = new MapI([
['foo1', 'bar'],
['baz', [1, 2, 3, 4]],
]);
var containingMap = new MapI([
[
jasmine.stringMatching(/^foo\d/),
'bar'
],
[
'baz',
jasmine.arrayContaining([4, 5])
],
]);
var containing = new jasmineUnderTest.MapContaining(containingMap);
expect(containing.asymmetricMatch(actualMap)).toBe(false);
});
it('matches recursively', function() {
var actualMap = new MapI([
['foo', new MapI([['foo1', 1], ['foo2', 2]])],
[new MapI([[1, 'bar1'], [2, 'bar2']]), 'bar'],
['other', 'any'],
]);
var containingMap = new MapI([
[
'foo',
new jasmineUnderTest.MapContaining(new MapI([['foo1', 1]]))
],
[
new jasmineUnderTest.MapContaining(new MapI([[2, 'bar2']])),
'bar'
],
]);
var containing = new jasmineUnderTest.MapContaining(containingMap);
expect(containing.asymmetricMatch(actualMap)).toBe(true);
});
it('uses custom equality testers', function() {
function tester(a, b) {
// treat all negative numbers as equal
return (typeof a == 'number' && typeof b == 'number') ? (a < 0 && b < 0) : a === b;
}
var actualMap = new MapI([['foo', -1]]);
var containing = new jasmineUnderTest.MapContaining(new MapI([['foo', -2]]));
expect(containing.asymmetricMatch(actualMap, [tester])).toBe(true);
});
it('does not match when actual is not a map', function() {
var containingMap = new MapI([['foo', 'bar']]);
expect(new jasmineUnderTest.MapContaining(containingMap).asymmetricMatch('foo')).toBe(false);
expect(new jasmineUnderTest.MapContaining(containingMap).asymmetricMatch(-1)).toBe(false);
expect(new jasmineUnderTest.MapContaining(containingMap).asymmetricMatch({'foo': 'bar'})).toBe(false);
});
it('throws an error when sample is not a map', function() {
expect(function() {
new jasmineUnderTest.MapContaining({'foo': 'bar'}).asymmetricMatch(new Map());
}).toThrowError(/You must provide a map/);
});
it('defines a `jasmineToString` method', function() {
var containing = new jasmineUnderTest.MapContaining(new Map());
expect(containing.jasmineToString()).toMatch(/^<jasmine\.mapContaining/);
});
});

View File

@@ -0,0 +1,118 @@
describe('SetContaining', function() {
function SetI(iterable) { // for IE11
var set = new Set();
iterable.forEach(function(v) {
set.add(v);
});
return set;
}
beforeEach(function() {
jasmine.getEnv().requireFunctioningSets();
});
it('matches any actual set to an empty set', function() {
var actualSet = new SetI(['foo', 'bar']);
var containing = new jasmineUnderTest.SetContaining(new Set());
expect(containing.asymmetricMatch(actualSet)).toBe(true);
});
it('matches when all the values in sample have matches in actual', function() {
var actualSet = new SetI([
{'foo': 'bar'}, 'baz', [1, 2, 3]
]);
var containingSet = new SetI([
[1, 2, 3], {'foo': 'bar'}
]);
var containing = new jasmineUnderTest.SetContaining(containingSet);
expect(containing.asymmetricMatch(actualSet)).toBe(true);
});
it('does not match when a value is not in actual', function() {
var actualSet = new SetI([
{'foo': 'bar'}, 'baz', [1, 2, 3]
]);
var containingSet = new SetI([
[1, 2], {'foo': 'bar'}
]);
var containing = new jasmineUnderTest.SetContaining(containingSet);
expect(containing.asymmetricMatch(actualSet)).toBe(false);
});
it('matches when all the values in sample have asymmetric matches in actual', function() {
var actualSet = new SetI([
[1, 2, 3, 4], 'other', 'foo1'
]);
var containingSet = new SetI([
jasmine.stringMatching(/^foo\d/),
jasmine.arrayContaining([2, 3]),
]);
var containing = new jasmineUnderTest.SetContaining(containingSet);
expect(containing.asymmetricMatch(actualSet)).toBe(true);
});
it('does not match when a value in sample has no asymmetric matches in actual', function() {
var actualSet = new SetI([
'a-foo1', [1, 2, 3, 4], 'other'
]);
var containingSet = new SetI([
jasmine.stringMatching(/^foo\d/),
jasmine.arrayContaining([2, 3]),
]);
var containing = new jasmineUnderTest.SetContaining(containingSet);
expect(containing.asymmetricMatch(actualSet)).toBe(false);
});
it('matches recursively', function() {
var actualSet = new SetI([
'foo', new SetI([1, 'bar', 2]), 'other'
]);
var containingSet = new SetI([
new jasmineUnderTest.SetContaining(new SetI(['bar'])), 'foo'
]);
var containing = new jasmineUnderTest.SetContaining(containingSet);
expect(containing.asymmetricMatch(actualSet)).toBe(true);
});
it('uses custom equality testers', function() {
function tester(a, b) {
// treat all negative numbers as equal
return (typeof a == 'number' && typeof b == 'number') ? (a < 0 && b < 0) : a === b;
}
var actualSet = new SetI(['foo', -1]);
var containing = new jasmineUnderTest.SetContaining(new SetI([-2, 'foo']));
expect(containing.asymmetricMatch(actualSet, [tester])).toBe(true);
});
it('does not match when actual is not a set', function() {
var containingSet = new SetI(['foo']);
expect(new jasmineUnderTest.SetContaining(containingSet).asymmetricMatch('foo')).toBe(false);
expect(new jasmineUnderTest.SetContaining(containingSet).asymmetricMatch(1)).toBe(false);
expect(new jasmineUnderTest.SetContaining(containingSet).asymmetricMatch(['foo'])).toBe(false);
});
it('throws an error when sample is not a set', function() {
expect(function() {
new jasmineUnderTest.SetContaining({'foo': 'bar'}).asymmetricMatch(new Set());
}).toThrowError(/You must provide a set/);
});
it('defines a `jasmineToString` method', function() {
var containing = new jasmineUnderTest.SetContaining(new Set());
expect(containing.jasmineToString()).toMatch(/^<jasmine\.setContaining/);
});
});

View File

@@ -290,6 +290,32 @@ describe("matchersUtil", function() {
expect(jasmineUnderTest.matchersUtil.equals(containing, obj)).toBe(true);
});
it("passes when MapContaining is used", function() {
jasmine.getEnv().requireFunctioningMaps();
var obj = new Map();
obj.set(1, 2);
obj.set('foo', 'bar');
var containing = new jasmineUnderTest.MapContaining(new Map());
containing.sample.set('foo', 'bar');
expect(jasmineUnderTest.matchersUtil.equals(obj, containing)).toBe(true);
expect(jasmineUnderTest.matchersUtil.equals(containing, obj)).toBe(true);
});
it("passes when SetContaining is used", function() {
jasmine.getEnv().requireFunctioningSets();
var obj = new Set();
obj.add(1);
obj.add('foo');
var containing = new jasmineUnderTest.SetContaining(new Set());
containing.sample.add(1);
expect(jasmineUnderTest.matchersUtil.equals(obj, containing)).toBe(true);
expect(jasmineUnderTest.matchersUtil.equals(containing, obj)).toBe(true);
});
it("passes when an asymmetric equality tester returns true", function() {
var tester = { asymmetricMatch: function(other) { return true; } };
@@ -613,6 +639,26 @@ describe("matchersUtil", function() {
testFunction('foo', 'bar');
expect(jasmineUnderTest.matchersUtil.contains(capturedArgs, 'bar')).toBe(true);
});
it("passes for set members", function() {
jasmine.getEnv().requireFunctioningSets();
var setItem = {'foo': 'bar'};
var set = new Set();
set.add(setItem);
expect(jasmineUnderTest.matchersUtil.contains(set, setItem)).toBe(true);
});
// documenting current behavior
it("fails (!) for objects that equal to a set member", function() {
jasmine.getEnv().requireFunctioningSets();
var set = new Set();
set.add({'foo': 'bar'});
expect(jasmineUnderTest.matchersUtil.contains(set, {'foo': 'bar'})).toBe(false);
});
});
describe("buildMessage", function() {

View File

@@ -0,0 +1,41 @@
getJasmineRequireObj().MapContaining = function(j$) {
function MapContaining(sample) {
if (!j$.isMap(sample)) {
throw new Error('You must provide a map to `mapContaining`, not ' + j$.pp(sample));
}
this.sample = sample;
}
MapContaining.prototype.asymmetricMatch = function(other, customTesters) {
if (!j$.isMap(other)) return false;
var hasAllMatches = true;
j$.util.forEachBreakable(this.sample, function(breakLoop, value, key) {
// for each key/value pair in `sample`
// there should be at least one pair in `other` whose key and value both match
var hasMatch = false;
j$.util.forEachBreakable(other, function(oBreakLoop, oValue, oKey) {
if (
j$.matchersUtil.equals(oKey, key, customTesters)
&& j$.matchersUtil.equals(oValue, value, customTesters)
) {
hasMatch = true;
oBreakLoop();
}
});
if (!hasMatch) {
hasAllMatches = false;
breakLoop();
}
});
return hasAllMatches;
};
MapContaining.prototype.jasmineToString = function() {
return '<jasmine.mapContaining(' + j$.pp(this.sample) + ')>';
};
return MapContaining;
};

View File

@@ -0,0 +1,39 @@
getJasmineRequireObj().SetContaining = function(j$) {
function SetContaining(sample) {
if (!j$.isSet(sample)) {
throw new Error('You must provide a set to `setContaining`, not ' + j$.pp(sample));
}
this.sample = sample;
}
SetContaining.prototype.asymmetricMatch = function(other, customTesters) {
if (!j$.isSet(other)) return false;
var hasAllMatches = true;
j$.util.forEachBreakable(this.sample, function(breakLoop, item) {
// for each item in `sample` there should be at least one matching item in `other`
// (not using `j$.matchersUtil.contains` because it compares set members by reference,
// not by deep value equality)
var hasMatch = false;
j$.util.forEachBreakable(other, function(oBreakLoop, oItem) {
if (j$.matchersUtil.equals(oItem, item, customTesters)) {
hasMatch = true;
oBreakLoop();
}
});
if (!hasMatch) {
hasAllMatches = false;
breakLoop();
}
});
return hasAllMatches;
};
SetContaining.prototype.jasmineToString = function() {
return '<jasmine.setContaining(' + j$.pp(this.sample) + ')>';
};
return SetContaining;
};

View File

@@ -128,6 +128,8 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) {
j$.isMap = function(obj) {
return (
obj !== null &&
typeof obj !== 'undefined' &&
typeof jasmineGlobal.Map !== 'undefined' &&
obj.constructor === jasmineGlobal.Map
);
@@ -135,6 +137,8 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) {
j$.isSet = function(obj) {
return (
obj !== null &&
typeof obj !== 'undefined' &&
typeof jasmineGlobal.Set !== 'undefined' &&
obj.constructor === jasmineGlobal.Set
);
@@ -279,6 +283,32 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) {
return new j$.ArrayWithExactContents(sample);
};
/**
* Get a matcher, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
* that will succeed if every key/value pair in the sample passes the deep equality comparison
* with at least one key/value pair in the actual value being compared
* @name jasmine.mapContaining
* @since
* @function
* @param {Map} sample - The subset of items that _must_ be in the actual.
*/
j$.mapContaining = function(sample) {
return new j$.MapContaining(sample);
};
/**
* Get a matcher, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}),
* that will succeed if every item in the sample passes the deep equality comparison
* with at least one item in the actual value being compared
* @name jasmine.mapContaining
* @since
* @function
* @param {Set} sample - The subset of items that _must_ be in the actual.
*/
j$.setContaining = function(sample) {
return new j$.SetContaining(sample);
};
j$.isSpy = function(putativeSpy) {
if (!putativeSpy) {
return false;

View File

@@ -7,7 +7,7 @@ getJasmineRequireObj().matchersUtil = function(j$) {
contains: function(haystack, needle, customTesters) {
customTesters = customTesters || [];
if ((Object.prototype.toString.apply(haystack) === '[object Set]')) {
if (j$.isSet(haystack)) {
return haystack.has(needle);
}

View File

@@ -55,6 +55,8 @@ var getJasmineRequireObj = (function(jasmineGlobal) {
j$.ObjectContaining = jRequire.ObjectContaining(j$);
j$.ArrayContaining = jRequire.ArrayContaining(j$);
j$.ArrayWithExactContents = jRequire.ArrayWithExactContents(j$);
j$.MapContaining = jRequire.MapContaining(j$);
j$.SetContaining = jRequire.SetContaining(j$);
j$.pp = jRequire.pp(j$);
j$.QueueRunner = jRequire.QueueRunner(j$);
j$.ReportDispatcher = jRequire.ReportDispatcher(j$);

View File

@@ -133,5 +133,24 @@ getJasmineRequireObj().util = function(j$) {
};
})();
function StopIteration() {}
StopIteration.prototype = Object.create(Error.prototype);
StopIteration.prototype.constructor = StopIteration;
// useful for maps and sets since `forEach` is the only IE11-compatible way to iterate them
util.forEachBreakable = function(iterable, iteratee) {
function breakLoop() {
throw new StopIteration();
}
try {
iterable.forEach(function(value, key) {
iteratee(breakLoop, value, key, iterable);
});
} catch (error) {
if (!(error instanceof StopIteration)) throw error;
}
};
return util;
};