diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 3577abe5..56ec0572 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -76,6 +76,9 @@ var getJasmineRequireObj = (function (jasmineGlobal) { j$.TreeProcessor = jRequire.TreeProcessor(); j$.version = jRequire.version(); j$.Order = jRequire.Order(); + j$.DiffBuilder = jRequire.DiffBuilder(j$); + j$.NullDiffBuilder = jRequire.NullDiffBuilder(j$); + j$.ObjectPath = jRequire.ObjectPath(j$); j$.matchers = jRequire.requireMatchers(jRequire, j$); @@ -309,6 +312,22 @@ getJasmineRequireObj().util = function() { return descriptor; }; + util.objectDifference = function(obj, toRemove) { + var diff = {}; + + for (var key in obj) { + if (util.has(obj, key) && !util.has(toRemove, key)) { + diff[key] = obj[key]; + } + } + + return diff; + }; + + util.has = function(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); + }; + return util; }; @@ -1110,6 +1129,161 @@ getJasmineRequireObj().JsApiReporter = function() { return JsApiReporter; }; +getJasmineRequireObj().Any = function(j$) { + + function Any(expectedObject) { + if (typeof expectedObject === 'undefined') { + throw new TypeError( + 'jasmine.any() expects to be passed a constructor function. ' + + 'Please pass one or use jasmine.anything() to match any object.' + ); + } + this.expectedObject = expectedObject; + } + + Any.prototype.asymmetricMatch = function(other) { + if (this.expectedObject == String) { + return typeof other == 'string' || other instanceof String; + } + + if (this.expectedObject == Number) { + return typeof other == 'number' || other instanceof Number; + } + + if (this.expectedObject == Function) { + return typeof other == 'function' || other instanceof Function; + } + + if (this.expectedObject == Object) { + return typeof other == 'object'; + } + + if (this.expectedObject == Boolean) { + return typeof other == 'boolean'; + } + + return other instanceof this.expectedObject; + }; + + Any.prototype.jasmineToString = function() { + return ''; + }; + + return Any; +}; + +getJasmineRequireObj().Anything = function(j$) { + + function Anything() {} + + Anything.prototype.asymmetricMatch = function(other) { + return !j$.util.isUndefined(other) && other !== null; + }; + + Anything.prototype.jasmineToString = function() { + return ''; + }; + + return Anything; +}; + +getJasmineRequireObj().ArrayContaining = function(j$) { + function ArrayContaining(sample) { + this.sample = sample; + } + + ArrayContaining.prototype.asymmetricMatch = function(other, customTesters) { + var className = Object.prototype.toString.call(this.sample); + if (className !== '[object Array]') { throw new Error('You must provide an array to arrayContaining, not \'' + this.sample + '\'.'); } + + for (var i = 0; i < this.sample.length; i++) { + var item = this.sample[i]; + if (!j$.matchersUtil.contains(other, item, customTesters)) { + return false; + } + } + + return true; + }; + + ArrayContaining.prototype.jasmineToString = function () { + return ''; + }; + + return ArrayContaining; +}; + +getJasmineRequireObj().ObjectContaining = function(j$) { + + function ObjectContaining(sample) { + this.sample = sample; + } + + function getPrototype(obj) { + if (Object.getPrototypeOf) { + return Object.getPrototypeOf(obj); + } + + if (obj.constructor.prototype == obj) { + return null; + } + + return obj.constructor.prototype; + } + + function hasProperty(obj, property) { + if (!obj) { + return false; + } + + if (Object.prototype.hasOwnProperty.call(obj, property)) { + return true; + } + + return hasProperty(getPrototype(obj), property); + } + + ObjectContaining.prototype.asymmetricMatch = function(other, customTesters) { + if (typeof(this.sample) !== 'object') { throw new Error('You must provide an object to objectContaining, not \''+this.sample+'\'.'); } + + for (var property in this.sample) { + if (!hasProperty(other, property) || + !j$.matchersUtil.equals(this.sample[property], other[property], customTesters)) { + return false; + } + } + + return true; + }; + + ObjectContaining.prototype.jasmineToString = function() { + return ''; + }; + + return ObjectContaining; +}; + +getJasmineRequireObj().StringMatching = function(j$) { + + function StringMatching(expected) { + if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) { + throw new Error('Expected is not a String or a RegExp'); + } + + this.regexp = new RegExp(expected); + } + + StringMatching.prototype.asymmetricMatch = function(other) { + return this.regexp.test(other); + }; + + StringMatching.prototype.jasmineToString = function() { + return ''; + }; + + return StringMatching; +}; + getJasmineRequireObj().CallTracker = function(j$) { function CallTracker() { @@ -1513,6 +1687,16 @@ getJasmineRequireObj().DelayedFunctionScheduler = function() { return DelayedFunctionScheduler; }; +getJasmineRequireObj().errors = function() { + function ExpectationFailed() {} + + ExpectationFailed.prototype = new Error(); + ExpectationFailed.prototype.constructor = ExpectationFailed; + + return { + ExpectationFailed: ExpectationFailed + }; +}; getJasmineRequireObj().ExceptionFormatter = function() { function ExceptionFormatter() { this.message = function(error) { @@ -1688,6 +1872,1030 @@ getJasmineRequireObj().buildExpectationResult = function() { return buildExpectationResult; }; +getJasmineRequireObj().formatErrorMsg = function() { + function generateErrorMsg(domain, usage) { + var usageDefinition = usage ? '\nUsage: ' + usage : ''; + + return function errorMsg(msg) { + return domain + ' : ' + msg + usageDefinition; + }; + } + + return generateErrorMsg; +}; + +getJasmineRequireObj().DiffBuilder = function(j$) { + return function DiffBuilder() { + var path = new j$.ObjectPath(), + mismatches = []; + + return { + record: function (actual, expected, formatter) { + formatter = formatter || defaultFormatter; + mismatches.push(formatter(actual, expected, path)); + }, + + getMessage: function () { + return mismatches.join('\n'); + }, + + withPath: function (pathComponent, block) { + var oldPath = path; + path = path.add(pathComponent); + block(); + path = oldPath; + } + }; + + function defaultFormatter (actual, expected, path) { + return 'Expected ' + + path + (path.depth() ? ' = ' : '') + + j$.pp(actual) + + ' to equal ' + + j$.pp(expected) + + '.'; + } + }; +}; + +getJasmineRequireObj().matchersUtil = function(j$) { + // TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter? + + return { + equals: equals, + + contains: function(haystack, needle, customTesters) { + customTesters = customTesters || []; + + if ((Object.prototype.toString.apply(haystack) === '[object Set]')) { + 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 + '.'; + } + }; + + function isAsymmetric(obj) { + return obj && j$.isA_('Function', obj.asymmetricMatch); + } + + function asymmetricMatch(a, b, customTesters, diffBuilder) { + var asymmetricA = isAsymmetric(a), + asymmetricB = isAsymmetric(b), + result; + + if (asymmetricA && asymmetricB) { + return undefined; + } + + if (asymmetricA) { + result = a.asymmetricMatch(b, customTesters); + diffBuilder.record(a, b); + return result; + } + + if (asymmetricB) { + result = b.asymmetricMatch(a, customTesters); + diffBuilder.record(a, b); + return result; + } + } + + function equals(a, b, customTesters, diffBuilder) { + customTesters = customTesters || []; + diffBuilder = diffBuilder || j$.NullDiffBuilder(); + + return 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; + + var asymmetricResult = asymmetricMatch(a, b, customTesters, diffBuilder); + if (!j$.util.isUndefined(asymmetricResult)) { + return asymmetricResult; + } + + for (i = 0; i < customTesters.length; i++) { + var customTesterResult = customTesters[i](a, b); + if (!j$.util.isUndefined(customTesterResult)) { + if (!customTesterResult) { + diffBuilder.record(a, b); + } + return customTesterResult; + } + } + + if (a instanceof Error && b instanceof Error) { + result = a.message == b.message; + if (!result) { + diffBuilder.record(a, b); + } + return result; + } + + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) { + result = a !== 0 || 1 / a == 1 / b; + if (!result) { + diffBuilder.record(a, b); + } + return result; + } + // A strict comparison is necessary because `null == undefined`. + if (a === null || b === null) { + result = a === b; + if (!result) { + diffBuilder.record(a, b); + } + return result; + } + var className = Object.prototype.toString.call(a); + if (className != Object.prototype.toString.call(b)) { + diffBuilder.record(a, b); + return false; + } + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + result = a == String(b); + if (!result) { + diffBuilder.record(a, b); + } + return result; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + result = a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b); + if (!result) { + diffBuilder.record(a, b); + } + return result; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + result = +a == +b; + if (!result) { + diffBuilder.record(a, b); + } + return result; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') { + diffBuilder.record(a, b); + return false; + } + + var aIsDomNode = j$.isDomNode(a); + var bIsDomNode = j$.isDomNode(b); + if (aIsDomNode && bIsDomNode) { + // At first try to use DOM3 method isEqualNode + if (a.isEqualNode) { + result = a.isEqualNode(b); + if (!result) { + diffBuilder.record(a, b); + } + return result; + } + // IE8 doesn't support isEqualNode, try to use outerHTML && innerText + var aIsElement = a instanceof Element; + var bIsElement = b instanceof Element; + if (aIsElement && bIsElement) { + return a.outerHTML == b.outerHTML; + } + if (aIsElement || bIsElement) { + return false; + } + return a.innerText == b.innerText && a.textContent == b.textContent; + } + if (aIsDomNode || bIsDomNode) { + return false; + } + + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) { return bStack[length] == b; } + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0; + // Recursively compare objects and arrays. + // Compare array lengths to determine if a deep comparison is necessary. + if (className == '[object Array]') { + size = a.length; + if (size !== b.length) { + diffBuilder.record(a, b); + return false; + } + + for (i = 0; i < size; i++) { + diffBuilder.withPath(i, function() { + result = eq(a[i], b[i], aStack, bStack, customTesters, diffBuilder) && result; + }); + } + if (!result) { + return false; + } + } else if (className == '[object Set]') { + if (a.size != b.size) { + diffBuilder.record(a, b); + return false; + } + var iterA = a.values(), iterB = b.values(); + var valA, valB; + do { + valA = iterA.next(); + valB = iterB.next(); + if (!eq(valA.value, valB.value, aStack, bStack, customTesters, j$.NullDiffBuilder())) { + diffBuilder.record(a, b); + return false; + } + } while (!valA.done && !valB.done); + } else { + + // Objects with different constructors are not equivalent, but `Object`s + // or `Array`s from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && + isFunction(aCtor) && isFunction(bCtor) && + a instanceof aCtor && b instanceof bCtor && + !(aCtor instanceof aCtor && bCtor instanceof bCtor)) { + + diffBuilder.record(a, b, constructorsAreDifferentFormatter); + return false; + } + } + + // Deep compare objects. + var aKeys = keys(a, className == '[object Array]'), key; + size = aKeys.length; + + // 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); + return false; + } + + for (i = 0; i < size; i++) { + key = aKeys[i]; + // Deep compare each member + if (!j$.util.has(b, key)) { + diffBuilder.record(a, b, objectKeysAreDifferentFormatter); + result = false; + continue; + } + + diffBuilder.withPath(key, function() { + if(!eq(a[key], b[key], aStack, bStack, customTesters, diffBuilder)) { + result = false; + } + }); + } + + if (!result) { + return false; + } + + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + + return result; + } + + function keys(obj, isArray) { + var allKeys = Object.keys ? Object.keys(obj) : + (function(o) { + var keys = []; + for (var key in o) { + if (j$.util.has(o, key)) { + keys.push(key); + } + } + return keys; + })(obj); + + if (!isArray) { + return allKeys; + } + + if (allKeys.length === 0) { + return allKeys; + } + + var extraKeys = []; + for (var i in allKeys) { + if (!allKeys[i].match(/^[0-9]+$/)) { + extraKeys.push(allKeys[i]); + } + } + + return extraKeys; + } + + function has(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); + } + + function isFunction(obj) { + return typeof obj === 'function'; + } + + function objectKeysAreDifferentFormatter(actual, expected, path) { + var missingProperties = j$.util.objectDifference(expected, actual), + extraProperties = j$.util.objectDifference(actual, expected), + missingPropertiesMessage = formatKeyValuePairs(missingProperties), + extraPropertiesMessage = formatKeyValuePairs(extraProperties), + messages = []; + + if (!path.depth()) { + path = 'object'; + } + + if (missingPropertiesMessage.length) { + messages.push('Expected ' + path + ' to have properties' + missingPropertiesMessage); + } + + if (extraPropertiesMessage.length) { + messages.push('Expected ' + path + ' not to have properties' + extraPropertiesMessage); + } + + return messages.join('\n'); + } + + function constructorsAreDifferentFormatter(actual, expected, path) { + if (!path.depth()) { + path = 'object'; + } + + return 'Expected ' + + path + ' to be a kind of ' + + expected.constructor.name + + ', but was ' + j$.pp(actual) + '.'; + } + + function formatKeyValuePairs(obj) { + var formatted = ''; + for (var key in obj) { + formatted += '\n ' + key + ': ' + j$.pp(obj[key]); + } + return formatted; + } +}; + +getJasmineRequireObj().NullDiffBuilder = function(j$) { + return function() { + return { + withPath: function(_, block) { + block(); + }, + record: function() {} + }; + }; +}; + +getJasmineRequireObj().ObjectPath = function(j$) { + function ObjectPath(components) { + this.components = components || []; + } + + ObjectPath.prototype.toString = function() { + if (this.components.length) { + return '$' + map(this.components, formatPropertyAccess).join(''); + } else { + return ''; + } + }; + + ObjectPath.prototype.add = function(component) { + return new ObjectPath(this.components.concat([component])); + }; + + ObjectPath.prototype.depth = function() { + return this.components.length; + }; + + function formatPropertyAccess(prop) { + if (typeof prop === 'number') { + return '[' + prop + ']'; + } + + if (isValidIdentifier(prop)) { + return '.' + prop; + } + + return '[\'' + prop + '\']'; + } + + function map(array, fn) { + var results = []; + for (var i = 0; i < array.length; i++) { + results.push(fn(array[i])); + } + return results; + } + + function isValidIdentifier(string) { + return /^[A-Za-z\$_][A-Za-z0-9\$_]*$/.test(string); + } + + return ObjectPath; +}; + +getJasmineRequireObj().toBe = function() { + function toBe() { + return { + compare: function(actual, expected) { + return { + pass: actual === expected + }; + } + }; + } + + return toBe; +}; + +getJasmineRequireObj().toBeCloseTo = function() { + + function toBeCloseTo() { + return { + compare: function(actual, expected, precision) { + if (precision !== 0) { + precision = precision || 2; + } + + return { + pass: Math.abs(expected - actual) < (Math.pow(10, -precision) / 2) + }; + } + }; + } + + return toBeCloseTo; +}; + +getJasmineRequireObj().toBeDefined = function() { + function toBeDefined() { + return { + compare: function(actual) { + return { + pass: (void 0 !== actual) + }; + } + }; + } + + return toBeDefined; +}; + +getJasmineRequireObj().toBeFalsy = function() { + function toBeFalsy() { + return { + compare: function(actual) { + return { + pass: !!!actual + }; + } + }; + } + + return toBeFalsy; +}; + +getJasmineRequireObj().toBeGreaterThan = function() { + + function toBeGreaterThan() { + return { + compare: function(actual, expected) { + return { + pass: actual > expected + }; + } + }; + } + + return toBeGreaterThan; +}; + + +getJasmineRequireObj().toBeGreaterThanOrEqual = function() { + + function toBeGreaterThanOrEqual() { + return { + compare: function(actual, expected) { + return { + pass: actual >= expected + }; + } + }; + } + + return toBeGreaterThanOrEqual; +}; + +getJasmineRequireObj().toBeLessThan = function() { + function toBeLessThan() { + return { + + compare: function(actual, expected) { + return { + pass: actual < expected + }; + } + }; + } + + return toBeLessThan; +}; +getJasmineRequireObj().toBeLessThanOrEqual = function() { + function toBeLessThanOrEqual() { + return { + + compare: function(actual, expected) { + return { + pass: actual <= expected + }; + } + }; + } + + return toBeLessThanOrEqual; +}; + +getJasmineRequireObj().toBeNaN = function(j$) { + + function toBeNaN() { + return { + compare: function(actual) { + var result = { + pass: (actual !== actual) + }; + + if (result.pass) { + result.message = 'Expected actual not to be NaN.'; + } else { + result.message = function() { return 'Expected ' + j$.pp(actual) + ' to be NaN.'; }; + } + + return result; + } + }; + } + + return toBeNaN; +}; + +getJasmineRequireObj().toBeNull = function() { + + function toBeNull() { + return { + compare: function(actual) { + return { + pass: actual === null + }; + } + }; + } + + return toBeNull; +}; + +getJasmineRequireObj().toBeTruthy = function() { + + function toBeTruthy() { + return { + compare: function(actual) { + return { + pass: !!actual + }; + } + }; + } + + return toBeTruthy; +}; + +getJasmineRequireObj().toBeUndefined = function() { + + function toBeUndefined() { + return { + compare: function(actual) { + return { + pass: void 0 === actual + }; + } + }; + } + + return toBeUndefined; +}; + +getJasmineRequireObj().toContain = function() { + function toContain(util, customEqualityTesters) { + customEqualityTesters = customEqualityTesters || []; + + return { + compare: function(actual, expected) { + + return { + pass: util.contains(actual, expected, customEqualityTesters) + }; + } + }; + } + + return toContain; +}; + +getJasmineRequireObj().toEqual = function(j$) { + + function toEqual(util, customEqualityTesters) { + customEqualityTesters = customEqualityTesters || []; + + return { + compare: function(actual, expected) { + var result = { + pass: false + }, + diffBuilder = j$.DiffBuilder(); + + result.pass = util.equals(actual, expected, customEqualityTesters, diffBuilder); + + // TODO: only set error message if test fails + result.message = diffBuilder.getMessage(); + + return result; + } + }; + } + + return toEqual; +}; + +getJasmineRequireObj().toHaveBeenCalled = function(j$) { + + var getErrorMsg = j$.formatErrorMsg('', 'expect().toHaveBeenCalled()'); + + function toHaveBeenCalled() { + return { + compare: function(actual) { + var result = {}; + + if (!j$.isSpy(actual)) { + throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(actual) + '.')); + } + + if (arguments.length > 1) { + throw new Error(getErrorMsg('Does not take arguments, use toHaveBeenCalledWith')); + } + + result.pass = actual.calls.any(); + + result.message = result.pass ? + 'Expected spy ' + actual.and.identity() + ' not to have been called.' : + 'Expected spy ' + actual.and.identity() + ' to have been called.'; + + return result; + } + }; + } + + return toHaveBeenCalled; +}; + +getJasmineRequireObj().toHaveBeenCalledTimes = function(j$) { + + var getErrorMsg = j$.formatErrorMsg('', 'expect().toHaveBeenCalledTimes()'); + + function toHaveBeenCalledTimes() { + return { + compare: function(actual, expected) { + if (!j$.isSpy(actual)) { + throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(actual) + '.')); + } + + var args = Array.prototype.slice.call(arguments, 0), + result = { pass: false }; + + if (!j$.isNumber_(expected)){ + throw new Error(getErrorMsg('The expected times failed is a required argument and must be a number.')); + } + + actual = args[0]; + var calls = actual.calls.count(); + var timesMessage = expected === 1 ? 'once' : expected + ' times'; + result.pass = calls === expected; + result.message = result.pass ? + 'Expected spy ' + actual.and.identity() + ' not to have been called ' + timesMessage + '. It was called ' + calls + ' times.' : + 'Expected spy ' + actual.and.identity() + ' to have been called ' + timesMessage + '. It was called ' + calls + ' times.'; + return result; + } + }; + } + + return toHaveBeenCalledTimes; +}; + +getJasmineRequireObj().toHaveBeenCalledWith = function(j$) { + + var getErrorMsg = j$.formatErrorMsg('', 'expect().toHaveBeenCalledWith(...arguments)'); + + function toHaveBeenCalledWith(util, customEqualityTesters) { + return { + compare: function() { + var args = Array.prototype.slice.call(arguments, 0), + actual = args[0], + expectedArgs = args.slice(1), + result = { pass: false }; + + if (!j$.isSpy(actual)) { + throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(actual) + '.')); + } + + if (!actual.calls.any()) { + result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + j$.pp(expectedArgs) + ' but it was never called.'; }; + return result; + } + + if (util.contains(actual.calls.allArgs(), expectedArgs, customEqualityTesters)) { + result.pass = true; + result.message = function() { return 'Expected spy ' + actual.and.identity() + ' not to have been called with ' + j$.pp(expectedArgs) + ' but it was.'; }; + } else { + result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + j$.pp(expectedArgs) + ' but actual calls were ' + j$.pp(actual.calls.allArgs()).replace(/^\[ | \]$/g, '') + '.'; }; + } + + return result; + } + }; + } + + return toHaveBeenCalledWith; +}; + +getJasmineRequireObj().toMatch = function(j$) { + + var getErrorMsg = j$.formatErrorMsg('', 'expect().toMatch( || )'); + + function toMatch() { + return { + compare: function(actual, expected) { + if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) { + throw new Error(getErrorMsg('Expected is not a String or a RegExp')); + } + + var regexp = new RegExp(expected); + + return { + pass: regexp.test(actual) + }; + } + }; + } + + return toMatch; +}; + +getJasmineRequireObj().toThrow = function(j$) { + + var getErrorMsg = j$.formatErrorMsg('', 'expect(function() {}).toThrow()'); + + function toThrow(util) { + return { + compare: function(actual, expected) { + var result = { pass: false }, + threw = false, + thrown; + + if (typeof actual != 'function') { + throw new Error(getErrorMsg('Actual is not a Function')); + } + + try { + actual(); + } catch (e) { + threw = true; + thrown = e; + } + + if (!threw) { + result.message = 'Expected function to throw an exception.'; + return result; + } + + if (arguments.length == 1) { + result.pass = true; + result.message = function() { return 'Expected function not to throw, but it threw ' + j$.pp(thrown) + '.'; }; + + return result; + } + + if (util.equals(thrown, expected)) { + result.pass = true; + result.message = function() { return 'Expected function not to throw ' + j$.pp(expected) + '.'; }; + } else { + result.message = function() { return 'Expected function to throw ' + j$.pp(expected) + ', but it threw ' + j$.pp(thrown) + '.'; }; + } + + return result; + } + }; + } + + return toThrow; +}; + +getJasmineRequireObj().toThrowError = function(j$) { + + var getErrorMsg = j$.formatErrorMsg('', 'expect(function() {}).toThrowError(, )'); + + function toThrowError () { + return { + compare: function(actual) { + var threw = false, + pass = {pass: true}, + fail = {pass: false}, + thrown; + + if (typeof actual != 'function') { + throw new Error(getErrorMsg('Actual is not a Function')); + } + + var errorMatcher = getMatcher.apply(null, arguments); + + try { + actual(); + } catch (e) { + threw = true; + thrown = e; + } + + if (!threw) { + fail.message = 'Expected function to throw an Error.'; + return fail; + } + + if (!(thrown instanceof Error)) { + fail.message = function() { return 'Expected function to throw an Error, but it threw ' + j$.pp(thrown) + '.'; }; + return fail; + } + + if (errorMatcher.hasNoSpecifics()) { + pass.message = 'Expected function not to throw an Error, but it threw ' + j$.fnNameFor(thrown) + '.'; + return pass; + } + + if (errorMatcher.matches(thrown)) { + pass.message = function() { + return 'Expected function not to throw ' + errorMatcher.errorTypeDescription + errorMatcher.messageDescription() + '.'; + }; + return pass; + } else { + fail.message = function() { + return 'Expected function to throw ' + errorMatcher.errorTypeDescription + errorMatcher.messageDescription() + + ', but it threw ' + errorMatcher.thrownDescription(thrown) + '.'; + }; + return fail; + } + } + }; + + function getMatcher() { + var expected = null, + errorType = null; + + if (arguments.length == 2) { + expected = arguments[1]; + if (isAnErrorType(expected)) { + errorType = expected; + expected = null; + } + } else if (arguments.length > 2) { + errorType = arguments[1]; + expected = arguments[2]; + if (!isAnErrorType(errorType)) { + throw new Error(getErrorMsg('Expected error type is not an Error.')); + } + } + + if (expected && !isStringOrRegExp(expected)) { + if (errorType) { + throw new Error(getErrorMsg('Expected error message is not a string or RegExp.')); + } else { + throw new Error(getErrorMsg('Expected is not an Error, string, or RegExp.')); + } + } + + function messageMatch(message) { + if (typeof expected == 'string') { + return expected == message; + } else { + return expected.test(message); + } + } + + return { + errorTypeDescription: errorType ? j$.fnNameFor(errorType) : 'an exception', + thrownDescription: function(thrown) { + var thrownName = errorType ? j$.fnNameFor(thrown.constructor) : 'an exception', + thrownMessage = ''; + + if (expected) { + thrownMessage = ' with message ' + j$.pp(thrown.message); + } + + return thrownName + thrownMessage; + }, + messageDescription: function() { + if (expected === null) { + return ''; + } else if (expected instanceof RegExp) { + return ' with a message matching ' + j$.pp(expected); + } else { + return ' with message ' + j$.pp(expected); + } + }, + hasNoSpecifics: function() { + return expected === null && errorType === null; + }, + matches: function(error) { + return (errorType === null || error instanceof errorType) && + (expected === null || messageMatch(error.message)); + } + }; + } + + function isStringOrRegExp(potential) { + return potential instanceof RegExp || (typeof potential == 'string'); + } + + function isAnErrorType(type) { + if (typeof type !== 'function') { + return false; + } + + var Surrogate = function() {}; + Surrogate.prototype = type.prototype; + return (new Surrogate()) instanceof Error; + } + } + + return toThrowError; +}; + getJasmineRequireObj().MockDate = function() { function MockDate(global) { var self = this; @@ -1918,7 +3126,13 @@ getJasmineRequireObj().pp = function(j$) { }; StringPrettyPrinter.prototype.emitObject = function(obj) { - var constructorName = obj.constructor ? j$.fnNameFor(obj.constructor) : 'null'; + var ctor = obj.constructor, + constructorName; + + constructorName = typeof ctor === 'function' && obj instanceof ctor ? + j$.fnNameFor(obj.constructor) : + 'null'; + this.append(constructorName); if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { @@ -2113,6 +3327,90 @@ getJasmineRequireObj().ReportDispatcher = function() { }; +getJasmineRequireObj().interface = function(jasmine, env) { + var jasmineInterface = { + describe: function(description, specDefinitions) { + return env.describe(description, specDefinitions); + }, + + xdescribe: function(description, specDefinitions) { + return env.xdescribe(description, specDefinitions); + }, + + fdescribe: function(description, specDefinitions) { + return env.fdescribe(description, specDefinitions); + }, + + it: function() { + return env.it.apply(env, arguments); + }, + + xit: function() { + return env.xit.apply(env, arguments); + }, + + fit: function() { + return env.fit.apply(env, arguments); + }, + + beforeEach: function() { + return env.beforeEach.apply(env, arguments); + }, + + afterEach: function() { + return env.afterEach.apply(env, arguments); + }, + + beforeAll: function() { + return env.beforeAll.apply(env, arguments); + }, + + afterAll: function() { + return env.afterAll.apply(env, arguments); + }, + + expect: function(actual) { + return env.expect(actual); + }, + + pending: function() { + return env.pending.apply(env, arguments); + }, + + fail: function() { + return env.fail.apply(env, arguments); + }, + + spyOn: function(obj, methodName) { + return env.spyOn(obj, methodName); + }, + + spyOnProperty: function(obj, methodName, accessType) { + return env.spyOnProperty(obj, methodName, accessType); + }, + + jsApiReporter: new jasmine.JsApiReporter({ + timer: new jasmine.Timer() + }), + + jasmine: jasmine + }; + + jasmine.addCustomEqualityTester = function(tester) { + env.addCustomEqualityTester(tester); + }; + + jasmine.addMatchers = function(matchers) { + return env.addMatchers(matchers); + }; + + jasmine.clock = function() { + return env.clock; + }; + + return jasmineInterface; +}; + getJasmineRequireObj().Spy = function (j$) { function Spy(name, originalFn) { @@ -2784,1077 +4082,6 @@ getJasmineRequireObj().TreeProcessor = function() { return TreeProcessor; }; -getJasmineRequireObj().Any = function(j$) { - - function Any(expectedObject) { - if (typeof expectedObject === 'undefined') { - throw new TypeError( - 'jasmine.any() expects to be passed a constructor function. ' + - 'Please pass one or use jasmine.anything() to match any object.' - ); - } - this.expectedObject = expectedObject; - } - - Any.prototype.asymmetricMatch = function(other) { - if (this.expectedObject == String) { - return typeof other == 'string' || other instanceof String; - } - - if (this.expectedObject == Number) { - return typeof other == 'number' || other instanceof Number; - } - - if (this.expectedObject == Function) { - return typeof other == 'function' || other instanceof Function; - } - - if (this.expectedObject == Object) { - return typeof other == 'object'; - } - - if (this.expectedObject == Boolean) { - return typeof other == 'boolean'; - } - - return other instanceof this.expectedObject; - }; - - Any.prototype.jasmineToString = function() { - return ''; - }; - - return Any; -}; - -getJasmineRequireObj().Anything = function(j$) { - - function Anything() {} - - Anything.prototype.asymmetricMatch = function(other) { - return !j$.util.isUndefined(other) && other !== null; - }; - - Anything.prototype.jasmineToString = function() { - return ''; - }; - - return Anything; -}; - -getJasmineRequireObj().ArrayContaining = function(j$) { - function ArrayContaining(sample) { - this.sample = sample; - } - - ArrayContaining.prototype.asymmetricMatch = function(other, customTesters) { - var className = Object.prototype.toString.call(this.sample); - if (className !== '[object Array]') { throw new Error('You must provide an array to arrayContaining, not \'' + this.sample + '\'.'); } - - for (var i = 0; i < this.sample.length; i++) { - var item = this.sample[i]; - if (!j$.matchersUtil.contains(other, item, customTesters)) { - return false; - } - } - - return true; - }; - - ArrayContaining.prototype.jasmineToString = function () { - return ''; - }; - - return ArrayContaining; -}; - -getJasmineRequireObj().ObjectContaining = function(j$) { - - function ObjectContaining(sample) { - this.sample = sample; - } - - function getPrototype(obj) { - if (Object.getPrototypeOf) { - return Object.getPrototypeOf(obj); - } - - if (obj.constructor.prototype == obj) { - return null; - } - - return obj.constructor.prototype; - } - - function hasProperty(obj, property) { - if (!obj) { - return false; - } - - if (Object.prototype.hasOwnProperty.call(obj, property)) { - return true; - } - - return hasProperty(getPrototype(obj), property); - } - - ObjectContaining.prototype.asymmetricMatch = function(other, customTesters) { - if (typeof(this.sample) !== 'object') { throw new Error('You must provide an object to objectContaining, not \''+this.sample+'\'.'); } - - for (var property in this.sample) { - if (!hasProperty(other, property) || - !j$.matchersUtil.equals(this.sample[property], other[property], customTesters)) { - return false; - } - } - - return true; - }; - - ObjectContaining.prototype.jasmineToString = function() { - return ''; - }; - - return ObjectContaining; -}; - -getJasmineRequireObj().StringMatching = function(j$) { - - function StringMatching(expected) { - if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) { - throw new Error('Expected is not a String or a RegExp'); - } - - this.regexp = new RegExp(expected); - } - - StringMatching.prototype.asymmetricMatch = function(other) { - return this.regexp.test(other); - }; - - StringMatching.prototype.jasmineToString = function() { - return ''; - }; - - return StringMatching; -}; - -getJasmineRequireObj().errors = function() { - function ExpectationFailed() {} - - ExpectationFailed.prototype = new Error(); - ExpectationFailed.prototype.constructor = ExpectationFailed; - - return { - ExpectationFailed: ExpectationFailed - }; -}; -getJasmineRequireObj().formatErrorMsg = function() { - function generateErrorMsg(domain, usage) { - var usageDefinition = usage ? '\nUsage: ' + usage : ''; - - return function errorMsg(msg) { - return domain + ' : ' + msg + usageDefinition; - }; - } - - return generateErrorMsg; -}; - -getJasmineRequireObj().matchersUtil = function(j$) { - // TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter? - - return { - equals: function(a, b, customTesters) { - customTesters = customTesters || []; - - return eq(a, b, [], [], customTesters); - }, - - contains: function(haystack, needle, customTesters) { - customTesters = customTesters || []; - - if ((Object.prototype.toString.apply(haystack) === '[object Set]')) { - return haystack.has(needle); - } - - if ((Object.prototype.toString.apply(haystack) === '[object Array]') || - (!!haystack && !haystack.indexOf)) - { - for (var i = 0; i < haystack.length; i++) { - if (eq(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 + '.'; - } - }; - - function isAsymmetric(obj) { - return obj && j$.isA_('Function', obj.asymmetricMatch); - } - - function asymmetricMatch(a, b, customTesters) { - var asymmetricA = isAsymmetric(a), - asymmetricB = isAsymmetric(b); - - if (asymmetricA && asymmetricB) { - return undefined; - } - - if (asymmetricA) { - return a.asymmetricMatch(b, customTesters); - } - - if (asymmetricB) { - return b.asymmetricMatch(a, customTesters); - } - } - - // Equality function lovingly adapted from isEqual in - // [Underscore](http://underscorejs.org) - function eq(a, b, aStack, bStack, customTesters) { - var result = true; - - var asymmetricResult = asymmetricMatch(a, b, customTesters); - if (!j$.util.isUndefined(asymmetricResult)) { - return asymmetricResult; - } - - for (var i = 0; i < customTesters.length; i++) { - var customTesterResult = customTesters[i](a, b); - if (!j$.util.isUndefined(customTesterResult)) { - return customTesterResult; - } - } - - if (a instanceof Error && b instanceof Error) { - return a.message == b.message; - } - - // Identical objects are equal. `0 === -0`, but they aren't identical. - // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). - if (a === b) { return a !== 0 || 1 / a == 1 / b; } - // A strict comparison is necessary because `null == undefined`. - if (a === null || b === null) { return a === b; } - var className = Object.prototype.toString.call(a); - if (className != Object.prototype.toString.call(b)) { return false; } - switch (className) { - // Strings, numbers, dates, and booleans are compared by value. - case '[object String]': - // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is - // equivalent to `new String("5")`. - return a == String(b); - case '[object Number]': - // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for - // other numeric values. - return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b); - case '[object Date]': - case '[object Boolean]': - // Coerce dates and booleans to numeric primitive values. Dates are compared by their - // millisecond representations. Note that invalid dates with millisecond representations - // of `NaN` are not equivalent. - return +a == +b; - // RegExps are compared by their source patterns and flags. - case '[object RegExp]': - return a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; - } - if (typeof a != 'object' || typeof b != 'object') { return false; } - - var aIsDomNode = j$.isDomNode(a); - var bIsDomNode = j$.isDomNode(b); - if (aIsDomNode && bIsDomNode) { - // At first try to use DOM3 method isEqualNode - if (a.isEqualNode) { - return a.isEqualNode(b); - } - // IE8 doesn't support isEqualNode, try to use outerHTML && innerText - var aIsElement = a instanceof Element; - var bIsElement = b instanceof Element; - if (aIsElement && bIsElement) { - return a.outerHTML == b.outerHTML; - } - if (aIsElement || bIsElement) { - return false; - } - return a.innerText == b.innerText && a.textContent == b.textContent; - } - if (aIsDomNode || bIsDomNode) { - return false; - } - - // Assume equality for cyclic structures. The algorithm for detecting cyclic - // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - var length = aStack.length; - while (length--) { - // Linear search. Performance is inversely proportional to the number of - // unique nested structures. - if (aStack[length] == a) { return bStack[length] == b; } - } - // Add the first object to the stack of traversed objects. - aStack.push(a); - bStack.push(b); - var size = 0; - // Recursively compare objects and arrays. - // Compare array lengths to determine if a deep comparison is necessary. - if (className == '[object Array]') { - size = a.length; - if (size !== b.length) { - return false; - } - - while (size--) { - result = eq(a[size], b[size], aStack, bStack, customTesters); - if (!result) { - return false; - } - } - } else if (className == '[object Set]') { - if (a.size != b.size) { - return false; - } - var iterA = a.values(), iterB = b.values(); - var valA, valB; - do { - valA = iterA.next(); - valB = iterB.next(); - if (!eq(valA.value, valB.value, aStack, bStack, customTesters)) { - return false; - } - } while (!valA.done && !valB.done); - } else { - - // Objects with different constructors are not equivalent, but `Object`s - // or `Array`s from different frames are. - var aCtor = a.constructor, bCtor = b.constructor; - if (aCtor !== bCtor && !(isObjectConstructor(aCtor) && - isObjectConstructor(bCtor))) { - return false; - } - } - - // Deep compare objects. - var aKeys = keys(a, className == '[object Array]'), key; - size = aKeys.length; - - // Ensure that both objects contain the same number of properties before comparing deep equality. - if (keys(b, className == '[object Array]').length !== size) { return false; } - - while (size--) { - key = aKeys[size]; - // Deep compare each member - result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters); - - if (!result) { - return false; - } - } - // Remove the first object from the stack of traversed objects. - aStack.pop(); - bStack.pop(); - - return result; - - function keys(obj, isArray) { - var allKeys = Object.keys ? Object.keys(obj) : - (function(o) { - var keys = []; - for (var key in o) { - if (has(o, key)) { - keys.push(key); - } - } - return keys; - })(obj); - - if (!isArray) { - return allKeys; - } - - var extraKeys = []; - if (allKeys.length === 0) { - return allKeys; - } - - for (var x = 0; x < allKeys.length; x++) { - if (!allKeys[x].match(/^[0-9]+$/)) { - extraKeys.push(allKeys[x]); - } - } - - return extraKeys; - } - } - - function has(obj, key) { - return Object.prototype.hasOwnProperty.call(obj, key); - } - - function isFunction(obj) { - return typeof obj === 'function'; - } - - function isObjectConstructor(ctor) { - // aCtor instanceof aCtor is true for the Object and Function - // constructors (since a constructor is-a Function and a function is-a - // Object). We don't just compare ctor === Object because the constructor - // might come from a different frame with different globals. - return isFunction(ctor) && ctor instanceof ctor; - } -}; - -getJasmineRequireObj().toBe = function() { - function toBe() { - return { - compare: function(actual, expected) { - return { - pass: actual === expected - }; - } - }; - } - - return toBe; -}; - -getJasmineRequireObj().toBeCloseTo = function() { - - function toBeCloseTo() { - return { - compare: function(actual, expected, precision) { - if (precision !== 0) { - precision = precision || 2; - } - - return { - pass: Math.abs(expected - actual) < (Math.pow(10, -precision) / 2) - }; - } - }; - } - - return toBeCloseTo; -}; - -getJasmineRequireObj().toBeDefined = function() { - function toBeDefined() { - return { - compare: function(actual) { - return { - pass: (void 0 !== actual) - }; - } - }; - } - - return toBeDefined; -}; - -getJasmineRequireObj().toBeFalsy = function() { - function toBeFalsy() { - return { - compare: function(actual) { - return { - pass: !!!actual - }; - } - }; - } - - return toBeFalsy; -}; - -getJasmineRequireObj().toBeGreaterThan = function() { - - function toBeGreaterThan() { - return { - compare: function(actual, expected) { - return { - pass: actual > expected - }; - } - }; - } - - return toBeGreaterThan; -}; - - -getJasmineRequireObj().toBeGreaterThanOrEqual = function() { - - function toBeGreaterThanOrEqual() { - return { - compare: function(actual, expected) { - return { - pass: actual >= expected - }; - } - }; - } - - return toBeGreaterThanOrEqual; -}; - -getJasmineRequireObj().toBeLessThan = function() { - function toBeLessThan() { - return { - - compare: function(actual, expected) { - return { - pass: actual < expected - }; - } - }; - } - - return toBeLessThan; -}; -getJasmineRequireObj().toBeLessThanOrEqual = function() { - function toBeLessThanOrEqual() { - return { - - compare: function(actual, expected) { - return { - pass: actual <= expected - }; - } - }; - } - - return toBeLessThanOrEqual; -}; - -getJasmineRequireObj().toBeNaN = function(j$) { - - function toBeNaN() { - return { - compare: function(actual) { - var result = { - pass: (actual !== actual) - }; - - if (result.pass) { - result.message = 'Expected actual not to be NaN.'; - } else { - result.message = function() { return 'Expected ' + j$.pp(actual) + ' to be NaN.'; }; - } - - return result; - } - }; - } - - return toBeNaN; -}; - -getJasmineRequireObj().toBeNull = function() { - - function toBeNull() { - return { - compare: function(actual) { - return { - pass: actual === null - }; - } - }; - } - - return toBeNull; -}; - -getJasmineRequireObj().toBeTruthy = function() { - - function toBeTruthy() { - return { - compare: function(actual) { - return { - pass: !!actual - }; - } - }; - } - - return toBeTruthy; -}; - -getJasmineRequireObj().toBeUndefined = function() { - - function toBeUndefined() { - return { - compare: function(actual) { - return { - pass: void 0 === actual - }; - } - }; - } - - return toBeUndefined; -}; - -getJasmineRequireObj().toContain = function() { - function toContain(util, customEqualityTesters) { - customEqualityTesters = customEqualityTesters || []; - - return { - compare: function(actual, expected) { - - return { - pass: util.contains(actual, expected, customEqualityTesters) - }; - } - }; - } - - return toContain; -}; - -getJasmineRequireObj().toEqual = function() { - - function toEqual(util, customEqualityTesters) { - customEqualityTesters = customEqualityTesters || []; - - return { - compare: function(actual, expected) { - var result = { - pass: false - }; - - result.pass = util.equals(actual, expected, customEqualityTesters); - - return result; - } - }; - } - - return toEqual; -}; - -getJasmineRequireObj().toHaveBeenCalled = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('', 'expect().toHaveBeenCalled()'); - - function toHaveBeenCalled() { - return { - compare: function(actual) { - var result = {}; - - if (!j$.isSpy(actual)) { - throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(actual) + '.')); - } - - if (arguments.length > 1) { - throw new Error(getErrorMsg('Does not take arguments, use toHaveBeenCalledWith')); - } - - result.pass = actual.calls.any(); - - result.message = result.pass ? - 'Expected spy ' + actual.and.identity() + ' not to have been called.' : - 'Expected spy ' + actual.and.identity() + ' to have been called.'; - - return result; - } - }; - } - - return toHaveBeenCalled; -}; - -getJasmineRequireObj().toHaveBeenCalledTimes = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('', 'expect().toHaveBeenCalledTimes()'); - - function toHaveBeenCalledTimes() { - return { - compare: function(actual, expected) { - if (!j$.isSpy(actual)) { - throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(actual) + '.')); - } - - var args = Array.prototype.slice.call(arguments, 0), - result = { pass: false }; - - if (!j$.isNumber_(expected)){ - throw new Error(getErrorMsg('The expected times failed is a required argument and must be a number.')); - } - - actual = args[0]; - var calls = actual.calls.count(); - var timesMessage = expected === 1 ? 'once' : expected + ' times'; - result.pass = calls === expected; - result.message = result.pass ? - 'Expected spy ' + actual.and.identity() + ' not to have been called ' + timesMessage + '. It was called ' + calls + ' times.' : - 'Expected spy ' + actual.and.identity() + ' to have been called ' + timesMessage + '. It was called ' + calls + ' times.'; - return result; - } - }; - } - - return toHaveBeenCalledTimes; -}; - -getJasmineRequireObj().toHaveBeenCalledWith = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('', 'expect().toHaveBeenCalledWith(...arguments)'); - - function toHaveBeenCalledWith(util, customEqualityTesters) { - return { - compare: function() { - var args = Array.prototype.slice.call(arguments, 0), - actual = args[0], - expectedArgs = args.slice(1), - result = { pass: false }; - - if (!j$.isSpy(actual)) { - throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(actual) + '.')); - } - - if (!actual.calls.any()) { - result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + j$.pp(expectedArgs) + ' but it was never called.'; }; - return result; - } - - if (util.contains(actual.calls.allArgs(), expectedArgs, customEqualityTesters)) { - result.pass = true; - result.message = function() { return 'Expected spy ' + actual.and.identity() + ' not to have been called with ' + j$.pp(expectedArgs) + ' but it was.'; }; - } else { - result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + j$.pp(expectedArgs) + ' but actual calls were ' + j$.pp(actual.calls.allArgs()).replace(/^\[ | \]$/g, '') + '.'; }; - } - - return result; - } - }; - } - - return toHaveBeenCalledWith; -}; - -getJasmineRequireObj().toMatch = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('', 'expect().toMatch( || )'); - - function toMatch() { - return { - compare: function(actual, expected) { - if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) { - throw new Error(getErrorMsg('Expected is not a String or a RegExp')); - } - - var regexp = new RegExp(expected); - - return { - pass: regexp.test(actual) - }; - } - }; - } - - return toMatch; -}; - -getJasmineRequireObj().toThrow = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('', 'expect(function() {}).toThrow()'); - - function toThrow(util) { - return { - compare: function(actual, expected) { - var result = { pass: false }, - threw = false, - thrown; - - if (typeof actual != 'function') { - throw new Error(getErrorMsg('Actual is not a Function')); - } - - try { - actual(); - } catch (e) { - threw = true; - thrown = e; - } - - if (!threw) { - result.message = 'Expected function to throw an exception.'; - return result; - } - - if (arguments.length == 1) { - result.pass = true; - result.message = function() { return 'Expected function not to throw, but it threw ' + j$.pp(thrown) + '.'; }; - - return result; - } - - if (util.equals(thrown, expected)) { - result.pass = true; - result.message = function() { return 'Expected function not to throw ' + j$.pp(expected) + '.'; }; - } else { - result.message = function() { return 'Expected function to throw ' + j$.pp(expected) + ', but it threw ' + j$.pp(thrown) + '.'; }; - } - - return result; - } - }; - } - - return toThrow; -}; - -getJasmineRequireObj().toThrowError = function(j$) { - - var getErrorMsg = j$.formatErrorMsg('', 'expect(function() {}).toThrowError(, )'); - - function toThrowError () { - return { - compare: function(actual) { - var threw = false, - pass = {pass: true}, - fail = {pass: false}, - thrown; - - if (typeof actual != 'function') { - throw new Error(getErrorMsg('Actual is not a Function')); - } - - var errorMatcher = getMatcher.apply(null, arguments); - - try { - actual(); - } catch (e) { - threw = true; - thrown = e; - } - - if (!threw) { - fail.message = 'Expected function to throw an Error.'; - return fail; - } - - if (!(thrown instanceof Error)) { - fail.message = function() { return 'Expected function to throw an Error, but it threw ' + j$.pp(thrown) + '.'; }; - return fail; - } - - if (errorMatcher.hasNoSpecifics()) { - pass.message = 'Expected function not to throw an Error, but it threw ' + j$.fnNameFor(thrown) + '.'; - return pass; - } - - if (errorMatcher.matches(thrown)) { - pass.message = function() { - return 'Expected function not to throw ' + errorMatcher.errorTypeDescription + errorMatcher.messageDescription() + '.'; - }; - return pass; - } else { - fail.message = function() { - return 'Expected function to throw ' + errorMatcher.errorTypeDescription + errorMatcher.messageDescription() + - ', but it threw ' + errorMatcher.thrownDescription(thrown) + '.'; - }; - return fail; - } - } - }; - - function getMatcher() { - var expected = null, - errorType = null; - - if (arguments.length == 2) { - expected = arguments[1]; - if (isAnErrorType(expected)) { - errorType = expected; - expected = null; - } - } else if (arguments.length > 2) { - errorType = arguments[1]; - expected = arguments[2]; - if (!isAnErrorType(errorType)) { - throw new Error(getErrorMsg('Expected error type is not an Error.')); - } - } - - if (expected && !isStringOrRegExp(expected)) { - if (errorType) { - throw new Error(getErrorMsg('Expected error message is not a string or RegExp.')); - } else { - throw new Error(getErrorMsg('Expected is not an Error, string, or RegExp.')); - } - } - - function messageMatch(message) { - if (typeof expected == 'string') { - return expected == message; - } else { - return expected.test(message); - } - } - - return { - errorTypeDescription: errorType ? j$.fnNameFor(errorType) : 'an exception', - thrownDescription: function(thrown) { - var thrownName = errorType ? j$.fnNameFor(thrown.constructor) : 'an exception', - thrownMessage = ''; - - if (expected) { - thrownMessage = ' with message ' + j$.pp(thrown.message); - } - - return thrownName + thrownMessage; - }, - messageDescription: function() { - if (expected === null) { - return ''; - } else if (expected instanceof RegExp) { - return ' with a message matching ' + j$.pp(expected); - } else { - return ' with message ' + j$.pp(expected); - } - }, - hasNoSpecifics: function() { - return expected === null && errorType === null; - }, - matches: function(error) { - return (errorType === null || error instanceof errorType) && - (expected === null || messageMatch(error.message)); - } - }; - } - - function isStringOrRegExp(potential) { - return potential instanceof RegExp || (typeof potential == 'string'); - } - - function isAnErrorType(type) { - if (typeof type !== 'function') { - return false; - } - - var Surrogate = function() {}; - Surrogate.prototype = type.prototype; - return (new Surrogate()) instanceof Error; - } - } - - return toThrowError; -}; - -getJasmineRequireObj().interface = function(jasmine, env) { - var jasmineInterface = { - describe: function(description, specDefinitions) { - return env.describe(description, specDefinitions); - }, - - xdescribe: function(description, specDefinitions) { - return env.xdescribe(description, specDefinitions); - }, - - fdescribe: function(description, specDefinitions) { - return env.fdescribe(description, specDefinitions); - }, - - it: function() { - return env.it.apply(env, arguments); - }, - - xit: function() { - return env.xit.apply(env, arguments); - }, - - fit: function() { - return env.fit.apply(env, arguments); - }, - - beforeEach: function() { - return env.beforeEach.apply(env, arguments); - }, - - afterEach: function() { - return env.afterEach.apply(env, arguments); - }, - - beforeAll: function() { - return env.beforeAll.apply(env, arguments); - }, - - afterAll: function() { - return env.afterAll.apply(env, arguments); - }, - - expect: function(actual) { - return env.expect(actual); - }, - - pending: function() { - return env.pending.apply(env, arguments); - }, - - fail: function() { - return env.fail.apply(env, arguments); - }, - - spyOn: function(obj, methodName) { - return env.spyOn(obj, methodName); - }, - - spyOnProperty: function(obj, methodName, accessType) { - return env.spyOnProperty(obj, methodName, accessType); - }, - - jsApiReporter: new jasmine.JsApiReporter({ - timer: new jasmine.Timer() - }), - - jasmine: jasmine - }; - - jasmine.addCustomEqualityTester = function(tester) { - env.addCustomEqualityTester(tester); - }; - - jasmine.addMatchers = function(matchers) { - return env.addMatchers(matchers); - }; - - jasmine.clock = function() { - return env.clock; - }; - - return jasmineInterface; -}; - getJasmineRequireObj().version = function() { return '2.5.2'; }; diff --git a/spec/core/PrettyPrintSpec.js b/spec/core/PrettyPrintSpec.js index 301ba80e..3965ea73 100644 --- a/spec/core/PrettyPrintSpec.js +++ b/spec/core/PrettyPrintSpec.js @@ -99,6 +99,11 @@ describe("jasmineUnderTest.pp", function () { }, bar: [1, 2, 3]})).toEqual("Object({ foo: Function, bar: [ 1, 2, 3 ] })"); }); + it("should print 'null' as the constructor of an object with its own constructor property", function() { + expect(jasmineUnderTest.pp({constructor: function() {}})).toEqual("null({ constructor: Function })"); + expect(jasmineUnderTest.pp({constructor: 'foo'})).toEqual("null({ constructor: 'foo' })"); + }); + it("should not include inherited properties when stringifying an object", function() { var SomeClass = function SomeClass() {}; SomeClass.prototype.foo = "inherited foo"; @@ -164,7 +169,6 @@ describe("jasmineUnderTest.pp", function () { } }); - it('should not do HTML escaping of strings', function() { expect(jasmineUnderTest.pp('some html string &', false)).toEqual('\'some html string &\''); }); diff --git a/spec/core/UtilSpec.js b/spec/core/UtilSpec.js index e547edf1..faaca2c2 100644 --- a/spec/core/UtilSpec.js +++ b/spec/core/UtilSpec.js @@ -63,4 +63,37 @@ describe("jasmineUnderTest.util", function() { expect(actual).toEqual(expected); }); }); + + describe("objectDifference", function() { + it("given two objects A and B, returns the properties in A not present in B", function() { + var a = { + foo: 3, + bar: 4, + baz: 5 + }; + + var b = { + bar: 6, + quux: 7 + }; + + expect(jasmineUnderTest.util.objectDifference(a, b)).toEqual({foo: 3, baz: 5}) + }); + + it("only looks at own properties of both objects", function() { + function Foo() {} + + Foo.prototype.x = 1; + Foo.prototype.y = 2; + + var a = new Foo(); + a.x = 1; + + var b = new Foo(); + b.y = 2; + + expect(jasmineUnderTest.util.objectDifference(a, b)).toEqual({x: 1}); + expect(jasmineUnderTest.util.objectDifference(b, a)).toEqual({y: 2}); + }) + }) }); diff --git a/spec/core/integration/CustomMatchersSpec.js b/spec/core/integration/CustomMatchersSpec.js index ad73bbb4..0b0ee92c 100644 --- a/spec/core/integration/CustomMatchersSpec.js +++ b/spec/core/integration/CustomMatchersSpec.js @@ -79,6 +79,30 @@ describe("Custom Matchers (Integration)", function() { env.execute(); }); + it("displays an appropriate failure message if a custom equality matcher fails", function(done) { + env.it("spec using custom equality matcher", function() { + var customEqualityFn = function(a, b) { + // "foo" is not equal to anything + if (a === 'foo' || b === 'foo') { + return false; + } + }; + + env.addCustomEqualityTester(customEqualityFn); + env.expect({foo: 'foo'}).toEqual({foo: 'foo'}); + }); + + var specExpectations = function(result) { + expect(result.status).toEqual('failed'); + expect(result.failedExpectations[0].message).toEqual( + "Expected $.foo = 'foo' to equal 'foo'." + ); + }; + + env.addReporter({ specDone: specExpectations, jasmineDone: done }); + env.execute(); + }); + it("uses the negative compare function for a negative comparison, if provided", function(done) { env.it("spec with custom negative comparison matcher", function() { env.addMatchers({ diff --git a/spec/core/matchers/DiffBuilderSpec.js b/spec/core/matchers/DiffBuilderSpec.js new file mode 100644 index 00000000..2fe8f1bf --- /dev/null +++ b/spec/core/matchers/DiffBuilderSpec.js @@ -0,0 +1,47 @@ +describe("DiffBuilder", function() { + it("records the actual and expected objects", function() { + var diffBuilder = jasmineUnderTest.DiffBuilder(); + diffBuilder.record({x: 'actual'}, {x: 'expected'}); + + expect(diffBuilder.getMessage()).toEqual("Expected Object({ x: 'actual' }) to equal Object({ x: 'expected' })."); + }); + + it("prints the path at which the difference was found", function() { + var diffBuilder = jasmineUnderTest.DiffBuilder(); + + diffBuilder.withPath('foo', function() { + diffBuilder.record({x: 'actual'}, {x: 'expected'}); + }); + + expect(diffBuilder.getMessage()).toEqual("Expected $.foo = Object({ x: 'actual' }) to equal Object({ x: 'expected' })."); + }); + + it("prints multiple messages, separated by newlines", function() { + var diffBuilder = jasmineUnderTest.DiffBuilder(); + + diffBuilder.withPath('foo', function() { + diffBuilder.record(1, 2); + }); + + var message = + "Expected $.foo = 1 to equal 2.\n" + + "Expected 3 to equal 4."; + + diffBuilder.record(3, 4); + expect(diffBuilder.getMessage()).toEqual(message); + }); + + it("allows customization of the message", function() { + var diffBuilder = jasmineUnderTest.DiffBuilder(); + + 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); + }); + + expect(diffBuilder.getMessage()).toEqual("I find your lack of foo disturbing. (was bar, at $.x)"); + }); +}); diff --git a/spec/core/matchers/NullDiffBuilderSpec.js b/spec/core/matchers/NullDiffBuilderSpec.js new file mode 100644 index 00000000..a9aac3db --- /dev/null +++ b/spec/core/matchers/NullDiffBuilderSpec.js @@ -0,0 +1,13 @@ +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(); + }) +}); diff --git a/spec/core/matchers/ObjectPathSpec.js b/spec/core/matchers/ObjectPathSpec.js new file mode 100644 index 00000000..88b023d1 --- /dev/null +++ b/spec/core/matchers/ObjectPathSpec.js @@ -0,0 +1,43 @@ +describe('ObjectPath', function() { + var ObjectPath = jasmineUnderTest.ObjectPath; + + it('represents the path to a node in an object tree', function() { + expect(new ObjectPath(['foo', 'bar']).toString()).toEqual('$.foo.bar'); + }); + + it('has a depth', function() { + expect(new ObjectPath().depth()).toEqual(0); + expect(new ObjectPath(['foo']).depth()).toEqual(1); + }); + + it('renders numbers as array access', function() { + expect(new ObjectPath(['foo', 0]).toString()).toEqual('$.foo[0]'); + }); + + it('renders properties that are valid identifiers with dot notation', function() { + expect(new ObjectPath(['foo123']).toString()).toEqual('$.foo123'); + expect(new ObjectPath(['x_y']).toString()).toEqual('$.x_y'); + expect(new ObjectPath(['A$B']).toString()).toEqual('$.A$B'); + }); + + it('renders properties with non-identifier-safe characters with square bracket notation', function() { + expect(new ObjectPath(['a b c']).toString()).toEqual("$['a b c']"); + expect(new ObjectPath(['1hello']).toString()).toEqual("$['1hello']"); + }); + + it('renders as the empty string when empty', function() { + expect(new ObjectPath().toString()).toEqual(''); + }); + + it('stringifies properties that are not strings or numbers', function() { + expect(new ObjectPath([{}]).toString()).toEqual("$['[object Object]']"); + }); + + it('can be created based on another path', function() { + var root = new ObjectPath(); + var path = root.add('foo'); + + expect(path.toString()).toEqual('$.foo'); + expect(root.toString()).toEqual(''); + }) +}); diff --git a/spec/core/matchers/matchersUtilSpec.js b/spec/core/matchers/matchersUtilSpec.js index d5c9c200..3a8054a7 100644 --- a/spec/core/matchers/matchersUtilSpec.js +++ b/spec/core/matchers/matchersUtilSpec.js @@ -61,10 +61,14 @@ describe("matchersUtil", function() { expect(jasmineUnderTest.matchersUtil.equals(foo, [undefined])).toBe(true); }); - it("fails for Arrays that are not equivalent", function() { + it("fails for Arrays that have different lengths", function() { expect(jasmineUnderTest.matchersUtil.equals([1, 2], [1, 2, 3])).toBe(false); }); + it("fails for Arrays that have different elements", function() { + expect(jasmineUnderTest.matchersUtil.equals([1, 2, 3], [1, 5, 3])).toBe(false); + }); + it("fails for Arrays whose contents are equivalent, but have differing properties", function() { var one = [1,2,3], two = [1,2,3]; @@ -136,6 +140,13 @@ describe("matchersUtil", function() { expect(jasmineUnderTest.matchersUtil.equals(actual, expected)).toBe(false); }); + it("fails for Objects that have the same number of keys, but different keys/values", function () { + var expected = { a: undefined }, + actual = { b: 1 }; + + expect(jasmineUnderTest.matchersUtil.equals(actual, expected)).toBe(false); + }) + it("fails when comparing an empty object to an empty array (issue #114)", function() { var emptyObject = {}, emptyArray = []; diff --git a/spec/core/matchers/toEqualSpec.js b/spec/core/matchers/toEqualSpec.js index f038f9e9..2ac0a367 100644 --- a/spec/core/matchers/toEqualSpec.js +++ b/spec/core/matchers/toEqualSpec.js @@ -1,20 +1,39 @@ describe("toEqual", function() { + "use strict"; + + function compareEquals(actual, expected) { + var util = jasmineUnderTest.matchersUtil, + matcher = jasmineUnderTest.matchers.toEqual(util); + + var result = matcher.compare(actual, expected); + + return result; + } + it("delegates to equals function", function() { var util = { - equals: jasmine.createSpy('delegated-equals').and.returnValue(true) + equals: jasmine.createSpy('delegated-equals').and.returnValue(true), + buildFailureMessage: function() { + return 'does not matter' + }, + DiffBuilder: jasmineUnderTest.matchersUtil.DiffBuilder }, matcher = jasmineUnderTest.matchers.toEqual(util), result; result = matcher.compare(1, 1); - expect(util.equals).toHaveBeenCalledWith(1, 1, []); + expect(util.equals).toHaveBeenCalledWith(1, 1, [], jasmine.anything()); expect(result.pass).toBe(true); }); it("delegates custom equality testers, if present", function() { var util = { - equals: jasmine.createSpy('delegated-equals').and.returnValue(true) + equals: jasmine.createSpy('delegated-equals').and.returnValue(true), + buildFailureMessage: function() { + return 'does not matter' + }, + DiffBuilder: jasmineUnderTest.matchersUtil.DiffBuilder }, customEqualityTesters = ['a', 'b'], matcher = jasmineUnderTest.matchers.toEqual(util, customEqualityTesters), @@ -22,7 +41,457 @@ describe("toEqual", function() { result = matcher.compare(1, 1); - expect(util.equals).toHaveBeenCalledWith(1, 1, ['a', 'b']); + expect(util.equals).toHaveBeenCalledWith(1, 1, ['a', 'b'], jasmine.anything()); expect(result.pass).toBe(true); }); + + it("reports the difference between objects that are not equal", function() { + var actual = {x: 1, y: 3}, + expected = {x: 2, y: 3}, + message = "Expected $.x = 1 to equal 2."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports the difference between nested objects that are not equal", function() { + var actual = {x: {y: 1}}, + expected = {x: {y: 2}}, + message = "Expected $.x.y = 1 to equal 2."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("formats property access so that it's valid JavaScript", function() { + var actual = {'my prop': 1}, + expected = {'my prop': 2}, + message = "Expected $['my prop'] = 1 to equal 2."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports missing properties", function() { + var actual = {x: {}}, + expected = {x: {y: 1}}, + message = + "Expected $.x to have properties\n" + + " y: 1"; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports extra properties", function() { + var actual = {x: {y: 1, z: 2}}, + expected = {x: {}}, + message = + "Expected $.x not to have properties\n" + + " y: 1\n" + + " z: 2"; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("pretty-prints properties", function() { + var actual = {x: {y: 'foo bar'}}, + expected = {x: {}}, + message = + "Expected $.x not to have properties\n" + + " y: 'foo bar'" + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports extra and missing properties together", function() { + var actual = {x: {y: 1, z: 2, f: 4}}, + expected = {x: {y: 1, z: 2, g: 3}}, + message = + "Expected $.x to have properties\n" + + " g: 3\n" + + "Expected $.x not to have properties\n" + + " f: 4"; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports extra and missing properties of the root-level object", function() { + var actual = {x: 1}, + expected = {a: 1}, + message = + "Expected object to have properties\n" + + " a: 1\n" + + "Expected object not to have properties\n" + + " x: 1"; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports multiple incorrect values", function() { + var actual = {x: 1, y: 2}, + expected = {x: 3, y: 4}, + message = + "Expected $.x = 1 to equal 3.\n" + + "Expected $.y = 2 to equal 4."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatch between actual child object and expected child number", function() { + var actual = {x: {y: 2}}, + expected = {x: 1}, + message = "Expected $.x = Object({ y: 2 }) to equal 1."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("uses the default failure message if actual is not an object", function() { + var actual = 1, + expected = {x: {}}, + message = "Expected 1 to equal Object({ x: Object({ }) })."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("uses the default failure message if expected is not an object", function() { + var actual = {x: {}}, + expected = 1, + message = "Expected Object({ x: Object({ }) }) to equal 1."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("uses the default failure message given arrays with different lengths", function() { + var actual = [1, 2], + expected = [1, 2, 3], + message = + "Expected [ 1, 2 ] to equal [ 1, 2, 3 ]."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports a mismatch between elements of equal-length arrays", function() { + var actual = [1, 2, 5], + expected = [1, 2, 3], + message = "Expected $[2] = 5 to equal 3."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports a mismatch between multiple array elements", function() { + var actual = [2, 2, 5], + expected = [1, 2, 3], + message = + "Expected $[0] = 2 to equal 1.\n" + + "Expected $[2] = 5 to equal 3."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports a mismatch between properties of objects in arrays", function() { + var actual = [{x: 1}], + expected = [{x: 2}], + message = "Expected $[0].x = 1 to equal 2."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports a mismatch between arrays in objects", function() { + var actual = {x: [1]}, + expected = {x: [2]}, + message = + "Expected $.x[0] = 1 to equal 2."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches between nested arrays", function() { + var actual = [[1]], + expected = [[2]], + message = + "Expected $[0][0] = 1 to equal 2."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches involving NaN", function() { + var actual = {x: 0}, + expected = {x: 0/0}, + message = "Expected $.x = 0 to equal NaN."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches involving regular expressions", function() { + var actual = {x: '1'}, + expected = {x: /1/}, + message = "Expected $.x = '1' to equal /1/."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches involving infinities", function() { + var actual = {x: 0}, + expected = {x: 1/0}, + message = "Expected $.x = 0 to equal Infinity."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches involving booleans", function() { + var actual = {x: false}, + expected = {x: true}, + message = "Expected $.x = false to equal true."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches involving strings", function() { + var actual = {x: 'foo'}, + expected = {x: 'bar'}, + message = "Expected $.x = 'foo' to equal 'bar'."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches involving undefined", function() { + var actual = {x: void 0}, + expected = {x: 0}, + message = "Expected $.x = undefined to equal 0."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches involving null", function() { + var actual = {x: null}, + expected = {x: 0}, + message = "Expected $.x = null to equal 0."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches between objects with different constructors", function () { + function Foo() {} + function Bar() {} + + var actual = {x: new Foo()}, + expected = {x: new Bar()}, + message = "Expected $.x to be a kind of Bar, but was Foo({ })."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports type mismatches at the root level", function () { + function Foo() {} + function Bar() {} + + var actual = new Foo(), + expected = new Bar(), + message = "Expected object to be a kind of Bar, but was Foo({ })."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches between objects with their own constructor property", function () { + function Foo() {} + function Bar() {} + + var actual = {x: {constructor: 'blerf'}}, + expected = {x: {constructor: 'ftarrh'}}, + message = "Expected $.x.constructor = 'blerf' to equal 'ftarrh'."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches between an object with a real constructor and one with its own constructor property", function () { + function Foo() {} + function Bar() {} + + var actual = {x: {}}, + expected = {x: {constructor: 'ftarrh'}}, + message = + "Expected $.x to have properties\n" + + " constructor: 'ftarrh'"; + + expect(compareEquals(actual, expected).message).toEqual(message); + expect(compareEquals(expected, actual).message).toEqual( + "Expected $.x not to have properties\n constructor: 'ftarrh'" + ); + }); + + it("reports mismatches between 0 and -0", function() { + var actual = {x: 0}, + expected = {x: -0}, + message = "Expected $.x = 0 to equal -0."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches between Errors", function() { + var actual = {x: new Error("the error you got")}, + expected = {x: new Error("the error you want")}, + message = "Expected $.x = Error: the error you got to equal Error: the error you want."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches between Functions", function() { + var actual = {x: function() {}}, + expected = {x: function() {}}, + message = "Expected $.x = Function to equal Function."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("does not report deep mismatches within Sets", function() { + // TODO: implement deep comparison of set elements + jasmine.getEnv().requireFunctioningSets(); + + var actual = new Set([1]), + expected = new Set([2]), + message = 'Expected Set( 1 ) to equal Set( 2 ).'; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches between Sets nested in objects", function() { + jasmine.getEnv().requireFunctioningSets(); + + var actual = {sets: [new Set([1])]}, + expected = {sets: [new Set([2])]}, + message = "Expected $.sets[0] = Set( 1 ) to equal Set( 2 )."; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports mismatches between Sets of different lengths", function() { + jasmine.getEnv().requireFunctioningSets(); + + var actual = new Set([1, 2]), + expected = new Set([2]), + message = 'Expected Set( 1, 2 ) to equal Set( 2 ).'; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + function isNotRunningInBrowser() { + return typeof document === 'undefined' + } + + it("reports mismatches between DOM nodes with different tags", function() { + if(isNotRunningInBrowser()) { + return; + } + + var actual = {a: document.createElement('div')}, + expected = {a: document.createElement('p')}, + message = 'Expected $.a = HTMLNode to equal HTMLNode.'; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it('reports mismatches between DOM nodes with different content', function() { + if(isNotRunningInBrowser()) { + return; + } + + var nodeA = document.createElement('div'), + nodeB = document.createElement('div'); + + nodeA.innerText = 'foo' + nodeB.innerText = 'bar' + + var actual = {a: nodeA}, + expected = {a: nodeB}, + message = 'Expected $.a = HTMLNode to equal HTMLNode.'; + + expect(compareEquals(actual, expected).message).toEqual(message); + }) + + it("reports mismatches between a DOM node and a bare Object", function() { + if(isNotRunningInBrowser()) { + return; + } + + var actual = {a: document.createElement('div')}, + expected = {a: {}}, + message = 'Expected $.a = HTMLNode to equal Object({ }).'; + + expect(compareEquals(actual, expected).message).toEqual(message); + }); + + it("reports asymmetric mismatches", function() { + var actual = {a: 1}, + expected = {a: jasmineUnderTest.any(String)}, + message = 'Expected $.a = 1 to equal .'; + + expect(compareEquals(actual, expected).message).toEqual(message); + expect(compareEquals(actual, expected).pass).toBe(false) + }); + + it("reports asymmetric mismatches when the asymmetric comparand is the actual value", function() { + var actual = {a: jasmineUnderTest.any(String)}, + expected = {a: 1}, + message = 'Expected $.a = to equal 1.'; + + expect(compareEquals(actual, expected).message).toEqual(message); + expect(compareEquals(actual, expected).pass).toBe(false) + }); + + it("does not report a mismatch when asymmetric matchers are satisfied", function() { + var actual = {a: 'a'}, + expected = {a: jasmineUnderTest.any(String)}, + message = 'Expected $.a = 1 to equal .'; + + expect(compareEquals(actual, expected).pass).toBe(true) + }); + + it("works on big complex stuff", function() { + var actual = { + foo: [ + {bar: 1, things: ['a', 'b']}, + {bar: 2, things: ['a', 'b']} + ], + baz: [ + {a: {b: 1}} + ], + quux: 1, + nan: 0, + aRegexp: 'hi', + inf: -1/0, + boolean: false, + notDefined: 0, + aNull: void 0 + } + + var expected = { + foo: [ + {bar: 2, things: ['a', 'b', 'c']}, + {bar: 2, things: ['a', 'd']} + ], + baz: [ + {a: {b: 1, c: 1}} + ], + quux: [], + nan: 0/0, + aRegexp: /hi/, + inf: 1/0, + boolean: true, + notDefined: void 0, + aNull: null + } + + var message = + 'Expected $.foo[0].bar = 1 to equal 2.\n' + + "Expected $.foo[0].things = [ 'a', 'b' ] to equal [ 'a', 'b', 'c' ].\n" + + "Expected $.foo[1].things[1] = 'b' to equal 'd'.\n" + + 'Expected $.baz[0].a to have properties\n' + + ' c: 1\n' + + 'Expected $.quux = 1 to equal [ ].\n' + + 'Expected $.nan = 0 to equal NaN.\n' + + "Expected $.aRegexp = 'hi' to equal /hi/.\n" + + 'Expected $.inf = -Infinity to equal Infinity.\n' + + 'Expected $.boolean = false to equal true.\n' + + 'Expected $.notDefined = 0 to equal undefined.\n' + + 'Expected $.aNull = undefined to equal null.' + + expect(compareEquals(actual, expected).message).toEqual(message); + }) }); diff --git a/src/core/PrettyPrinter.js b/src/core/PrettyPrinter.js index 4aebd96f..d945d0c7 100644 --- a/src/core/PrettyPrinter.js +++ b/src/core/PrettyPrinter.js @@ -145,7 +145,13 @@ getJasmineRequireObj().pp = function(j$) { }; StringPrettyPrinter.prototype.emitObject = function(obj) { - var constructorName = obj.constructor ? j$.fnNameFor(obj.constructor) : 'null'; + var ctor = obj.constructor, + constructorName; + + constructorName = typeof ctor === 'function' && obj instanceof ctor ? + j$.fnNameFor(obj.constructor) : + 'null'; + this.append(constructorName); if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { diff --git a/src/core/matchers/DiffBuilder.js b/src/core/matchers/DiffBuilder.js new file mode 100644 index 00000000..131b5b92 --- /dev/null +++ b/src/core/matchers/DiffBuilder.js @@ -0,0 +1,33 @@ +getJasmineRequireObj().DiffBuilder = function(j$) { + return function DiffBuilder() { + var path = new j$.ObjectPath(), + mismatches = []; + + return { + record: function (actual, expected, formatter) { + formatter = formatter || defaultFormatter; + mismatches.push(formatter(actual, expected, path)); + }, + + getMessage: function () { + return mismatches.join('\n'); + }, + + withPath: function (pathComponent, block) { + var oldPath = path; + path = path.add(pathComponent); + block(); + path = oldPath; + } + }; + + function defaultFormatter (actual, expected, path) { + return 'Expected ' + + path + (path.depth() ? ' = ' : '') + + j$.pp(actual) + + ' to equal ' + + j$.pp(expected) + + '.'; + } + }; +}; diff --git a/src/core/matchers/NullDiffBuilder.js b/src/core/matchers/NullDiffBuilder.js new file mode 100644 index 00000000..de7d3464 --- /dev/null +++ b/src/core/matchers/NullDiffBuilder.js @@ -0,0 +1,10 @@ +getJasmineRequireObj().NullDiffBuilder = function(j$) { + return function() { + return { + withPath: function(_, block) { + block(); + }, + record: function() {} + }; + }; +}; diff --git a/src/core/matchers/ObjectPath.js b/src/core/matchers/ObjectPath.js new file mode 100644 index 00000000..cd1629e2 --- /dev/null +++ b/src/core/matchers/ObjectPath.js @@ -0,0 +1,47 @@ +getJasmineRequireObj().ObjectPath = function(j$) { + function ObjectPath(components) { + this.components = components || []; + } + + ObjectPath.prototype.toString = function() { + if (this.components.length) { + return '$' + map(this.components, formatPropertyAccess).join(''); + } else { + return ''; + } + }; + + ObjectPath.prototype.add = function(component) { + return new ObjectPath(this.components.concat([component])); + }; + + ObjectPath.prototype.depth = function() { + return this.components.length; + }; + + function formatPropertyAccess(prop) { + if (typeof prop === 'number') { + return '[' + prop + ']'; + } + + if (isValidIdentifier(prop)) { + return '.' + prop; + } + + return '[\'' + prop + '\']'; + } + + function map(array, fn) { + var results = []; + for (var i = 0; i < array.length; i++) { + results.push(fn(array[i])); + } + return results; + } + + function isValidIdentifier(string) { + return /^[A-Za-z\$_][A-Za-z0-9\$_]*$/.test(string); + } + + return ObjectPath; +}; diff --git a/src/core/matchers/matchersUtil.js b/src/core/matchers/matchersUtil.js index 52dfaece..5998c393 100644 --- a/src/core/matchers/matchersUtil.js +++ b/src/core/matchers/matchersUtil.js @@ -2,11 +2,7 @@ getJasmineRequireObj().matchersUtil = function(j$) { // TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter? return { - equals: function(a, b, customTesters) { - customTesters = customTesters || []; - - return eq(a, b, [], [], customTesters); - }, + equals: equals, contains: function(haystack, needle, customTesters) { customTesters = customTesters || []; @@ -19,7 +15,7 @@ getJasmineRequireObj().matchersUtil = function(j$) { (!!haystack && !haystack.indexOf)) { for (var i = 0; i < haystack.length; i++) { - if (eq(haystack[i], needle, [], [], customTesters)) { + if (equals(haystack[i], needle, customTesters)) { return true; } } @@ -59,67 +55,113 @@ getJasmineRequireObj().matchersUtil = function(j$) { return obj && j$.isA_('Function', obj.asymmetricMatch); } - function asymmetricMatch(a, b, customTesters) { + function asymmetricMatch(a, b, customTesters, diffBuilder) { var asymmetricA = isAsymmetric(a), - asymmetricB = isAsymmetric(b); + asymmetricB = isAsymmetric(b), + result; if (asymmetricA && asymmetricB) { return undefined; } if (asymmetricA) { - return a.asymmetricMatch(b, customTesters); + result = a.asymmetricMatch(b, customTesters); + diffBuilder.record(a, b); + return result; } if (asymmetricB) { - return b.asymmetricMatch(a, customTesters); + result = b.asymmetricMatch(a, customTesters); + diffBuilder.record(a, b); + return result; } } + function equals(a, b, customTesters, diffBuilder) { + customTesters = customTesters || []; + diffBuilder = diffBuilder || j$.NullDiffBuilder(); + + return eq(a, b, [], [], customTesters, diffBuilder); + } + // Equality function lovingly adapted from isEqual in // [Underscore](http://underscorejs.org) - function eq(a, b, aStack, bStack, customTesters) { - var result = true; + function eq(a, b, aStack, bStack, customTesters, diffBuilder) { + var result = true, i; - var asymmetricResult = asymmetricMatch(a, b, customTesters); + var asymmetricResult = asymmetricMatch(a, b, customTesters, diffBuilder); if (!j$.util.isUndefined(asymmetricResult)) { return asymmetricResult; } - for (var i = 0; i < customTesters.length; i++) { + for (i = 0; i < customTesters.length; i++) { var customTesterResult = customTesters[i](a, b); if (!j$.util.isUndefined(customTesterResult)) { + if (!customTesterResult) { + diffBuilder.record(a, b); + } return customTesterResult; } } if (a instanceof Error && b instanceof Error) { - return a.message == b.message; + result = a.message == b.message; + if (!result) { + diffBuilder.record(a, b); + } + return result; } // Identical objects are equal. `0 === -0`, but they aren't identical. // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). - if (a === b) { return a !== 0 || 1 / a == 1 / b; } + if (a === b) { + result = a !== 0 || 1 / a == 1 / b; + if (!result) { + diffBuilder.record(a, b); + } + return result; + } // A strict comparison is necessary because `null == undefined`. - if (a === null || b === null) { return a === b; } + if (a === null || b === null) { + result = a === b; + if (!result) { + diffBuilder.record(a, b); + } + return result; + } var className = Object.prototype.toString.call(a); - if (className != Object.prototype.toString.call(b)) { return false; } + if (className != Object.prototype.toString.call(b)) { + diffBuilder.record(a, b); + return false; + } switch (className) { // Strings, numbers, dates, and booleans are compared by value. case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. - return a == String(b); + result = a == String(b); + if (!result) { + diffBuilder.record(a, b); + } + return result; case '[object Number]': // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for // other numeric values. - return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b); + result = a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b); + if (!result) { + diffBuilder.record(a, b); + } + return result; case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. - return +a == +b; + result = +a == +b; + if (!result) { + diffBuilder.record(a, b); + } + return result; // RegExps are compared by their source patterns and flags. case '[object RegExp]': return a.source == b.source && @@ -127,14 +169,21 @@ getJasmineRequireObj().matchersUtil = function(j$) { a.multiline == b.multiline && a.ignoreCase == b.ignoreCase; } - if (typeof a != 'object' || typeof b != 'object') { return false; } + if (typeof a != 'object' || typeof b != 'object') { + diffBuilder.record(a, b); + return false; + } var aIsDomNode = j$.isDomNode(a); var bIsDomNode = j$.isDomNode(b); if (aIsDomNode && bIsDomNode) { // At first try to use DOM3 method isEqualNode if (a.isEqualNode) { - return a.isEqualNode(b); + result = a.isEqualNode(b); + if (!result) { + diffBuilder.record(a, b); + } + return result; } // IE8 doesn't support isEqualNode, try to use outerHTML && innerText var aIsElement = a instanceof Element; @@ -168,17 +217,21 @@ getJasmineRequireObj().matchersUtil = function(j$) { if (className == '[object Array]') { size = a.length; if (size !== b.length) { + diffBuilder.record(a, b); return false; } - while (size--) { - result = eq(a[size], b[size], aStack, bStack, customTesters); - if (!result) { - return false; - } + for (i = 0; i < size; i++) { + diffBuilder.withPath(i, function() { + result = eq(a[i], b[i], aStack, bStack, customTesters, diffBuilder) && result; + }); + } + if (!result) { + return false; } } else if (className == '[object Set]') { if (a.size != b.size) { + diffBuilder.record(a, b); return false; } var iterA = a.values(), iterB = b.values(); @@ -186,7 +239,8 @@ getJasmineRequireObj().matchersUtil = function(j$) { do { valA = iterA.next(); valB = iterB.next(); - if (!eq(valA.value, valB.value, aStack, bStack, customTesters)) { + if (!eq(valA.value, valB.value, aStack, bStack, customTesters, j$.NullDiffBuilder())) { + diffBuilder.record(a, b); return false; } } while (!valA.done && !valB.done); @@ -195,8 +249,12 @@ getJasmineRequireObj().matchersUtil = function(j$) { // Objects with different constructors are not equivalent, but `Object`s // or `Array`s from different frames are. var aCtor = a.constructor, bCtor = b.constructor; - if (aCtor !== bCtor && !(isObjectConstructor(aCtor) && - isObjectConstructor(bCtor))) { + if (aCtor !== bCtor && + isFunction(aCtor) && isFunction(bCtor) && + a instanceof aCtor && b instanceof bCtor && + !(aCtor instanceof aCtor && bCtor instanceof bCtor)) { + + diffBuilder.record(a, b, constructorsAreDifferentFormatter); return false; } } @@ -206,52 +264,66 @@ getJasmineRequireObj().matchersUtil = function(j$) { size = aKeys.length; // Ensure that both objects contain the same number of properties before comparing deep equality. - if (keys(b, className == '[object Array]').length !== size) { return false; } - - while (size--) { - key = aKeys[size]; - // Deep compare each member - result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters); - - if (!result) { - return false; - } + if (keys(b, className == '[object Array]').length !== size) { + diffBuilder.record(a, b, objectKeysAreDifferentFormatter); + return false; } + + for (i = 0; i < size; i++) { + key = aKeys[i]; + // Deep compare each member + if (!j$.util.has(b, key)) { + diffBuilder.record(a, b, objectKeysAreDifferentFormatter); + result = false; + continue; + } + + diffBuilder.withPath(key, function() { + if(!eq(a[key], b[key], aStack, bStack, customTesters, diffBuilder)) { + result = false; + } + }); + } + + if (!result) { + return false; + } + // Remove the first object from the stack of traversed objects. aStack.pop(); bStack.pop(); return result; + } - function keys(obj, isArray) { - var allKeys = Object.keys ? Object.keys(obj) : - (function(o) { - var keys = []; - for (var key in o) { - if (has(o, key)) { - keys.push(key); - } - } - return keys; - })(obj); - - if (!isArray) { - return allKeys; - } - - var extraKeys = []; - if (allKeys.length === 0) { - return allKeys; - } - - for (var x = 0; x < allKeys.length; x++) { - if (!allKeys[x].match(/^[0-9]+$/)) { - extraKeys.push(allKeys[x]); + function keys(obj, isArray) { + var allKeys = Object.keys ? Object.keys(obj) : + (function(o) { + var keys = []; + for (var key in o) { + if (j$.util.has(o, key)) { + keys.push(key); + } } - } + return keys; + })(obj); - return extraKeys; + if (!isArray) { + return allKeys; } + + if (allKeys.length === 0) { + return allKeys; + } + + var extraKeys = []; + for (var i in allKeys) { + if (!allKeys[i].match(/^[0-9]+$/)) { + extraKeys.push(allKeys[i]); + } + } + + return extraKeys; } function has(obj, key) { @@ -262,11 +334,44 @@ getJasmineRequireObj().matchersUtil = function(j$) { return typeof obj === 'function'; } - function isObjectConstructor(ctor) { - // aCtor instanceof aCtor is true for the Object and Function - // constructors (since a constructor is-a Function and a function is-a - // Object). We don't just compare ctor === Object because the constructor - // might come from a different frame with different globals. - return isFunction(ctor) && ctor instanceof ctor; + function objectKeysAreDifferentFormatter(actual, expected, path) { + var missingProperties = j$.util.objectDifference(expected, actual), + extraProperties = j$.util.objectDifference(actual, expected), + missingPropertiesMessage = formatKeyValuePairs(missingProperties), + extraPropertiesMessage = formatKeyValuePairs(extraProperties), + messages = []; + + if (!path.depth()) { + path = 'object'; + } + + if (missingPropertiesMessage.length) { + messages.push('Expected ' + path + ' to have properties' + missingPropertiesMessage); + } + + if (extraPropertiesMessage.length) { + messages.push('Expected ' + path + ' not to have properties' + extraPropertiesMessage); + } + + return messages.join('\n'); + } + + function constructorsAreDifferentFormatter(actual, expected, path) { + if (!path.depth()) { + path = 'object'; + } + + return 'Expected ' + + path + ' to be a kind of ' + + expected.constructor.name + + ', but was ' + j$.pp(actual) + '.'; + } + + function formatKeyValuePairs(obj) { + var formatted = ''; + for (var key in obj) { + formatted += '\n ' + key + ': ' + j$.pp(obj[key]); + } + return formatted; } }; diff --git a/src/core/matchers/toEqual.js b/src/core/matchers/toEqual.js index 653541bc..e6236457 100644 --- a/src/core/matchers/toEqual.js +++ b/src/core/matchers/toEqual.js @@ -1,4 +1,4 @@ -getJasmineRequireObj().toEqual = function() { +getJasmineRequireObj().toEqual = function(j$) { function toEqual(util, customEqualityTesters) { customEqualityTesters = customEqualityTesters || []; @@ -6,10 +6,14 @@ getJasmineRequireObj().toEqual = function() { return { compare: function(actual, expected) { var result = { - pass: false - }; + pass: false + }, + diffBuilder = j$.DiffBuilder(); - result.pass = util.equals(actual, expected, customEqualityTesters); + result.pass = util.equals(actual, expected, customEqualityTesters, diffBuilder); + + // TODO: only set error message if test fails + result.message = diffBuilder.getMessage(); return result; } diff --git a/src/core/requireCore.js b/src/core/requireCore.js index 50d07359..b82d1aa1 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -54,6 +54,9 @@ var getJasmineRequireObj = (function (jasmineGlobal) { j$.TreeProcessor = jRequire.TreeProcessor(); j$.version = jRequire.version(); j$.Order = jRequire.Order(); + j$.DiffBuilder = jRequire.DiffBuilder(j$); + j$.NullDiffBuilder = jRequire.NullDiffBuilder(j$); + j$.ObjectPath = jRequire.ObjectPath(j$); j$.matchers = jRequire.requireMatchers(jRequire, j$); diff --git a/src/core/util.js b/src/core/util.js index 5b53bf8d..121e3746 100644 --- a/src/core/util.js +++ b/src/core/util.js @@ -67,5 +67,21 @@ getJasmineRequireObj().util = function() { return descriptor; }; + util.objectDifference = function(obj, toRemove) { + var diff = {}; + + for (var key in obj) { + if (util.has(obj, key) && !util.has(toRemove, key)) { + diff[key] = obj[key]; + } + } + + return diff; + }; + + util.has = function(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); + }; + return util; };