diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index ca636eca..d05ae562 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -77,6 +77,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$); @@ -283,6 +285,8 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { j$.isMap = function(obj) { return ( + obj !== null && + typeof obj !== 'undefined' && typeof jasmineGlobal.Map !== 'undefined' && obj.constructor === jasmineGlobal.Map ); @@ -290,6 +294,8 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { j$.isSet = function(obj) { return ( + obj !== null && + typeof obj !== 'undefined' && typeof jasmineGlobal.Set !== 'undefined' && obj.constructor === jasmineGlobal.Set ); @@ -434,6 +440,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; @@ -580,6 +612,25 @@ 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; }; @@ -2247,6 +2298,48 @@ getJasmineRequireObj().Falsy = function(j$) { return Falsy; }; +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 ''; + }; + + return MapContaining; +}; + getJasmineRequireObj().NotEmpty = function (j$) { function NotEmpty() {} @@ -2324,6 +2417,46 @@ getJasmineRequireObj().ObjectContaining = function(j$) { return ObjectContaining; }; +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; +}; + getJasmineRequireObj().StringMatching = function(j$) { function StringMatching(expected) { @@ -3860,7 +3993,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); } @@ -5624,11 +5757,16 @@ getJasmineRequireObj().pp = function(j$) { function hasCustomToString(value) { // value.toString !== Object.prototype.toString if value has no custom toString but is from another context (e.g. // iframe, web worker) - return ( - j$.isFunction_(value.toString) && - value.toString !== Object.prototype.toString && - value.toString() !== Object.prototype.toString.call(value) - ); + try { + return ( + j$.isFunction_(value.toString) && + value.toString !== Object.prototype.toString && + value.toString() !== Object.prototype.toString.call(value) + ); + } catch (e) { + // The custom toString() threw. + return true; + } } PrettyPrinter.prototype.format = function(value) { @@ -5674,7 +5812,11 @@ getJasmineRequireObj().pp = function(j$) { !j$.isArray_(value) && hasCustomToString(value) ) { - this.emitScalar(value.toString()); + try { + this.emitScalar(value.toString()); + } catch (e) { + this.emitScalar('has-invalid-toString-method'); + } } else if (j$.util.arrayContains(this.seen, value)) { this.emitScalar( '