diff --git a/spec/core/asymmetric_equality/MapContainingSpec.js b/spec/core/asymmetric_equality/MapContainingSpec.js new file mode 100644 index 00000000..a214dce8 --- /dev/null +++ b/spec/core/asymmetric_equality/MapContainingSpec.js @@ -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(/^'; + }; + + return MapContaining; +}; diff --git a/src/core/asymmetric_equality/SetContaining.js b/src/core/asymmetric_equality/SetContaining.js new file mode 100644 index 00000000..d9dd8fd2 --- /dev/null +++ b/src/core/asymmetric_equality/SetContaining.js @@ -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 ''; + }; + + return SetContaining; +}; diff --git a/src/core/base.js b/src/core/base.js index deed4ef2..b962e129 100644 --- a/src/core/base.js +++ b/src/core/base.js @@ -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; diff --git a/src/core/matchers/matchersUtil.js b/src/core/matchers/matchersUtil.js index 14647dd6..cfc0c749 100644 --- a/src/core/matchers/matchersUtil.js +++ b/src/core/matchers/matchersUtil.js @@ -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); } diff --git a/src/core/requireCore.js b/src/core/requireCore.js index d6b98eb5..a2c5f177 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -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$); diff --git a/src/core/util.js b/src/core/util.js index 0c25382a..4118deec 100644 --- a/src/core/util.js +++ b/src/core/util.js @@ -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; };