google-closure-library
Version:
Google's common JavaScript library
1,448 lines (1,312 loc) • 66.7 kB
JavaScript
/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('goog.testing.asserts');
goog.setTestOnly();
goog.require('goog.dom.safe');
goog.require('goog.html.uncheckedconversions');
goog.require('goog.string');
goog.require('goog.string.Const');
goog.require('goog.testing.JsUnitException');
var DOUBLE_EQUALITY_PREDICATE = function(var1, var2) {
'use strict';
return var1 == var2;
};
var JSUNIT_UNDEFINED_VALUE = void 0;
var TO_STRING_EQUALITY_PREDICATE = function(var1, var2) {
'use strict';
return var1.toString() === var2.toString();
};
var OUTPUT_NEW_LINE_THRESHOLD = 40;
/** @typedef {function(?, ?):boolean} */
var PredicateFunctionType;
/**
* An associative array of constructors corresponding to primitive and
* well-known JS types.
* @const {!Array<string>}
*/
const PRIMITIVE_TRUE_TYPES =
['String', 'Boolean', 'Number', 'Array', 'RegExp', 'Date', 'Function'];
if (typeof ArrayBuffer === 'function') {
PRIMITIVE_TRUE_TYPES.push('ArrayBuffer');
}
/**
* @const {{
* String : !PredicateFunctionType,
* Number : !PredicateFunctionType,
* Boolean : !PredicateFunctionType,
* Date : !PredicateFunctionType,
* RegExp : !PredicateFunctionType,
* Function : !PredicateFunctionType,
* TrustedHTML : !PredicateFunctionType,
* TrustedScript : !PredicateFunctionType,
* TrustedScriptURL : !PredicateFunctionType
* }}
*/
const EQUALITY_PREDICATES = {
'String': DOUBLE_EQUALITY_PREDICATE,
'Number': DOUBLE_EQUALITY_PREDICATE,
'Bigint': DOUBLE_EQUALITY_PREDICATE,
'Boolean': DOUBLE_EQUALITY_PREDICATE,
'Date': function(date1, date2) {
'use strict';
return date1.getTime() == date2.getTime();
},
'RegExp': TO_STRING_EQUALITY_PREDICATE,
'Function': TO_STRING_EQUALITY_PREDICATE,
'TrustedHTML': TO_STRING_EQUALITY_PREDICATE,
'TrustedScript': TO_STRING_EQUALITY_PREDICATE,
'TrustedScriptURL': TO_STRING_EQUALITY_PREDICATE
};
/**
* Compares equality of two numbers, allowing them to differ up to a given
* tolerance.
* @param {number} var1 A number.
* @param {number} var2 A number.
* @param {number} tolerance the maximum allowed difference.
* @return {boolean} Whether the two variables are sufficiently close.
* @private
*/
goog.testing.asserts.numberRoughEqualityPredicate_ = function(
var1, var2, tolerance) {
'use strict';
return Math.abs(var1 - var2) <= tolerance;
};
/**
* @type {!Object<string, function(?, ?, number): boolean>}
* @private
*/
goog.testing.asserts.primitiveRoughEqualityPredicates_ = {
'Number': goog.testing.asserts.numberRoughEqualityPredicate_
};
var _trueTypeOf = function(something) {
'use strict';
let result = typeof something;
try {
switch (result) {
case 'string':
break;
case 'boolean':
break;
case 'number':
break;
case 'object':
if (something == null) {
result = 'null';
break;
}
case 'function':
let foundConstructor = false;
for (let i = 0; i < PRIMITIVE_TRUE_TYPES.length; i++) {
// NOTE: this cannot be a for-of loop because it's used from Rhino
// without the necessary Array.prototype[Symbol.iterator] polyfill.
const trueType = PRIMITIVE_TRUE_TYPES[i];
if (something.constructor === goog.global[trueType]) {
result = trueType;
foundConstructor = true;
break;
}
}
// Constructor doesn't match any of the known "primitive" constructors.
if (!foundConstructor) {
const m =
something.constructor.toString().match(/function\s*([^( ]+)\(/);
if (m) {
result = m[1];
}
}
break;
}
} catch (e) {
} finally {
result = result.slice(0, 1).toUpperCase() + result.slice(1);
}
return result;
};
var _displayStringForValue = function(aVar) {
'use strict';
var result;
try {
result = '<' + String(aVar) + '>';
} catch (ex) {
result = '<toString failed: ' + ex.message + '>';
// toString does not work on this object :-(
}
if (!(aVar === null || aVar === JSUNIT_UNDEFINED_VALUE)) {
result += ' (' + _trueTypeOf(aVar) + ')';
}
return result;
};
/** @param {?} failureMessage */
goog.testing.asserts.fail = function(failureMessage) {
'use strict';
_assert('Call to fail()', false, failureMessage);
};
/**
* @const
* @suppress {duplicate,checkTypes} Test frameworks like Jasmine may also
* define global fail functions.
*/
var fail = goog.testing.asserts.fail;
var argumentsIncludeComments = function(expectedNumberOfNonCommentArgs, args) {
'use strict';
return args.length == expectedNumberOfNonCommentArgs + 1;
};
var commentArg = function(expectedNumberOfNonCommentArgs, args) {
'use strict';
if (argumentsIncludeComments(expectedNumberOfNonCommentArgs, args)) {
return args[0];
}
return null;
};
var nonCommentArg = function(
desiredNonCommentArgIndex, expectedNumberOfNonCommentArgs, args) {
'use strict';
return argumentsIncludeComments(expectedNumberOfNonCommentArgs, args) ?
args[desiredNonCommentArgIndex] :
args[desiredNonCommentArgIndex - 1];
};
var _validateArguments = function(expectedNumberOfNonCommentArgs, args) {
'use strict';
var valid = args.length == expectedNumberOfNonCommentArgs ||
args.length == expectedNumberOfNonCommentArgs + 1 &&
typeof args[0] === 'string';
if (!valid) {
goog.testing.asserts.raiseException(
'Incorrect arguments passed to assert function.\n' +
'Expected ' + expectedNumberOfNonCommentArgs + ' argument(s) plus ' +
'optional comment; got ' + args.length + '.');
}
};
/**
* @return {?} goog.testing.TestCase or null
* We suppress the lint error and we explicitly do not goog.require()
* goog.testing.TestCase to avoid a build time dependency cycle.
* @suppress {missingRequire|undefinedVars|missingProperties}
* @private
*/
var _getCurrentTestCase = function() {
'use strict';
// Some users of goog.testing.asserts do not use goog.testing.TestRunner and
// they do not include goog.testing.TestCase. Exceptions will not be
// completely correct for these users.
if (!goog.testing.TestCase) {
if (goog.global.console) {
goog.global.console.error(
'Missing goog.testing.TestCase, ' +
'add /* @suppress {extraRequire} */' +
'goog.require(\'goog.testing.TestCase\')');
}
return null;
}
return goog.testing.TestCase.getActiveTestCase();
};
var _assert = function(comment, booleanValue, failureMessage) {
'use strict';
// If another framework has installed an adapter, tell it about the assertion.
var adapter =
typeof window !== 'undefined' && window['Closure assert adapter'];
if (adapter) {
adapter['assertWithMessage'](
booleanValue,
goog.testing.JsUnitException.generateMessage(comment, failureMessage));
// Also throw an error, for callers that assume that asserts throw. We don't
// include error details to avoid duplicate failure messages.
if (!booleanValue) throw new Error('goog.testing assertion failed');
}
if (!booleanValue) {
goog.testing.asserts.raiseException(comment, failureMessage);
}
};
/**
* @param {*} expected The expected value.
* @param {*} actual The actual value.
* @return {string} A failure message of the values don't match.
* @private
*/
goog.testing.asserts.getDefaultErrorMsg_ = function(expected, actual) {
'use strict';
var expectedDisplayString = _displayStringForValue(expected);
var actualDisplayString = _displayStringForValue(actual);
var shouldUseNewLines =
expectedDisplayString.length > OUTPUT_NEW_LINE_THRESHOLD ||
actualDisplayString.length > OUTPUT_NEW_LINE_THRESHOLD;
var msg = [
'Expected', expectedDisplayString, 'but was', actualDisplayString
].join(shouldUseNewLines ? '\n' : ' ');
if ((typeof expected == 'string') && (typeof actual == 'string')) {
// Try to find a human-readable difference.
var limit = Math.min(expected.length, actual.length);
var commonPrefix = 0;
while (commonPrefix < limit &&
expected.charAt(commonPrefix) == actual.charAt(commonPrefix)) {
commonPrefix++;
}
var commonSuffix = 0;
while (commonSuffix < limit &&
expected.charAt(expected.length - commonSuffix - 1) ==
actual.charAt(actual.length - commonSuffix - 1)) {
commonSuffix++;
}
if (commonPrefix + commonSuffix > limit) {
commonSuffix = 0;
}
if (commonPrefix > 2 || commonSuffix > 2) {
var printString = function(str) {
'use strict';
var startIndex = Math.max(0, commonPrefix - 2);
var endIndex = Math.min(str.length, str.length - (commonSuffix - 2));
return (startIndex > 0 ? '...' : '') +
str.substring(startIndex, endIndex) +
(endIndex < str.length ? '...' : '');
};
var expectedPrinted = printString(expected);
var expectedActual = printString(actual);
var shouldUseNewLinesInDiff =
expectedPrinted.length > OUTPUT_NEW_LINE_THRESHOLD ||
expectedActual.length > OUTPUT_NEW_LINE_THRESHOLD;
msg += '\nDifference was at position ' + commonPrefix + '. ' + [
'Expected', '[' + expectedPrinted + ']', 'vs. actual',
'[' + expectedActual + ']'
].join(shouldUseNewLinesInDiff ? '\n' : ' ');
}
}
return msg;
};
/**
* @param {*} a The value to assert (1 arg) or debug message (2 args).
* @param {*=} opt_b The value to assert (2 args only).
*/
goog.testing.asserts.assert = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var comment = commentArg(1, arguments);
var booleanValue = nonCommentArg(1, 1, arguments);
_assert(
comment, typeof booleanValue === 'boolean',
'Bad argument to assert(boolean): ' +
_displayStringForValue(booleanValue));
_assert(comment, booleanValue, 'Call to assert(boolean) with false');
};
/** @const */
var assert = goog.testing.asserts.assert;
/**
* Asserts that the function throws an error.
*
* @param {!(string|Function)} a The assertion comment or the function to call.
* @param {!Function=} opt_b The function to call (if the first argument of
* `assertThrows` was the comment).
* @return {!Error} The error thrown by the function. Beware that code may throw
* other types in strange scenarios.
* @throws {goog.testing.JsUnitException} If the assertion failed.
*/
goog.testing.asserts.assertThrows = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var func = nonCommentArg(1, 1, arguments);
var comment = commentArg(1, arguments);
_assert(
comment, typeof func == 'function',
'Argument passed to assertThrows is not a function');
try {
func();
} catch (e) {
goog.testing.asserts.removeOperaStacktrace_(e);
var testCase = _getCurrentTestCase();
if (e && e['isJsUnitException'] && testCase) {
goog.testing.asserts.raiseException(
comment,
'Function passed to assertThrows caught a JsUnitException (usually ' +
'from an assert or call to fail()). If this is expected, use ' +
'assertThrowsJsUnitException instead.');
}
return e;
}
goog.testing.asserts.raiseException(
comment, 'No exception thrown from function passed to assertThrows');
throw new Error('Should have thrown an error.'); // Make the compiler happy.
};
/** @const */
var assertThrows = goog.testing.asserts.assertThrows;
/**
* Removes a stacktrace from an Error object for Opera 10.0.
* @param {*} e
* @private
*/
goog.testing.asserts.removeOperaStacktrace_ = function(e) {
'use strict';
if (!goog.isObject(e)) return;
const stack = e['stacktrace'];
const errorMsg = e['message'];
if (typeof stack !== 'string' || typeof errorMsg !== 'string') {
return;
}
const stackStartIndex = errorMsg.length - stack.length;
if (errorMsg.indexOf(stack, stackStartIndex) == stackStartIndex) {
e['message'] = errorMsg.slice(0, stackStartIndex - 14);
}
};
/**
* Asserts that the function does not throw an error.
*
* @param {!(string|Function)} a The assertion comment or the function to call.
* @param {!Function=} opt_b The function to call (if the first argument of
* `assertNotThrows` was the comment).
* @return {*} The return value of the function.
* @throws {goog.testing.JsUnitException} If the assertion failed.
*/
goog.testing.asserts.assertNotThrows = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var comment = commentArg(1, arguments);
var func = nonCommentArg(1, 1, arguments);
_assert(
comment, typeof func == 'function',
'Argument passed to assertNotThrows is not a function');
try {
return func();
} catch (e) {
comment = comment ? (comment + '\n') : '';
comment += 'A non expected exception was thrown from function passed to ' +
'assertNotThrows';
// Some browsers don't have a stack trace so at least have the error
// description.
var stackTrace = e['stack'] || e['stacktrace'] || e.toString();
goog.testing.asserts.raiseException(comment, stackTrace);
}
};
/** @const */
var assertNotThrows = goog.testing.asserts.assertNotThrows;
/**
* Asserts that the given callback function results in a JsUnitException when
* called, and that the resulting failure message matches the given expected
* message.
* @param {function() : void} callback Function to be run expected to result
* in a JsUnitException (usually contains a call to an assert).
* @param {string=} opt_expectedMessage Failure message expected to be given
* with the exception.
* @return {!goog.testing.JsUnitException} The error thrown by the function.
* @throws {goog.testing.JsUnitException} If the function did not throw a
* JsUnitException.
*/
goog.testing.asserts.assertThrowsJsUnitException = function(
callback, opt_expectedMessage) {
'use strict';
try {
callback();
} catch (e) {
var testCase = _getCurrentTestCase();
if (testCase) {
testCase.invalidateAssertionException(e);
} else {
goog.global.console.error(
'Failed to remove expected exception: no test case is installed.');
}
if (!e.isJsUnitException) {
goog.testing.asserts.fail(
'Expected a JsUnitException, got \'' + e + '\' instead');
}
if (typeof opt_expectedMessage != 'undefined' &&
e.message != opt_expectedMessage) {
goog.testing.asserts.fail(
'Expected message [' + opt_expectedMessage + '] but got [' +
e.message + ']');
}
return e;
}
var msg = 'Expected a failure';
if (typeof opt_expectedMessage != 'undefined') {
msg += ': ' + opt_expectedMessage;
}
throw new goog.testing.JsUnitException(msg);
};
/** @const */
var assertThrowsJsUnitException =
goog.testing.asserts.assertThrowsJsUnitException;
/**
* Asserts that the IThenable rejects.
*
* This is useful for asserting that async functions throw, like an asynchronous
* assertThrows. Example:
*
* ```
* async function shouldThrow() { throw new Error('error!'); }
* async function testShouldThrow() {
* const error = await assertRejects(shouldThrow());
* assertEquals('error!', error.message);
* }
* ```
*
* @param {!(string|IThenable)} a The assertion comment or the IThenable.
* @param {!IThenable=} opt_b The IThenable (if the first argument of
* `assertRejects` was the comment).
* @return {!IThenable<*>} A child IThenable which resolves with the error that
* the passed in IThenable rejects with. This IThenable will reject if the
* passed in IThenable does not reject.
*/
goog.testing.asserts.assertRejects = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var thenable = /** @type {!IThenable<*>} */ (nonCommentArg(1, 1, arguments));
var comment = commentArg(1, arguments);
_assert(
comment, goog.isObject(thenable) && typeof thenable.then === 'function',
'Argument passed to assertRejects is not an IThenable');
return thenable.then(
function() {
'use strict';
goog.testing.asserts.raiseException(
comment, 'IThenable passed into assertRejects did not reject');
},
function(e) {
'use strict';
goog.testing.asserts.removeOperaStacktrace_(e);
return e;
});
};
/** @const */
var assertRejects = goog.testing.asserts.assertRejects;
/**
* @param {*} a The value to assert (1 arg) or debug message (2 args).
* @param {*=} opt_b The value to assert (2 args only).
*/
goog.testing.asserts.assertTrue = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var comment = commentArg(1, arguments);
var booleanValue = nonCommentArg(1, 1, arguments);
_assert(
comment, typeof booleanValue === 'boolean',
'Bad argument to assertTrue(boolean): ' +
_displayStringForValue(booleanValue));
_assert(comment, booleanValue, 'Call to assertTrue(boolean) with false');
};
/** @const */
var assertTrue = goog.testing.asserts.assertTrue;
/**
* @param {*} a The value to assert (1 arg) or debug message (2 args).
* @param {*=} opt_b The value to assert (2 args only).
*/
goog.testing.asserts.assertFalse = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var comment = commentArg(1, arguments);
var booleanValue = nonCommentArg(1, 1, arguments);
_assert(
comment, typeof booleanValue === 'boolean',
'Bad argument to assertFalse(boolean): ' +
_displayStringForValue(booleanValue));
_assert(comment, !booleanValue, 'Call to assertFalse(boolean) with true');
};
/** @const */
var assertFalse = goog.testing.asserts.assertFalse;
/**
* @param {*} a The expected value (2 args) or the debug message (3 args).
* @param {*} b The actual value (2 args) or the expected value (3 args).
* @param {*=} opt_c The actual value (3 args only).
*/
goog.testing.asserts.assertEquals = function(a, b, opt_c) {
'use strict';
_validateArguments(2, arguments);
var var1 = nonCommentArg(1, 2, arguments);
var var2 = nonCommentArg(2, 2, arguments);
_assert(
commentArg(2, arguments), var1 === var2,
goog.testing.asserts.getDefaultErrorMsg_(var1, var2));
};
/** @const */
var assertEquals = goog.testing.asserts.assertEquals;
/**
* @param {*} a The expected value (2 args) or the debug message (3 args).
* @param {*} b The actual value (2 args) or the expected value (3 args).
* @param {*=} opt_c The actual value (3 args only).
*/
goog.testing.asserts.assertNotEquals = function(a, b, opt_c) {
'use strict';
_validateArguments(2, arguments);
var var1 = nonCommentArg(1, 2, arguments);
var var2 = nonCommentArg(2, 2, arguments);
_assert(
commentArg(2, arguments), var1 !== var2,
'Expected not to be ' + _displayStringForValue(var2));
};
/** @const */
var assertNotEquals = goog.testing.asserts.assertNotEquals;
/**
* @param {*} a The value to assert (1 arg) or debug message (2 args).
* @param {*=} opt_b The value to assert (2 args only).
*/
goog.testing.asserts.assertNull = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var aVar = nonCommentArg(1, 1, arguments);
_assert(
commentArg(1, arguments), aVar === null,
goog.testing.asserts.getDefaultErrorMsg_(null, aVar));
};
/** @const */
var assertNull = goog.testing.asserts.assertNull;
/**
* @param {*} a The value to assert (1 arg) or debug message (2 args).
* @param {*=} opt_b The value to assert (2 args only).
*/
goog.testing.asserts.assertNotNull = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var aVar = nonCommentArg(1, 1, arguments);
_assert(
commentArg(1, arguments), aVar !== null,
'Expected not to be ' + _displayStringForValue(null));
};
/** @const */
var assertNotNull = goog.testing.asserts.assertNotNull;
/**
* @param {*} a The value to assert (1 arg) or debug message (2 args).
* @param {*=} opt_b The value to assert (2 args only).
*/
goog.testing.asserts.assertUndefined = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var aVar = nonCommentArg(1, 1, arguments);
_assert(
commentArg(1, arguments), aVar === JSUNIT_UNDEFINED_VALUE,
goog.testing.asserts.getDefaultErrorMsg_(JSUNIT_UNDEFINED_VALUE, aVar));
};
/** @const */
var assertUndefined = goog.testing.asserts.assertUndefined;
/**
* @param {*} a The value to assert (1 arg) or debug message (2 args).
* @param {*=} opt_b The value to assert (2 args only).
*/
goog.testing.asserts.assertNotUndefined = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var aVar = nonCommentArg(1, 1, arguments);
_assert(
commentArg(1, arguments), aVar !== JSUNIT_UNDEFINED_VALUE,
'Expected not to be ' + _displayStringForValue(JSUNIT_UNDEFINED_VALUE));
};
/** @const */
var assertNotUndefined = goog.testing.asserts.assertNotUndefined;
/**
* @param {*} a The value to assert (1 arg) or debug message (2 args).
* @param {*=} opt_b The value to assert (2 args only).
*/
goog.testing.asserts.assertNullOrUndefined = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var aVar = nonCommentArg(1, 1, arguments);
_assert(
commentArg(1, arguments), aVar == null,
'Expected ' + _displayStringForValue(null) + ' or ' +
_displayStringForValue(JSUNIT_UNDEFINED_VALUE) + ' but was ' +
_displayStringForValue(aVar));
};
/** @const */
var assertNullOrUndefined = goog.testing.asserts.assertNullOrUndefined;
/**
* @param {*} a The value to assert (1 arg) or debug message (2 args).
* @param {*=} opt_b The value to assert (2 args only).
*/
goog.testing.asserts.assertNotNullNorUndefined = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
goog.testing.asserts.assertNotNull.apply(null, arguments);
goog.testing.asserts.assertNotUndefined.apply(null, arguments);
};
/** @const */
var assertNotNullNorUndefined = goog.testing.asserts.assertNotNullNorUndefined;
/**
* @param {*} a The value to assert (1 arg) or debug message (2 args).
* @param {*=} opt_b The value to assert (2 args only).
*/
goog.testing.asserts.assertNonEmptyString = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var aVar = nonCommentArg(1, 1, arguments);
_assert(
commentArg(1, arguments), aVar !== JSUNIT_UNDEFINED_VALUE &&
aVar !== null && typeof aVar == 'string' && aVar !== '',
'Expected non-empty string but was ' + _displayStringForValue(aVar));
};
/** @const */
var assertNonEmptyString = goog.testing.asserts.assertNonEmptyString;
/**
* @param {*} a The value to assert (1 arg) or debug message (2 args).
* @param {*=} opt_b The value to assert (2 args only).
*/
goog.testing.asserts.assertNaN = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var aVar = nonCommentArg(1, 1, arguments);
_assert(
commentArg(1, arguments), aVar !== aVar,
'Expected NaN but was ' + _displayStringForValue(aVar));
};
/** @const */
var assertNaN = goog.testing.asserts.assertNaN;
/**
* @param {*} a The value to assert (1 arg) or debug message (2 args).
* @param {*=} opt_b The value to assert (2 args only).
*/
goog.testing.asserts.assertNotNaN = function(a, opt_b) {
'use strict';
_validateArguments(1, arguments);
var aVar = nonCommentArg(1, 1, arguments);
_assert(commentArg(1, arguments), !isNaN(aVar), 'Expected not NaN');
};
/** @const */
var assertNotNaN = goog.testing.asserts.assertNotNaN;
/**
* The return value of the equality predicate passed to findDifferences below,
* in cases where the predicate can't test the input variables for equality.
* @type {?string}
*/
goog.testing.asserts.EQUALITY_PREDICATE_CANT_PROCESS = null;
/**
* The return value of the equality predicate passed to findDifferences below,
* in cases where the input vriables are equal.
* @type {?string}
*/
goog.testing.asserts.EQUALITY_PREDICATE_VARS_ARE_EQUAL = '';
/**
* @const {!Object<string, boolean>}
*/
goog.testing.asserts.ARRAY_TYPES = {
'Array': true,
'Float32Array': true,
'Float64Array': true,
'Int8Array': true,
'Int16Array': true,
'Int32Array': true,
'Uint8Array': true,
'Uint8ClampedArray': true,
'Uint16Array': true,
'Uint32Array': true,
'BigInt64Array': true,
'BigUint64Array': true
};
/**
* The result of a comparison performed by an EqualityFunction: if undefined,
* the inputs are equal; otherwise, a human-readable description of their
* inequality.
*
* @typedef {string|undefined}
*/
goog.testing.asserts.ComparisonResult;
/**
* A equality predicate.
*
* The first two arguments are the values to be compared. The third is an
* equality function which can be used to recursively apply findDifferences.
*
* An example comparison implementation for Array could be:
*
* function arrayEq(a, b, eq) {
* if (a.length !== b.length) {
* return "lengths unequal";
* }
*
* const differences = [];
* for (let i = 0; i < a.length; i++) {
* // Use the findDifferences implementation to perform recursive
* // comparisons.
* const diff = eq(a[i], b[i], eq);
* if (diff) {
* differences[i] = diff;
* }
* }
*
* if (differences) {
* return `found array differences: ${differences}`;
* }
*
* // Otherwise return undefined, indicating no differences.
* return undefined;
* }
*
* @typedef {function(?, ?, !goog.testing.asserts.EqualityFunction):
* ?goog.testing.asserts.ComparisonResult}
*/
goog.testing.asserts.EqualityFunction;
/**
* A map from prototype to custom equality matcher.
*
* @type {!Map<!Object, !goog.testing.asserts.EqualityFunction>}
* @private
*/
goog.testing.asserts.CUSTOM_EQUALITY_MATCHERS = new Map();
/**
* Returns the custom equality predicate for a given prototype, or else
* undefined.
*
* @param {?Object} prototype
* @return {!goog.testing.asserts.EqualityFunction|undefined}
* @private
*/
goog.testing.asserts.getCustomEquality = function(prototype) {
for (; (prototype != null) && (typeof prototype === 'object') &&
(prototype !== Object.prototype);
prototype = Object.getPrototypeOf(prototype)) {
const matcher = goog.testing.asserts.CUSTOM_EQUALITY_MATCHERS.get(
/** @type {!Object} */ (prototype));
if (matcher) {
return matcher;
}
}
return undefined;
};
/**
* Returns the most specific custom equality predicate which can be applied to
* both arguments, or else undefined.
*
* @param {!Object} obj1
* @param {!Object} obj2
* @return {!goog.testing.asserts.EqualityFunction|undefined}
* @private
*/
goog.testing.asserts.getMostSpecificCustomEquality = function(obj1, obj2) {
for (let prototype = Object.getPrototypeOf(obj1); (prototype != null) &&
(typeof prototype === 'object') && (prototype !== Object.prototype);
prototype = Object.getPrototypeOf(prototype)) {
if (prototype.isPrototypeOf(obj2)) {
return goog.testing.asserts.getCustomEquality(prototype);
}
}
// Otherwise, obj1 and obj2 did not share a common ancestor other than
// Object.prototype so we cannot have a comparator.
return undefined;
};
/**
* Executes a custom equality function
*
* @param {!goog.testing.asserts.EqualityFunction} comparator
* @param {!Object} obj1
* @param {!Object} obj2
* @param {string} path of the current field being checked.
* @return {?goog.testing.asserts.ComparisonResult}
* @private
*/
goog.testing.asserts.applyCustomEqualityFunction = function(
comparator, obj1, obj2, path) {
const /* !goog.testing.asserts.EqualityFunction */ callback =
(left, right, unusedEq) => {
const result = goog.testing.asserts.findDifferences(left, right);
return result ? (path ? path + ': ' : '') + result : undefined;
};
return comparator(obj1, obj2, callback);
};
/**
* Marks the given prototype as having equality semantics provided by the given
* custom equality function.
*
* This will cause findDifferences and assertObjectEquals to use the given
* function when comparing objects with this prototype. When comparing two
* objects with different prototypes, the equality (if any) attached to their
* lowest common ancestor in the prototype hierarchy will be used.
*
* @param {!Object} prototype
* @param {!goog.testing.asserts.EqualityFunction} fn
*/
goog.testing.asserts.registerComparator = function(prototype, fn) {
// First check that there is no comparator currently defined for this
// prototype.
if (goog.testing.asserts.CUSTOM_EQUALITY_MATCHERS.has(prototype)) {
throw new Error('duplicate comparator installation for ' + prototype);
}
// We cannot install custom equality matchers on Object.prototype, as it
// would replace all other comparisons.
if (prototype === Object.prototype) {
throw new Error('cannot customize root object comparator');
}
goog.testing.asserts.CUSTOM_EQUALITY_MATCHERS.set(prototype, fn);
};
/**
* Clears the custom equality function currently applied to the given prototype.
* Returns true if a function was removed.
*
* @param {!Object} prototype
* @return {boolean} whether a comparator was removed.
*/
goog.testing.asserts.clearCustomComparator = function(prototype) {
return goog.testing.asserts.CUSTOM_EQUALITY_MATCHERS.delete(prototype);
};
/**
* Determines if two items of any type match, and formulates an error message
* if not.
* @param {*} expected Expected argument to match.
* @param {*} actual Argument as a result of performing the test.
* @param {(function(string, *, *): ?string)=} opt_equalityPredicate An optional
* function that can be used to check equality of variables. It accepts 3
* arguments: type-of-variables, var1, var2 (in that order) and returns an
* error message if the variables are not equal,
* goog.testing.asserts.EQUALITY_PREDICATE_VARS_ARE_EQUAL if the variables
* are equal, or
* goog.testing.asserts.EQUALITY_PREDICATE_CANT_PROCESS if the predicate
* couldn't check the input variables. The function will be called only if
* the types of var1 and var2 are identical.
* @return {?string} Null on success, error message on failure.
*/
goog.testing.asserts.findDifferences = function(
expected, actual, opt_equalityPredicate) {
'use strict';
var failures = [];
// Non-null if there an error at the root (with no path). If so, we should
// fail, but not add to the failures array (because it will be included at the
// top anyway).
let /** ?string*/ rootFailure = null;
var seen1 = [];
var seen2 = [];
// To avoid infinite recursion when the two parameters are self-referential
// along the same path of properties, keep track of the object pairs already
// seen in this call subtree, and abort when a cycle is detected.
function innerAssertWithCycleCheck(var1, var2, path) {
// This is used for testing, so we can afford to be slow (but more
// accurate). So we just check whether var1 is in seen1. If we
// found var1 in index i, we simply need to check whether var2 is
// in seen2[i]. If it is, we do not recurse to check var1/var2. If
// it isn't, we know that the structures of the two objects must be
// different.
//
// This is based on the fact that values at index i in seen1 and
// seen2 will be checked for equality eventually (when
// innerAssertImplementation(seen1[i], seen2[i], path) finishes).
for (var i = 0; i < seen1.length; ++i) {
var match1 = seen1[i] === var1;
var match2 = seen2[i] === var2;
if (match1 || match2) {
if (!match1 || !match2) {
// Asymmetric cycles, so the objects have different structure.
failures.push('Asymmetric cycle detected at ' + path);
}
return;
}
}
seen1.push(var1);
seen2.push(var2);
innerAssertImplementation(var1, var2, path);
seen1.pop();
seen2.pop();
}
const equalityPredicate = function(type, var1, var2) {
'use strict';
// use the custom predicate if supplied.
const customPredicateResult = opt_equalityPredicate ?
opt_equalityPredicate(type, var1, var2) :
goog.testing.asserts.EQUALITY_PREDICATE_CANT_PROCESS;
if (customPredicateResult !==
goog.testing.asserts.EQUALITY_PREDICATE_CANT_PROCESS) {
return customPredicateResult;
}
// otherwise use the default behavior.
const typedPredicate = EQUALITY_PREDICATES[type];
if (!typedPredicate) {
return goog.testing.asserts.EQUALITY_PREDICATE_CANT_PROCESS;
}
const equal = typedPredicate(var1, var2);
return equal ? goog.testing.asserts.EQUALITY_PREDICATE_VARS_ARE_EQUAL :
goog.testing.asserts.getDefaultErrorMsg_(var1, var2);
};
/**
* @param {*} var1 An item in the expected object.
* @param {*} var2 The corresponding item in the actual object.
* @param {string} path Their path in the objects.
* @suppress {missingProperties} The map_ property is unknown to the compiler
* unless goog.structs.Map is loaded.
*/
function innerAssertImplementation(var1, var2, path) {
if (var1 === var2) {
return;
}
var typeOfVar1 = _trueTypeOf(var1);
var typeOfVar2 = _trueTypeOf(var2);
if (typeOfVar1 === typeOfVar2) {
// For two objects of the same type, if one is a prototype of another, use
// the custom equality function for the more generic of the two
// prototypes, if available.
if (var1 && typeof var1 === 'object') {
try {
const o1 = /** @type {!Object} */ (var1);
const o2 = /** @type {!Object} */ (var2);
const comparator =
goog.testing.asserts.getMostSpecificCustomEquality(o1, o2);
if (comparator) {
const result = goog.testing.asserts.applyCustomEqualityFunction(
comparator, o1, o2, path);
if (result != null) {
if (path) {
failures.push(path + ': ' + result);
} else {
rootFailure = result;
}
}
return;
}
} catch (e) {
// Catch and log errors from custom comparators but fall back onto
// ordinary comparisons. Such errors can occur, e.g. with proxies or
// when the prototypes of a polyfill are not traversable.
//
// If you see a failure due to this line, please do not use
// findDifferences or assertObjectEquals on these argument types.
goog.global.console.error('Error in custom comparator: ' + e);
}
}
const isArrayBuffer = typeOfVar1 === 'ArrayBuffer';
if (isArrayBuffer) {
// Since ArrayBuffer instances can't themselves be iterated through,
// compare 1-byte-per-element views of them.
var1 = new Uint8Array(/** @type {!ArrayBuffer} */ (var1));
var2 = new Uint8Array(/** @type {!ArrayBuffer} */ (var2));
}
const isArray =
isArrayBuffer || goog.testing.asserts.ARRAY_TYPES[typeOfVar1];
var errorMessage = equalityPredicate(typeOfVar1, var1, var2);
if (errorMessage !=
goog.testing.asserts.EQUALITY_PREDICATE_CANT_PROCESS) {
if (errorMessage !=
goog.testing.asserts.EQUALITY_PREDICATE_VARS_ARE_EQUAL) {
if (path) {
failures.push(path + ': ' + errorMessage);
} else {
rootFailure = errorMessage;
}
}
} else if (isArray && var1.length != var2.length) {
failures.push(
(path ? path + ': ' : '') + 'Expected ' + var1.length +
'-element array ' +
'but got a ' + var2.length + '-element array');
} else if (typeOfVar1 == 'String') {
// If the comparer cannot process strings (eg, roughlyEquals).
if (var1 != var2) {
const error = goog.testing.asserts.getDefaultErrorMsg_(var1, var2);
if (path) {
failures.push(path + ': ' + error);
} else {
rootFailure = error;
}
}
} else {
var childPath = path + (isArray ? '[%s]' : (path ? '.%s' : '%s'));
// These type checks do not use _trueTypeOf because that does not work
// for polyfilled Map/Set. Note that these checks may potentially fail
// if var1 comes from a different window.
if ((typeof Map != 'undefined' && var1 instanceof Map) ||
(typeof Set != 'undefined' && var1 instanceof Set)) {
var1.forEach(function(value, key) {
'use strict';
if (var2.has(key)) {
// For a map, the values must be compared, but with Set, checking
// that the second set contains the first set's "keys" is
// sufficient.
if (var2.get) {
innerAssertWithCycleCheck(
// NOTE: replace will call functions, so stringify eagerly.
value, var2.get(key), childPath.replace('%s', String(key)));
}
} else {
failures.push(
key + ' not present in actual ' + (path || typeOfVar2));
}
});
var2.forEach(function(value, key) {
'use strict';
if (!var1.has(key)) {
failures.push(
key + ' not present in expected ' + (path || typeOfVar1));
}
});
} else if (!var1['__iterator__']) {
// if an object has an __iterator__ property, we have no way of
// actually inspecting its raw properties, and JS 1.7 doesn't
// overload [] to make it possible for someone to generically
// use what the iterator returns to compare the object-managed
// properties. This gets us into deep poo with things like
// goog.structs.Map, at least on systems that support iteration.
for (var prop in var1) {
if (isArray && goog.testing.asserts.isArrayIndexProp_(prop)) {
// Skip array indices for now. We'll handle them later.
continue;
}
if (prop in var2) {
innerAssertWithCycleCheck(
var1[prop], var2[prop], childPath.replace('%s', prop));
} else {
failures.push(
'property ' + prop + ' not present in actual ' +
(path || typeOfVar2));
}
}
// make sure there aren't properties in var2 that are missing
// from var1. if there are, then by definition they don't
// match.
for (var prop in var2) {
if (isArray && goog.testing.asserts.isArrayIndexProp_(prop)) {
// Skip array indices for now. We'll handle them later.
continue;
}
if (!(prop in var1)) {
failures.push(
'property ' + prop + ' not present in expected ' +
(path || typeOfVar1));
}
}
// Handle array indices by iterating from 0 to arr.length.
//
// Although all browsers allow holes in arrays, browsers
// are inconsistent in what they consider a hole. For example,
// "[0,undefined,2]" has a hole on IE but not on Firefox.
//
// Because our style guide bans for...in iteration over arrays,
// we assume that most users don't care about holes in arrays,
// and that it is ok to say that a hole is equivalent to a slot
// populated with 'undefined'.
if (isArray) {
for (prop = 0; prop < var1.length; prop++) {
innerAssertWithCycleCheck(
var1[prop], var2[prop],
childPath.replace('%s', String(prop)));
}
}
} else {
// special-case for closure objects that have iterators
if (typeof var1.equals === 'function') {
// use the object's own equals function, assuming it accepts an
// object and returns a boolean
if (!var1.equals(var2)) {
failures.push(
'equals() returned false for ' + (path || typeOfVar1));
}
} else if (var1.map_) {
// assume goog.structs.Map or goog.structs.Set, where comparing
// their private map_ field is sufficient
innerAssertWithCycleCheck(
var1.map_, var2.map_, childPath.replace('%s', 'map_'));
} else {
// else die, so user knows we can't do anything
failures.push(
'unable to check ' + (path || typeOfVar1) +
' for equality: it has an iterator we do not ' +
'know how to handle. please add an equals method');
}
}
}
} else if (path) {
failures.push(
path + ': ' + goog.testing.asserts.getDefaultErrorMsg_(var1, var2));
} else {
rootFailure = goog.testing.asserts.getDefaultErrorMsg_(var1, var2);
}
}
innerAssertWithCycleCheck(expected, actual, '');
if (rootFailure) {
return rootFailure;
}
return failures.length == 0 ? null : goog.testing.asserts.getDefaultErrorMsg_(
expected, actual) +
'\n ' + failures.join('\n ');
};
/**
* Notes:
* Object equality has some nasty browser quirks, and this implementation is
* not 100% correct. For example,
*
* <code>
* var a = [0, 1, 2];
* var b = [0, 1, 2];
* delete a[1];
* b[1] = undefined;
* assertObjectEquals(a, b); // should fail, but currently passes
* </code>
*
* See asserts_test.html for more interesting edge cases.
*
* The first comparison object provided is the expected value, the second is
* the actual.
*
* @param {*} a Assertion message or comparison object.
* @param {*} b Comparison object.
* @param {*=} opt_c Comparison object, if an assertion message was provided.
*/
goog.testing.asserts.assertObjectEquals = function(a, b, opt_c) {
'use strict';
_validateArguments(2, arguments);
var v1 = nonCommentArg(1, 2, arguments);
var v2 = nonCommentArg(2, 2, arguments);
var failureMessage = commentArg(2, arguments) ? commentArg(2, arguments) : '';
var differences = goog.testing.asserts.findDifferences(v1, v2);
_assert(failureMessage, !differences, differences);
};
/** @const */
var assertObjectEquals = goog.testing.asserts.assertObjectEquals;
/**
* Similar to assertObjectEquals above, but accepts a tolerance margin.
*
* @param {*} a Assertion message or comparison object.
* @param {*} b Comparison object.
* @param {*} c Comparison object or tolerance.
* @param {*=} opt_d Tolerance, if an assertion message was provided.
*/
goog.testing.asserts.assertObjectRoughlyEquals = function(a, b, c, opt_d) {
'use strict';
_validateArguments(3, arguments);
var v1 = nonCommentArg(1, 3, arguments);
var v2 = nonCommentArg(2, 3, arguments);
var tolerance = nonCommentArg(3, 3, arguments);
var failureMessage = commentArg(3, arguments) ? commentArg(3, arguments) : '';
var equalityPredicate = function(type, var1, var2) {
'use strict';
var typedPredicate =
goog.testing.asserts.primitiveRoughEqualityPredicates_[type];
if (!typedPredicate) {
return goog.testing.asserts.EQUALITY_PREDICATE_CANT_PROCESS;
}
var equal = typedPredicate(var1, var2, tolerance);
return equal ? goog.testing.asserts.EQUALITY_PREDICATE_VARS_ARE_EQUAL :
goog.testing.asserts.getDefaultErrorMsg_(var1, var2) +
' which was more than ' + tolerance + ' away';
};
var differences =
goog.testing.asserts.findDifferences(v1, v2, equalityPredicate);
_assert(failureMessage, !differences, differences);
};
/** @const */
var assertObjectRoughlyEquals = goog.testing.asserts.assertObjectRoughlyEquals;
/**
* Compares two arbitrary objects for non-equalness.
*
* All the same caveats as for assertObjectEquals apply here:
* Undefined values may be confused for missing values, or vice versa.
*
* @param {*} a Assertion message or comparison object.
* @param {*} b Comparison object.
* @param {*=} opt_c Comparison object, if an assertion message was provided.
*/
goog.testing.asserts.assertObjectNotEquals = function(a, b, opt_c) {
'use strict';
_validateArguments(2, arguments);
var v1 = nonCommentArg(1, 2, arguments);
var v2 = nonCommentArg(2, 2, arguments);
var failureMessage = commentArg(2, arguments) ? commentArg(2, arguments) : '';
var differences = goog.testing.asserts.findDifferences(v1, v2);
_assert(failureMessage, differences, 'Objects should not be equal');
};
/** @const */
var assertObjectNotEquals = goog.testing.asserts.assertObjectNotEquals;
/**
* Compares two arrays ignoring negative indexes and extra properties on the
* array objects. Use case: Internet Explorer adds the index, lastIndex and
* input enumerable fields to the result of string.match(/regexp/g), which makes
* assertObjectEquals fail.
* @param {*} a The expected array (2 args) or the debug message (3 args).
* @param {*} b The actual array (2 args) or the expected array (3 args).
* @param {*=} opt_c The actual array (3 args only).
*/
goog.testing.asserts.assertArrayEquals = function(a, b, opt_c) {
'use strict';
_validateArguments(2, arguments);
var v1 = nonCommentArg(1, 2, arguments);
var v2 = nonCommentArg(2, 2, arguments);
var failureMessage = commentArg(2, arguments) ? commentArg(2, arguments) : '';
var typeOfVar1 = _trueTypeOf(v1);
_assert(
failureMessage, typeOfVar1 == 'Array',
'Expected an array for assertArrayEquals but found a ' + typeOfVar1);
var typeOfVar2 = _trueTypeOf(v2);
_assert(
failureMessage, typeOfVar2 == 'Array',
'Expected an array for assertArrayEquals but found a ' + typeOfVar2);
goog.testing.asserts.assertObjectEquals(
failureMessage, Array.prototype.concat.call(v1),
Array.prototype.concat.call(v2));
};
/** @const */
var assertArrayEquals = goog.testing.asserts.assertArrayEquals;
/**
* Compares two objects that can be accessed like an array and assert that
* each element is equal.
* @param {string|Object} a Failure message (3 arguments)
* or object #1 (2 arguments).
* @param {Object} b Object #2 (2 arguments) or object #1 (3 arguments).
* @param {Object=} opt_c Object #2 (3 arguments).
*/
goog.testing.asserts.assertElementsEquals = function(a, b, opt_c) {
'use strict';
_validateArguments(2, arguments);
var v1 = nonCommentArg(1, 2, arguments);
var v2 = nonCommentArg(2, 2, arguments);
var failureMessage = commentArg(2, arguments) ? commentArg(2, arguments) : '';
if (!v1) {
goog.testing.asserts.assert(failureMessage, !v2);
} else {
goog.testing.asserts.assertEquals(
'length mismatch: ' + failureMessage, v1.length, v2.length);
for (var i = 0; i < v1.length; ++i) {
goog.testing.asserts.assertEquals(
'mismatch at index ' + i + ': ' + failureMessage, v1[i], v2[i]);
}
}
};
/** @const */
var assertElementsEquals = goog.testing.asserts.assertElementsEquals;
/**
* Compares two objects that can be accessed like an array and assert that
* each element is roughly equal.
* @param {string|Object} a Failure message (4 arguments)
* or object #1 (3 arguments).
* @param {Object} b Object #1 (4 arguments) or object #2 (3 arguments).
* @param {Object|number} c Object #2 (4 arguments) or tolerance (3 arguments).
* @param {number=} opt_d tolerance (4 arguments).
*/
goog.testing.asserts.assertElementsRoughlyEqual = function(a, b, c, opt_d) {
'use strict';
_validateArguments(3, arguments);
var v1 = nonCommentArg(1, 3, arguments);
var v2 = nonCommentArg(2, 3, arguments);
var tolerance = nonCommentArg(3, 3, arguments);
var failureMessage = commentArg(3, arguments) ? commentArg(3, arguments) : '';
if (!v1) {
goog.testing.asserts.assert(failureMessage, !v2);
} else {
goog.testing.asserts.assertEquals(
'length mismatch: ' + failureMessage, v1.length, v2.length);
for (var i = 0; i < v1.length; ++i) {
goog.testing.asserts.assertRoughlyEquals(
failureMessage, v1[i], v2[i], tolerance);
}
}
};
/** @const */
var assertElementsRoughlyEqual =
goog.testing.asserts.assertElementsRoughlyEqual;
/**
* Compares elements of two array-like or iterable objects using strict equality
* without taking their order into account.
* @param {string|!IArrayLike|!Iterable} a Assertion message or the
* expected elements.
* @param {!IArrayLike|!Iterable} b Expected elements or the actual
* elements.
* @param {!IArrayLike|!Iterable=} opt_c Actual elements.
*/
goog.testing.asserts.assertSameElements = function(a, b, opt_c) {
'use strict';
_validateArguments(2, arguments);
var expected = nonCommentArg(1, 2, arguments);
var actual = nonCommentArg(2, 2, arguments);
var message = commentArg(2, arguments);
goog.testing.asserts.assertTrue(
'Value of \'expected\' should be array-like or iterable',
goog.testing.asserts.isArrayLikeOrIterable_(expected));
goog.testing.asserts.assertTrue(
'Value of \'actual\' should be array-like or iterable',
goog.testing.asserts.isArrayLikeOrIterable_(actual));
// Clones expected and actual and converts them to real arrays.
expected = goog.testing.asserts.toArray_(expected);
actual = goog.testing.asserts.toArray_(actual);
// TODO(user): It would be great to show only the difference
// between the expected and actual elements.
_assert(
message, expected.length == actual.length, 'Expected ' + expected.length +