This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/478188/1332408/NH_xunit.js
// ==UserScript==
// ==UserLibrary==
// @name NH_xunit
// @description xUnit style testing.
// @version 53
// @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0-standalone.html
// @homepageURL https://github.com/nexushoratio/userscripts
// @supportURL https://github.com/nexushoratio/userscripts/issues
// @match https://www.example.com/*
// ==/UserLibrary==
// ==/UserScript==
window.NexusHoratio ??= {};
window.NexusHoratio.xunit = (function xunit() {
'use strict';
/** @type {number} - Bumped per release. */
const version = 53;
/**
* @type {object} - For testing support.
*/
const testing = {
enabled: false,
missingDescriptionsAreErrors: true,
testCases: [],
};
/** Data about a test execution. */
class TestExecution {
start = 0;
stop = 0;
}
/** Accumulated results from running a TestCase. */
class TestResult {
/** Unexpected exceptions. */
errors = [];
/** Explicit test failures (typically failed asserts). */
failures = [];
/** Skipped tests. */
skipped = [];
/** Successes. */
successes = [];
/** All test executions. */
tests = new Map();
/**
* Record an unexpected exception from a execution.
* @param {string} name - Name of the TestCase.testMethod.
* @param {Error} exception - Exception caught.
*/
addError(name, exception) {
this.errors.push({
name: name,
error: exception.name,
message: exception.message,
});
}
/**
* Record a test failure.
* @param {string} name - Name of the TestCase.testMethod.
* @param {string} message - Message from the test or framework.
*/
addFailure(name, message) {
this.failures.push({
name: name,
message: message,
});
}
/**
* Record a test skipped.
* @param {string} name - Name of the TestCase.testMethod.
* @param {string} message - Reason the test was skipped.
*/
addSkip(name, message) {
this.skipped.push({
name: name,
message: message,
});
}
/**
* Record a successful execution.
* @param {string} name - Name of the TestCase.testMethod.
*/
addSuccess(name) {
this.successes.push(name);
}
/**
* Record the start of a test execution.
* @param {string} name - Name of the TestCase.testMethod.
*/
startTest(name) {
const execution = new TestExecution();
execution.start = Date.now();
this.tests.set(name, execution);
}
/**
* Record the stop of a test execution.
* @param {string} name - Name of the TestCase.testMethod.
*/
stopTest(name) {
this.tests.get(name).stop = Date.now();
}
/** @returns {boolean} - Indicates success so far. */
wasSuccessful() {
return this.errors.length === 0 && this.failures.length === 0;
}
/**
* Text summary of the results.
*
* Useful for test runners.
* @param {boolean} [formatted=false] - Try to line things up columns.
* @returns {string[]} - Summary, one line per entry in the array.
*/
summary(formatted = false) {
const fields = ['total', 'successes', 'skipped', 'errors', 'failures'];
const numbers = new Map();
const results = [];
let maxFieldLength = 0;
let maxCountLength = 0;
for (const field of fields) {
if (field === 'total') {
// Double duty: renaming 'tests' to 'total', and using '.size'
numbers.set(field, this.tests.size);
} else {
numbers.set(field, this[field].length);
}
}
if (formatted) {
maxFieldLength = Math.max(...Array.from(numbers.keys())
.map(x => x.length));
maxCountLength = String(Math.max(...numbers.values())).length;
}
for (const field of fields) {
const f = field.padEnd(maxFieldLength);
const v = `${numbers.get(field)}`.padStart(maxCountLength);
results.push(`${f} : ${v}`);
}
return results;
}
}
/**
* Attempt to get the type of item.
*
* This is internal to xunit, so no need to make it equivalent to the
* built-in `typeof` operator. Hence, results are explicitly NOT
* lower-cased in order to reduce chances of conflicts.
*
* This just needs to be good enough to find a comparator function.
* @param {*} item - Item to inspect.
* @returns {string} - The likely type of item.
*/
function getType(item) {
const builtInClasses = [
'Array',
'Date',
'Error',
'Map',
'Set',
];
let type = Object.prototype.toString.call(item)
.replace(/^\[object (?<type>.*)\]$/u, '$<type>');
if (type === 'Function') {
if (String(item)
.startsWith('class ')) {
type = 'class';
} else if (builtInClasses.includes(item.name)) {
type = 'class';
}
}
if (type === 'Object') {
if (typeof item.constructor.name === 'string') {
type = item.constructor.name;
}
}
return type;
}
/**
* An xUnit style test framework.
*
* Many expected methods exist, such as setUp, setUpClass, addCleanup,
* addClassCleanup, etc. No tearDown methods, however; use addCleanup.
*
* Assertion methods should always take a plain text string, typically named
* `msg`, as the last parameter. This string should be added to the
* assertion specific error message in case of a failure.
*
* JavaScript does not have portable access to things like line numbers and
* stack traces (and in the case of userscripts, those may be inaccurate
* anyway). So it can be difficult to track down a particular test failure.
* The failure messages do include the name of the test class and test
* method, but, if the method happens to have several assertions in it, it
* may not be obvious which one failed. These extra descriptive messages
* can help with differentiation. This system will emit a debug message if
* any test method calls more than one assert method without a descriptive
* message.
*
* While the *assertEqual()* method will handle many cases by looking up
* special functions comparing by type. There may be times when what it can
* handle needs to be enhanced. There are currently two ways to make such
* enhancements.
*
* First, the method *addEqualFunc()* will allow the test method to register
* an additional function for comparing two identical instances.
*
* Second, the property *defaultEqual* points to whatever *equalX()*
* function should be used if one cannot be found, or if instances differ by
* type. This fallback defaults to *equalEqEqEq()* which uses the strict
* equality (`===`) operator. This can be explicitly set in the test
* method. The method *equalValueOf()* will use the instance's *valueOf()*
* method to get comparable values, and may be useful in such cases.
*
* In many languages, order does not matter for some built-in container
* types (e.g., Map, Set). The JavaScript standard explicitly specifies
* that order DOES matter for these types. However, for this test library,
* the default *equalX()* functions explicitly IGNORE order.
*
* Some built-in types (e.g., Map, Set), do not have good string
* representations when showing up in error messages. While user classes
* can provide a *toString()* method, sometimes they may not be available.
* To help with this situation, this class provides a registration system
* similar to the one used for equality functions.
*
* The property *defaultRepr* points to *String()*, but may be overridden
* for an invocation.
*
* The method *addReprFunc()* can allow users to register their own.
*
* Implementations for built-in types will be added as needed.
*
* All *assertX()* and *equalX()* methods should use *this.repr()* to turn
* values into strings for these error messages.
*
* TestCases should run only one test method per instance. The name of the
* method is registered during instantiation and invoked by calling
* *instance.run().*. Generally, a system, like {@link TestRunner} is used
* to register a number of TestCases, discover the test methods, and invoke
* all of them in turn.
*
* @example
* class FooTestCase extends TestCase {
* testMethod() {
* // Assemble - Act
*
* // Assert
* this.assertEqual(actual, expected, 'extra message');
* }
* }
*
* const test = new FooTestCase('testMethod');
* const result = test.run();
*/
class TestCase {
/**
* Instantiate a TestCase.
* @param {string} methodName - The method to run on this instantiation.
*/
constructor(methodName) {
if (new.target === TestCase) {
throw new TypeError('Abstract class; do not instantiate directly.');
}
this.#methodName = methodName;
this.defaultRepr = String;
this.addReprFunc('String', this.reprString);
this.addReprFunc('Array', this.reprArray);
this.addReprFunc('Object', this.reprObject);
this.addReprFunc('Map', this.reprMap);
this.addReprFunc('Set', this.reprSet);
this.defaultEqual = this.equalEqEqEq;
this.addEqualFunc('String', this.equalString);
this.addEqualFunc('Array', this.equalArray);
this.addEqualFunc('Object', this.equalObject);
this.addEqualFunc('Map', this.equalMap);
this.addEqualFunc('Set', this.equalSet);
this.addCleanup(this.#checkAssertionCounts);
}
static Error = class extends Error {
/** @inheritdoc */
constructor(...rest) {
super(...rest);
this.name = `TestCase.${this.constructor.name}`;
}
};
static Fail = class extends this.Error {}
static Skip = class extends this.Error {}
static classCleanups = [];
/** Called once before any instances are created. */
static setUpClass() {
// Empty.
}
/**
* Register a function with arguments to run after all tests in the class
* have ran.
* @param {function} func - Function to call.
* @param {...*} rest - Arbitrary arguments to func.
*/
static addClassCleanup(func, ...rest) {
this.classCleanups.push([func, rest]);
}
/** Execute all functions registered with addClassCleanup. */
static doClassCleanups() {
while (this.classCleanups.length) {
const [func, rest] = this.classCleanups.pop();
func.call(this, ...rest);
}
}
/** @type {string} */
get id() {
const methodName = this.#methodName;
return `${this.constructor.name}.${methodName}`;
}
/**
* Execute the test method registered upon instantiation.
* @param {TestResult} [result] - Instance for accumulating results.
* Typically, a test runner will pass in one of these to gather results
* across multiple tests.
* @returns {TestResult} - Accumulated results (one is created if not
* passed in).
*/
run(result) {
const localResult = result ?? new TestResult();
const klass = this.constructor.name;
localResult.startTest(this.id);
let stage = null;
try {
stage = `${klass}.setUp`;
this.setUp();
stage = this.id;
this[this.#methodName]();
stage = `${klass}.doCleanups`;
this.doCleanups();
localResult.addSuccess(this.id);
} catch (e) {
const inCleanup = stage.includes('.doCleanups');
if (e instanceof TestCase.Skip && !inCleanup) {
localResult.addSkip(stage, e.message);
} else if (e instanceof TestCase.Fail && !inCleanup) {
localResult.addFailure(stage, e.message);
} else {
localResult.addError(stage, e);
}
}
localResult.stopTest(this.id);
return localResult;
}
/** Called once before each test method. */
setUp() { // eslint-disable-line class-methods-use-this
// Empty.
}
/**
* Register a function with arguments to run after a test.
* @param {function} func - Function to call.
* @param {...*} rest - Arbitrary arguments to func.
*/
addCleanup(func, ...rest) {
this.#cleanups.push([func, rest]);
}
/** Execute all functions registered with addCleanup. */
doCleanups() {
while (this.#cleanups.length) {
const [func, rest] = this.#cleanups.pop();
func.call(this, ...rest);
}
}
/**
* Immediately skips a test method.
* @param {string} [msg=''] - Reason for skipping.
* @throws {TestCase.Skip}
*/
skip(msg = '') {
throw new this.constructor.Skip(msg);
}
/**
* Immediately fail a test method.
* @param {string} [msg=''] - Reason for the failure.
* @throws {TestCase.Fail}
*/
fail(msg = '') {
throw new this.constructor.Fail(msg);
}
/**
* Asserts that two arguments are equal.
* @param {*} first - First argument.
* @param {*} second - Second argument.
* @param {string} [msg=''] - Text to complement the failure message.
*/
assertEqual(first, second, msg = '') {
this.#countAsserts(msg);
this.#assertBase(first, second, true, msg);
}
/**
* Asserts that two arguments are NOT equal.
* @param {*} first - First argument.
* @param {*} second - Second argument.
* @param {string} [msg=''] - Text to complement the failure message.
*/
assertNotEqual(first, second, msg = '') {
this.#countAsserts(msg);
this.#assertBase(first, second, false, msg);
}
/**
* Asserts that the argument is a boolean true.
* @param {*} arg - Argument to test.
* @param {string} [msg=''] - Text to complement the failure message.
*/
assertTrue(arg, msg = '') {
this.#countAsserts(msg);
if (!arg) {
const failMsg = `${arg} is not true`;
this.#failMsgs(failMsg, msg);
}
}
/**
* Asserts that the argument is a boolean false.
* @param {*} arg - Argument to test.
* @param {string} [msg=''] - Text to complement the failure message.
*/
assertFalse(arg, msg = '') {
this.#countAsserts(msg);
if (arg) {
const s1 = this.repr(arg);
const failMsg = `${s1} is not false`;
this.#failMsgs(failMsg, msg);
}
}
/**
* Asserts the expected exception is raised.
* @param {function(): Error} exc - Expected Error class.
* @param {function} func - Function to call.
* @param {string} [msg=''] - Text to complement the failure message.
*/
assertRaises(exc, func, msg = '') {
this.assertRaisesRegExp(exc, /.*/u, func, msg);
}
/**
* Asserts that no exception is raised.
*
* Useful for supplying descriptive text when verifying an error does not
* occur.
* @param {function} func - Function to call.
* @param {string} [msg=''] - Text to complement the failure message.
*/
assertNoRaises(func, msg = '') {
this.#countAsserts(msg);
try {
func();
} catch (e) {
const failMsg = `Unexpected exception: ${e.name}: ${e.message}`;
this.#failMsgs(failMsg, msg);
}
}
/**
* Asserts the expected exception is raised and the message matches the
* regular expression.
* @param {function(): Error} exc - Expected Error class.
* @param {RegExp} regexp - Regular expression to match.
* @param {function} func - Function to call.
* @param {string} [msg=''] - Text to complement the failure message.
*/
assertRaisesRegExp(exc, regexp, func, msg = '') { // eslint-disable-line max-params
this.#countAsserts(msg);
let failMsg = `Expected ${exc.name}, caught nothing`;
try {
func();
} catch (e) {
if (e instanceof exc) {
if (regexp.test(e.message)) {
return;
}
failMsg = `Exception message:\n"${e.message}"\ndid not match ` +
`regular expression:\n"${regexp}"`;
} else {
failMsg = `Expected ${exc.name}, caught ${e.name}`;
}
}
this.#failMsgs(failMsg, msg);
}
/**
* Asserts the target matches the regular expression.
* @param {string} target - Target string to check.
* @param {RegExp} regexp - Regular expression to match.
* @param {string} [msg=''] - Text to complement the failure message.
*/
assertRegExp(target, regexp, msg = '') {
this.#countAsserts(msg);
if (!regexp.test(target)) {
const failMsg = `Target "${target}" did not match ` +
`regular expression "${regexp}"`;
this.#failMsgs(failMsg, msg);
}
}
// TODO: Add assertions as needed.
/**
* Returns a string representation of the item using the registration
* system.
*
* @param {*} item - Anything.
* @returns {string} - String version of item.
*/
repr(item) {
const reprFunc = this.getReprFunc(item);
return reprFunc(item);
}
/**
* @callback ReprFunc
* @param {*} item - Anything.
* @returns {string} - String version of item.
*/
/**
* Find a ReprFunc for the given item.
* @param {*} item - Item of interest.
* @returns {ReprFunc} - Function for this item.
*/
getReprFunc(item) {
const type = getType(item);
return this.#reprFuncs.get(type) ?? this.defaultRepr;
}
/**
* @param {string} type - Type of interest.
* @param {ReprFunc} func - Function for this type.
*/
addReprFunc(type, func) {
this.#reprFuncs.set(type, func);
}
/**
* @implements {ReprFunc}
* @param {string} item - String to wrap.
* @returns {string} - Wrapped version of item.
*/
reprString = (item) => {
const str = `"${item}"`;
return str;
}
/**
* @implements {ReprFunc}
* @param {[*]} array - Array of anything.
* @returns {string} - String version of item.
*/
reprArray = (array) => {
const items = array.map(this.repr.bind(this));
return `[${items.join(', ')}]`;
}
/**
* @implements {ReprFunc}
* @param {object} obj - Any object.
* @returns {string} - String version of obj.
*/
reprObject = (obj) => {
const items = [];
for (const [key, value] of Object.entries(obj)) {
const strKey = this.repr(key);
const strValue = this.repr(value);
items.push(`${strKey}: ${strValue}`);
}
return `{${items.join(', ')}}`;
}
/**
* @implements {ReprFunc}
* @param {Map<*,*>} map - Any Map.
* @returns {string} - String version of map.
*/
reprMap = (map) => {
const items = [];
for (const [key, value] of map.entries()) {
const strKey = this.repr(key);
const strValue = this.repr(value);
items.push(`[${strKey}, ${strValue}]`);
}
return `Map([${items.join(', ')}])`;
}
/**
* @implements {ReprFunc}
* @param {Set<*>} set - Any Set.
* @returns {string} - String version of set.
*/
reprSet = (set) => {
const items = [];
for (const value of set.values()) {
const strValue = this.repr(value);
items.push(`${strValue}`);
}
return `Set([${items.join(', ')}])`;
}
/**
* @typedef {object} EqualOutput
* @property {boolean} equal - Result of equality test.
* @property {string} detail - Details appropriate to the test (e.g.,
* where items differed).
*/
/**
* @callback EqualFunc
* @param {*} first - First argument.
* @param {*} second - Second argument.
* @returns {EqualOutput} - Results of testing equality.
*/
/**
* Find an equality function appropriate for the arguments.
* @param {*} first - First argument.
* @param {*} second - Second argument.
* @returns {EqualFunc} - Function that should be used to test equality.
*/
getEqualFunc(first, second) {
let equal = this.defaultEqual;
const t1 = getType(first);
const t2 = getType(second);
if (t1 === t2) {
equal = this.#equalFuncs.get(t1) ?? equal;
}
return equal;
}
/**
* @param {string} type - As returned from {@link getType}.
* @param {EqualFunc} func - Function to call to compare that type.
*/
addEqualFunc(type, func) {
this.#equalFuncs.set(type, func);
}
/**
* @implements {EqualFunc}
* @param {*} first - First argument.
* @param {*} second - Second argument.
* @returns {EqualOutput} - Results of testing equality.
*/
equalEqEqEq = (first, second) => {
const equal = first === second;
return {
equal: equal,
details: '',
};
}
/**
* For those cases when '===' is too strict.
* @implements {EqualFunc}
* @param {*} first - First argument.
* @param {*} second - Second argument.
* @returns {EqualOutput} - Results of testing equality.
*/
equalValueOf = (first, second) => {
const val1 = first?.valueOf() ?? first;
const val2 = second?.valueOf() ?? second;
const equal = val1 === val2;
return {
equal: equal,
details: 'Using valueOf()',
};
}
/**
* @implements {EqualFunc}
* @param {*} first - First argument.
* @param {*} second - Second argument.
* @returns {EqualOutput} - Results of testing equality.
*/
equalString = (first, second) => {
let details = '';
const equal = first === second;
if (!equal) {
let indicator = '';
const len = Math.min(first.length, second.length);
for (let idx = 0; idx < len; idx += 1) {
const c1 = first.at(idx);
const c2 = second.at(idx);
if (c1 === c2) {
indicator += ' ';
} else {
break;
}
}
indicator += '|';
details = `\n 1: ${first}\ndiff: ${indicator}\n 2: ${second}\n`;
}
return {
equal: equal,
details: details,
};
}
/**
* This currently only tests Object.entries().
*
* Order is ignored.
*
* Other tests, like frozen and sealed states may be implemented later.
* @implements {EqualFunc}
* @param {object} first - First argument.
* @param {object} second - Second argument.
* @returns {EqualOutput} - Results of testing equality.
*/
equalObject = (first, second) => {
const m1 = new Map(Object.entries(first));
const m2 = new Map(Object.entries(second));
return this.equalMap(m1, m2);
}
/**
* @implements {EqualFunc}
* @param {[*]} first - First argument.
* @param {[*]} second - Second argument.
* @returns {EqualOutput} - Results of testing equality.
*/
equalArray = (first, second) => {
let equal = true;
const details = [];
const len = Math.min(first.length, second.length);
for (let idx = 0; idx < len; idx += 1) {
const i1 = first.at(idx);
const i2 = second.at(idx);
const equalFunc = this.getEqualFunc(i1, i2);
const result = equalFunc(i1, i2);
if (!result.equal) {
equal = false;
details.push(
'',
`First difference at element ${idx}:`,
this.repr(i1),
this.repr(i2)
);
break;
}
}
if (first.length !== second.length) {
equal = false;
const diff = Math.abs(first.length - second.length);
const longest = first.length > second.length ? 'First' : 'Second';
details.push(
'',
`${longest} array contains ${diff} more elements.`,
`First additional element is at position ${len}:`,
this.repr(first.at(len) ?? second.at(len))
);
}
return {
equal: equal,
details: details.join('\n'),
};
}
/**
* Order is ignored.
*
* @implements {EqualFunc}
* @param {Map<*,*>} first - First argument.
* @param {Map<*,*>} second - Second argument.
* @returns {EqualOutput} - Results of testing equality.
*/
equalMap = (first, second) => {
const m1 = this.#normalizeContainer(first);
const m2 = this.#normalizeContainer(second);
let equal = true;
const differences = [];
const missingFirst = [];
const missingSecond = [];
for (const [key, val1] of m1.entries()) {
if (m2.has(key)) {
const val2 = m2.get(key);
if (val1 !== val2) {
equal = false;
differences.push(
'',
`Difference with key: ${key}`,
`Value in first : ${val1}`,
`Value in second: ${val2}`,
);
}
} else {
equal = false;
missingSecond.push(
'',
`Key missing from second: ${key}`,
`Value in first : ${val1}`,
);
}
}
for (const [key, val2] of m2.entries()) {
if (!m1.has(key)) {
equal = false;
missingFirst.push(
'',
`Key missing from first : ${key}`,
`Value in second: ${val2}`,
);
}
}
const details = [
...differences,
...missingFirst,
...missingSecond,
];
return {
equal: equal,
details: details.join('\n'),
};
}
/**
* Order is ignored.
*
* @implements {EqualFunc}
* @param {Set<*>} first - First argument.
* @param {Set<*>} second - Second argument.
* @returns {EqualOutput} - Results of testing equality.
*/
equalSet = (first, second) => {
const s1 = this.#normalizeContainer(first);
const s2 = this.#normalizeContainer(second);
let equal = true;
const missingFirst = [];
const missingSecond = [];
for (const val of s1.values()) {
if (!s2.has(val)) {
equal = false;
missingSecond.push(
'',
`Value missing from second: ${val}`,
);
}
}
for (const val of s2.values()) {
if (!s1.has(val)) {
equal = false;
missingFirst.push(
'',
`Value missing from first : ${val}`,
);
}
}
const details = [
...missingFirst,
...missingSecond,
];
return {
equal: equal,
details: details.join('\n'),
};
}
#assertsCalled = 0;
#assertsWithNoMsg = 0;
#cleanups = [];
#equalFuncs = new Map();
#methodName
#reprFuncs = new Map();
/**
* Count how many asserts are called per test method.
*
* Each *assertX()* method should call this first this along with its
* optional *msg* parameter.
* @param {string} msg - The message the assert method was given.
*/
#countAsserts = (msg) => {
this.#assertsCalled += 1;
if (!msg) {
this.#assertsWithNoMsg += 1;
}
}
#checkAssertionCounts = () => {
// How many asserts must exist in the test method before caring.
const MIN_ASSERTS = 2;
// How many asserts are allowed to be missing descriptions.
const MAX_MISSING = 1;
if (this.#assertsCalled >= MIN_ASSERTS &&
this.#assertsWithNoMsg > MAX_MISSING) {
// eslint-disable-next-line no-console
console.debug('Too many asserts without descriptions!',
this.id,
this.#assertsWithNoMsg);
if (testing.missingDescriptionsAreErrors) {
this.fail(`Too many asserts without descriptions: ${this.id}`);
}
}
}
/**
* Asserts that two arguments have the expected equality
*
* @param {*} first - First argument.
* @param {*} second - Second argument.
* @param {boolean} expected - Expectation of equality.
* @param {string} [msg=''] - Text to complement the failure message.
*/
#assertBase = (first, second, expected, msg) => { // eslint-disable-line max-params
const equal = this.getEqualFunc(first, second);
const results = equal(first, second);
const passed = results.equal === expected;
if (!passed) {
const badCmp = expected ? '!==' : '===';
const s1 = this.repr(first);
const s2 = this.repr(second);
const failMsg = `${s1} ${badCmp} ${s2}`;
if (!expected) {
results.details = '';
}
this.#failMsgs(failMsg, results.details, msg);
}
}
/**
* Turn all keys and values in a container into a string via *repr()*.
*
* Containers must meet the following criteria:
* + Have an *entries()* method that returns [key, value] pairs.
* + Have at least one of *set(key, value)* or *add(value)* method.
* + Support a constructor taking own type that results in a copy.
*
* @param {Map<*,*>|Set<*>} container - Or any type with similar
* signatures.
* @returns {Map<*,*>|Set<*>} - Clone of container with all keys and
* values transformed into a string.
*/
#normalizeContainer = (container) => {
const clone = new (Object.getPrototypeOf(container)
.constructor)(container);
if (!clone.set) {
clone.set = (k, v) => {
clone.add(v);
};
}
clone.clear();
for (const [k, v] of container.entries()) {
const newK = this.repr(k);
const newV = this.repr(v);
clone.set(newK, newV);
}
return clone;
}
/**
* Immediately fail while combining messages.
* @param {...string} messages - Messages to join.
*/
#failMsgs = (...messages) => {
const filtered = messages
.filter(x => x)
.map(x => String(x))
.join(' : ');
this.fail(filtered);
}
}
/* eslint-disable no-array-constructor */
/* eslint-disable no-new-wrappers */
/* eslint-disable no-undef */
/* eslint-disable no-undefined */
/* eslint-disable require-jsdoc */
class GetTypeTestCase extends TestCase {
testPrimitives() {
this.assertEqual(getType(0), 'Number', 'zero');
this.assertEqual(getType(NaN), 'Number', 'Nan');
this.assertEqual(getType('0'), 'String', '"0"');
this.assertEqual(getType(true), 'Boolean', 'true');
this.assertEqual(getType(false), 'Boolean', 'false');
this.assertEqual(getType(BigInt('123')), 'BigInt', 'BigInt()');
this.assertEqual(getType(456n), 'BigInt', '456n');
this.assertEqual(getType(undefined), 'Undefined', 'undefined');
this.assertEqual(getType(null), 'Null', 'null');
}
testBuiltInFunctionLike() {
this.assertEqual(getType(String('xyzzy')), 'String', 'string-xyzzy');
this.assertEqual(getType(new String('abc')), 'String', 'string-abc');
this.assertEqual(getType(String), 'Function', 'bare String');
this.assertEqual(getType(Symbol('xyzzy')), 'Symbol', 'symbol');
this.assertEqual(getType(Symbol), 'Function', 'bare Symbol');
this.assertEqual(getType(/abc123/u), 'RegExp', '/regexp/');
this.assertEqual(getType(new Date()), 'Date', 'new Date');
this.assertEqual(getType(Date()), 'String', 'Date()');
this.assertEqual(getType(Date), 'class', 'bare Date');
this.assertEqual(getType(Math.min), 'Function', 'math.min');
this.assertEqual(getType(Math), 'Math', 'Math');
}
testBuiltinClasses() {
this.assertEqual(getType({}), 'Object', '{}');
this.assertEqual(getType([]), 'Array', '[]');
this.assertEqual(getType(new Array()), 'Array', 'new array');
this.assertEqual(getType(Array), 'class', 'bare Array');
this.assertEqual(getType(new Map()), 'Map', 'map');
this.assertEqual(getType(Map), 'class', 'bare Map');
this.assertEqual(getType(new Set()), 'Set', 'set');
this.assertEqual(getType(Set), 'class', 'bare Set');
this.assertEqual(getType(new Error()), 'Error', 'error');
this.assertEqual(getType(Error), 'class', 'bare Error');
}
testRegularClasses() {
this.assertEqual(getType(TestCase), 'class', 'bare TestCase');
this.assertEqual(getType(this), 'GetTypeTestCase', 'this');
this.assertEqual(getType(getType), 'Function', 'bare getType');
this.assertEqual(getType(TestCase.Skip), 'class', 'nested class');
}
}
/* eslint-enable */
testing.testCases.push(GetTypeTestCase);
/* eslint-disable class-methods-use-this */
/* eslint-disable no-magic-numbers */
/* eslint-disable require-jsdoc */
/**
* For testing TestCase basic features.
*
* Do not use directly, but rather inside `TestCaseTestCase`.
*/
class BasicFeaturesTestCase extends TestCase {
static classCalls = [];
/** Register cleanup functions.. */
static setUpClassCleanups() {
this.classCalls = [];
this.addClassCleanup(this.one);
this.addClassCleanup(this.two, 3, 4);
}
/** Capture that it was called. */
static one() {
this.classCalls.push('one');
}
/**
* Capture that it was called with arguments.
* @param {*} a - Anything.
* @param {*} b - Anything.
*/
static two(a, b) {
this.classCalls.push('two', a, b);
}
testInstanceCleanups() {
this.instanceCalls = [];
this.addCleanup(this.three);
this.addCleanup(this.four, 5, 6);
}
/** Capture that it was called. */
three() {
this.instanceCalls.push('three');
}
/**
* Capture that it was called with arguments.
* @param {*} a - Anything.
* @param {*} b - Anything.
*/
four(a, b) {
this.instanceCalls.push('four', a, b);
}
testInstanceCleanupsWithError() {
this.addCleanup(this.willError);
}
testInstanceCleanupsWithSkip() {
this.addCleanup(this.willSkip);
}
testInstanceCleanupsWithFail() {
this.addCleanup(this.willFail);
}
willError() {
throw new Error('from willError');
}
willSkip() {
this.skip('from willSkip');
}
willFail() {
this.fail('from willFail');
}
}
/* eslint-enable */
/* eslint-disable max-lines-per-function */
/* eslint-disable max-statements */
/* eslint-disable no-array-constructor */
/* eslint-disable no-empty-function */
/* eslint-disable no-magic-numbers */
/* eslint-disable no-new */
/* eslint-disable no-new-wrappers */
/* eslint-disable no-undef */
/* eslint-disable no-undefined */
/* eslint-disable no-unused-vars */
/* eslint-disable require-jsdoc */
class TestCaseTestCase extends TestCase {
testCannotInstantiateDirectly() {
this.assertRaises(TypeError, () => {
new TestCase();
});
}
testStaticSetUpClassExists() {
this.assertNoRaises(() => {
TestCase.setUpClass();
});
}
testDoClassCleanups() {
// Assemble
BasicFeaturesTestCase.setUpClassCleanups();
// Act
BasicFeaturesTestCase.doClassCleanups();
// Assert
const actual = BasicFeaturesTestCase.classCalls;
const expected = ['two', 3, 4, 'one'];
this.assertEqual(actual, expected);
}
testId() {
// Assemble
const instance = new BasicFeaturesTestCase('testSomething');
// Assert
const actual = instance.id;
const expected = 'BasicFeaturesTestCase.testSomething';
this.assertEqual(actual, expected);
}
testDoInstanceCleanups() {
// Assemble
const method = 'testInstanceCleanups';
const instance = new BasicFeaturesTestCase(method);
// Act
const result = instance.run();
// Assert
this.assertTrue(result.wasSuccessful(), 'success');
// Next assert has timestamps in it.
this.assertEqual(result.tests.size, 1, 'tests collected');
const actual = instance.instanceCalls;
const expected = ['four', 5, 6, 'three'];
this.assertEqual(actual, expected, 'calls');
}
testDoInstanceCleanupsWithError() {
// Assemble
const method = 'testInstanceCleanupsWithError';
const instance = new BasicFeaturesTestCase(method);
// Act
const result = instance.run();
// Assert
this.assertFalse(result.wasSuccessful(), 'success');
// Next assert has timestamps in it.
this.assertEqual(result.tests.size, 1, 'tests collected');
this.assertEqual(
result.errors,
[
{
name: 'BasicFeaturesTestCase.doCleanups',
error: 'Error',
message: 'from willError',
},
],
'errors'
);
}
testDoInstanceCleanupsWithSkip() {
// Assemble
const method = 'testInstanceCleanupsWithSkip';
const instance = new BasicFeaturesTestCase(method);
// Act
const result = instance.run();
// Assert
this.assertFalse(result.wasSuccessful(), 'success');
// Next assert has timestamps in it.
this.assertEqual(result.tests.size, 1, 'tests collected');
this.assertEqual(
result.errors,
[
{
name: 'BasicFeaturesTestCase.doCleanups',
error: 'TestCase.Skip',
message: 'from willSkip',
},
],
'errors'
);
}
testDoInstanceCleanupsWithFail() {
// Assemble
const method = 'testInstanceCleanupsWithFail';
const instance = new BasicFeaturesTestCase(method);
// Act
const result = instance.run();
// Assert
this.assertFalse(result.wasSuccessful(), 'success');
// Next assert has timestamps in it.
this.assertEqual(result.tests.size, 1, 'tests collected');
this.assertEqual(
result.errors,
[
{
name: 'BasicFeaturesTestCase.doCleanups',
error: 'TestCase.Fail',
message: 'from willFail',
},
],
'errors'
);
}
testCollectTests() {
// Assemble
const result = new TestResult();
const methods = [
'testInstanceCleanups',
'testInstanceCleanupsWithError',
'testInstanceCleanupsWithSkip',
'testInstanceCleanupsWithFail',
];
// Act
for (const method of methods) {
const instance = new BasicFeaturesTestCase(method);
instance.run(result);
}
// Assert
// Next assert has timestamps in it.
this.assertEqual(result.tests.size, 4);
}
testSkip() {
// Act/Assert
this.assertRaisesRegExp(
TestCase.Skip,
/^$/u,
() => {
this.skip();
},
'basic skip'
);
// Act/Assert
this.assertRaisesRegExp(
TestCase.Skip,
/a message/u,
() => {
this.skip('a message');
},
'with a message'
);
}
testFail() {
// Act/Assert
this.assertRaisesRegExp(
TestCase.Fail,
/^$/u,
() => {
this.fail();
},
'no description'
);
// Act/Assert
this.assertRaisesRegExp(
TestCase.Fail,
/for the masses/u,
() => {
this.fail('for the masses');
},
'with description'
);
}
testGetReprFunc() {
this.assertEqual(this.getReprFunc(null), String, 'null');
this.assertEqual(this.getReprFunc(undefined), String, 'undefined');
this.assertEqual(this.getReprFunc(1), String, 'number');
this.assertEqual(this.getReprFunc(''), this.reprString, 'string');
this.assertEqual(this.getReprFunc([]), this.reprArray, 'array');
this.assertEqual(this.getReprFunc({}), this.reprObject, 'object');
this.assertEqual(this.getReprFunc(new Map()), this.reprMap, 'map');
this.assertEqual(this.getReprFunc(new Set()), this.reprSet, 'set');
}
testChangingDefaultRepr() {
// Assemble
function x() {}
// Act
this.defaultRepr = x;
// Assert
this.assertEqual(this.getReprFunc(null), x, 'null');
this.assertEqual(this.getReprFunc(undefined), x, 'undefined');
this.assertEqual(this.getReprFunc(1), x, 'number');
this.assertEqual(this.getReprFunc(''), this.reprString, 'string');
this.assertEqual(this.getReprFunc([]), this.reprArray, 'array');
this.assertEqual(this.getReprFunc({}), this.reprObject, 'object');
this.assertEqual(this.getReprFunc(new Map()), this.reprMap, 'map');
this.assertEqual(this.getReprFunc(new Set()), this.reprSet, 'set');
}
testAddReprFunc() {
// Assemble
class C {}
const c = new C();
function reprC(item) {}
this.assertNotEqual(this.getReprFunc(c), reprC, 'no reprC');
// Act
this.addReprFunc('C', reprC);
// Assert
this.assertEqual(this.getReprFunc(c), reprC, 'found reprC');
this.assertEqual(this.getReprFunc(null), String, 'null');
this.assertEqual(this.getReprFunc(undefined), String, 'undefined');
this.assertEqual(this.getReprFunc(1), String, 'number');
this.assertEqual(this.getReprFunc(''), this.reprString, 'string');
this.assertEqual(
this.getReprFunc(new String('str')), this.reprString, 'new string'
);
this.assertEqual(this.getReprFunc([]), this.reprArray, 'array');
this.assertEqual(
this.getReprFunc(new Array(1, 2, 3)), this.reprArray, 'new array'
);
this.assertEqual(this.getReprFunc({}), this.reprObject, 'object');
this.assertEqual(this.getReprFunc(new Map()), this.reprMap, 'map');
this.assertEqual(this.getReprFunc(new Set()), this.reprSet, 'set');
}
testReprPrimitives() {
this.assertEqual(this.repr(1), '1', 'number');
this.assertEqual(this.repr(null), 'null', 'null');
this.assertEqual(this.repr(undefined), 'undefined', 'undefined');
this.assertEqual(
this.repr(Symbol('qwerty')),
'Symbol(qwerty)',
'symbol'
);
}
testReprString() {
this.assertEqual(this.repr('a. b'), '"a. b"', 'string');
this.assertEqual(this.repr(new String('xyz')), '"xyz"', 'new string');
}
testReprArray() {
this.assertEqual(this.repr(['b', 2]), '["b", 2]', 'mixed array');
this.assertEqual(
this.repr(['b', [1, '2']]),
'["b", [1, "2"]]',
'nested array'
);
this.assertEqual(
this.repr(new Array(1, '2', 'three')),
'[1, "2", "three"]',
'new array'
);
}
testReprObject() {
this.assertEqual(this.repr({a: '1'}), '{"a": "1"}', 'simple');
this.assertEqual(
this.repr({b: {c: 'd', e: 1}}),
'{"b": {"c": "d", "e": 1}}',
'nested'
);
}
testReprMap() {
this.assertEqual(this.repr(new Map()), 'Map([])', 'empty');
this.assertEqual(
this.repr(new Map([])),
'Map([])',
'empty init'
);
this.assertEqual(
this.repr(new Map([[1, 'one'], ['two', 2]])),
'Map([[1, "one"], ["two", 2]])',
'with items'
);
this.assertEqual(
this.repr(new Map([[1, 'one'], ['map', new Map([['x', 3]])]])),
'Map([[1, "one"], ["map", Map([["x", 3]])]])',
'nested'
);
}
testReprSet() {
this.assertEqual(this.repr(new Set()), 'Set([])', 'empty');
this.assertEqual(
this.repr(new Set([])),
'Set([])',
'empty init'
);
this.assertEqual(
this.repr(new Set([1, 'b', 'b', 'xyz', 99])),
'Set([1, "b", "xyz", 99])',
'with items'
);
this.assertEqual(
this.repr(new Set([1, new Set(['x', 3]), 'qqq'])),
'Set([1, Set(["x", 3]), "qqq"])',
'nested'
);
}
testGetEqualFunc() {
this.assertEqual(
this.getEqualFunc({}, []),
this.equalEqEqEq,
'obj vs array'
);
this.assertEqual(
this.getEqualFunc('a', 'b'),
this.equalString,
'str vs str'
);
this.assertEqual(
this.getEqualFunc('a', new String('b')),
this.equalString,
'str vs new str'
);
}
testChangingDefaultEqual() {
// Assemble
this.assertEqual(this.getEqualFunc({}, []), this.equalEqEqEq, '===');
this.defaultEqual = this.equalValueOf;
// Act/Assert
this.assertEqual(
this.getEqualFunc({}, []),
this.equalValueOf,
'valueOf'
);
}
testAddEqualFunc() {
// Assemble
class C {}
const c = new C();
function equalC(first, second) {}
this.assertNotEqual(this.getEqualFunc(c, c), equalC, 'not equalC');
// Act
this.addEqualFunc(getType(c), equalC);
// Assert
this.assertEqual(this.getEqualFunc(c, c), equalC, 'found equalC');
}
testAssertEqualPrimitives() {
this.assertEqual(0, 0, '0 vs 0');
this.assertEqual(42, 42, 'number vs number');
this.assertEqual(true, true, 'true vs true');
this.assertEqual(false, false, 'false vs false');
this.assertEqual(
BigInt('123456789'),
BigInt('123456789'),
'bigint vs bigint'
);
this.assertEqual(undefined, {}.undef, 'undefined vs undef');
this.assertEqual(null, null, 'null vs null');
const bar = Symbol('bar');
this.assertEqual(bar, bar, 'same symbol');
// Equivalent Symbols cannot be equal.
this.assertRaisesRegExp(
TestCase.Fail,
/^Symbol.foo. !== Symbol.foo.$/u,
() => {
this.assertEqual(Symbol('foo'), Symbol('foo'));
},
'different, but equiv symbols'
);
}
testAssertEqualValueOf() {
// Assemble
class Silly extends Number {}
const n = new Number(3);
const s = new Silly(3);
this.assertNotEqual(n, s, 'before');
// Act
this.defaultEqual = this.equalValueOf;
// Assert
this.assertEqual(n, s, 'after');
}
testAssertEqualPrimitivesWithValueOf() {
this.defaultEqual = this.equalValueOf;
this.assertEqual(0, 0, '0 vs 0');
this.assertEqual(42, 42, 'number vs number');
this.assertEqual(true, true, 'true vs true');
this.assertEqual(false, false, 'false vs false');
this.assertEqual(
BigInt('123456789'),
BigInt('123456789'),
'bigint vs bigint'
);
this.assertEqual(undefined, {}.undef, 'undefined vs undef');
this.assertEqual(null, null, 'null vs null');
const bar = Symbol('bar');
this.assertEqual(bar, bar, 'same symbol');
// Equivalent Symbols cannot be equal, even with valueOf().
this.assertRaisesRegExp(
TestCase.Fail,
/^Symbol.foo. !== Symbol.foo. : Using valueOf/u,
() => {
this.assertEqual(Symbol('foo'), Symbol('foo'));
},
'different, but equiv symbols'
);
}
testAssertEqualFailureMessages() {
this.assertRaisesRegExp(
TestCase.Fail,
/^\{\} !== \[\] :/u,
() => {
this.assertEqual({}, [], 'assert under test');
},
'obj vs array'
);
this.assertRaisesRegExp(
TestCase.Fail,
/^undefined !== null :/u,
() => {
this.assertEqual(undefined, null, 'assert under test');
},
'undefined vs null'
);
this.assertRaisesRegExp(
TestCase.Fail,
/^0 !== "0" :/u,
() => {
this.assertEqual(0, '0', 'assert under test');
},
'number vs string of same'
);
}
// Old version of eslint does not know BigInt.
/* eslint-disable no-undef */
testAssertNotEqualPrimitives() {
this.assertNotEqual(NaN, NaN, 'NaN');
this.assertNotEqual(true, false, 'true/false');
this.assertNotEqual(false, true, 'false/true');
this.assertNotEqual(BigInt('12345678'), BigInt('123456789'), 'BigInt');
this.assertNotEqual(undefined, null, 'undef/null');
this.assertNotEqual(Symbol('foo'), Symbol('foo'), 'symbols');
}
testAssertNotEqualFailureMessages() {
this.assertRaisesRegExp(TestCase.Fail,
/^0 === 0 : assert under test$/u,
() => {
this.assertNotEqual(0, 0, 'assert under test');
}, '0 vs 0');
this.assertRaisesRegExp(TestCase.Fail,
/^undefined === undefined :/u,
() => {
this.assertNotEqual(undefined, undefined, 'assert under test');
}, 'undefined vs undefined');
this.assertRaisesRegExp(TestCase.Fail,
/^null === null :/u,
() => {
this.assertNotEqual(null, null, 'assert under test');
}, 'null vs null');
this.assertRaisesRegExp(TestCase.Fail,
/^Symbol\(sym\) === Symbol\(sym\) :/u,
() => {
const sym = Symbol('sym');
this.assertNotEqual(sym, sym, 'assert under test');
}, 'symbol vs self');
this.assertRaisesRegExp(
TestCase.Fail,
/"a" === "a" :/u,
() => {
this.assertNotEqual('a', 'a', 'assert under test');
},
'str vs str'
);
}
testEqualString() {
let expected = '';
this.assertEqual(this.getEqualFunc('a', 'b'),
this.equalString,
'equalFunc');
this.assertEqual('string', 'string', 'str === str');
this.assertNotEqual('string 1', 'string 2', 'str !== str');
expected = `"abc1234" !== "abc123" :[ ]
1: abc1234
diff: |
2: abc123`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual('abc1234', 'abc123', 'assert under test');
},
'first longer',
);
expected = `"abcd" !== "abxd" :[ ]
1: abcd
diff: |
2: abxd`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual('abcd', 'abxd', 'assert under test');
},
'diff in middle'
);
expected += '\n : extra';
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual('abcd', 'abxd', 'extra');
},
'diff in middle with description'
);
}
testEqualObject() {
let o1 = {};
let o2 = {};
let expected = '';
const sym = Symbol('xyzzy');
this.assertEqual(this.getEqualFunc({}, {a: 1}),
this.equalObject,
'equalFunc');
o1 = {};
o2 = {};
this.assertEqual(o1, o2, 'empty');
o1 = {1: 'a', b: 2};
o2 = {b: 2, 1: 'a'};
this.assertEqual(o1, o2, 'different order');
o1 = {1: 1, 2: {a: 42}};
o2 = {1: 1, 2: {a: 42}};
this.assertEqual(o1, o2, 'nested');
o1 = {sym: 'foo'};
o2 = {sym: 'foo'};
this.assertEqual(o1, o2, 'symbol');
o1 = {1: 'a', b: 2};
o2 = {b: 2};
this.assertNotEqual(o1, o2, 'first has more');
this.assertNotEqual(o2, o1, 'second has more');
o1 = {1: 'a'};
o2 = {1: 'b'};
this.assertNotEqual(o1, o2, 'different values');
o1 = {21: 'b'};
o2 = {21: 'a'};
expected = `\\{"21": "b"\\} !== \\{"21": "a"\\} :[ ]
Difference with key: "21"
Value in first : "b"
Value in second: "a"`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual(o1, o2, 'assert under test');
},
'same key, different values'
);
o1 = {1: 'a', 3: 'c', q: 86, null: 'abc', sym: 'x'};
o2 = {1: 'a', 2: 'b', q: 99, null: 54321, sym: 'y'};
expected = ` :[ ]
Difference with key: "q"
Value in first : 86
Value in second: 99
Difference with key: "null"
Value in first : "abc"
Value in second: 54321
Difference with key: "sym"
Value in first : "x"
Value in second: "y"
Key missing from first : "2"
Value in second: "b"
Key missing from second: "3"
Value in first : "c"`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual(o1, o2, 'assert under test');
},
'bit of everything',
);
o1 = {1: 1, 2: {a: 42}};
o2 = {1: 1, 2: {a: 43}};
expected = ` :[ ]
Difference with key: "2"
Value in first : \\{"a": 42\\}
Value in second: \\{"a": 43\\}`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual(o1, o2, 'assert under test');
},
'nested, different',
);
}
testEqualArray() {
let expected = '';
this.assertEqual(this.getEqualFunc([1], [2, 3]),
this.equalArray,
'equalFunc');
this.assertEqual([], [], 'empty');
this.assertEqual([1, 'a'], [1, 'a'], 'mixed');
this.assertEqual([1, [2, 3]], [1, [2, 3]], 'nested');
this.assertNotEqual([0], [1], 'simple notequal');
this.assertNotEqual([], [1], 'different lengths');
expected = ` !== .* :[ ]
First difference at element 1:
1
2`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual([0, 1], [0, 2], 'assert under test');
},
'simple unequal'
);
expected = ` :[ ]
First array contains 1 more elements.
First additional element is at position 1:
"xyzzy"`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual([3, 'xyzzy'], [3], 'assert under test');
},
'first longer'
);
expected = ` :[ ]
Second array contains 1 more elements.
First additional element is at position 2:
"asdf"`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual([1, 2], [1, 2, 'asdf'], 'assert under test');
},
'second longer'
);
expected = ` :[ ]
First difference at element 1:
3
2
Second array contains 1 more elements.
First additional element is at position 2:
"asdf"`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual([1, 3], [1, 2, 'asdf'], 'assert under test');
},
'different element and sizes'
);
expected = ` :[ ]
First difference at element 2:
\\[1, 2\\]
\\[1, 3\\]`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual(
[-1, 0, [1, 2]], [-1, 0, [1, 3]], 'assert under test'
);
},
'nested unequal'
);
}
testEqualMap() {
let m1 = new Map();
let m2 = new Map();
let expected = '';
this.assertEqual(this.getEqualFunc(m1, m2), this.equalMap, 'equalFunc');
this.assertEqual(m1, m2, 'empty');
m1 = new Map([[1, 'a'], ['b', 2]]);
m2 = new Map([['b', 2], [1, 'a']]);
this.assertEqual(m1, m2, 'different order');
m1 = new Map([[1, 'a'], [2, new Map([['a', 42]])]]);
m2 = new Map([[1, 'a'], [2, new Map([['a', 42]])]]);
this.assertEqual(m1, m2, 'nested');
m1 = new Map([[1, 'a'], ['b', 2]]);
m2 = new Map([[1, 'a']]);
this.assertNotEqual(m1, m2, 'first has more');
this.assertNotEqual(m2, m1, 'second has more');
m1 = new Map([[1, 'b']]);
m2 = new Map([[1, 'a']]);
this.assertNotEqual(m1, m2, 'different values');
m1 = new Map([[19, 'a']]);
m2 = new Map([[19, 'b']]);
expected = ` !== .* :[ ]
Difference with key: 19
Value in first : "a"
Value in second: "b"`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual(m1, m2, 'assert under test');
},
'same key, different values'
);
m1 = new Map([[1, 'a']]);
m2 = new Map([[1, 'a'], ['b', 42]]);
expected = ` :[ ]
Key missing from first : "b"
Value in second: 42`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual(m1, m2, 'assert under test');
},
'second has extra key'
);
m1 = new Map([[1, 'a'], ['b', 42]]);
m2 = new Map([[1, 'a']]);
expected = ` :[ ]
Key missing from second: "b"
Value in first : 42`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual(m1, m2, 'assert under test');
},
'first has extra key'
);
m1 = new Map([[1, 'a'], [3, 'c'], ['q', 86], [null, 'abc'], [{}, 'x']]);
m2 = new Map([[1, 'a'], [2, 'b'], ['q', 99], [null, 54321], [{}, 'y']]);
expected = ` !== .* :[ ]
Difference with key: "q"
Value in first : 86
Value in second: 99
Difference with key: null
Value in first : "abc"
Value in second: 54321
Difference with key: \\{\\}
Value in first : "x"
Value in second: "y"
Key missing from first : 2
Value in second: "b"
Key missing from second: 3
Value in first : "c"`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual(m1, m2, 'assert under test');
},
'bit of everything'
);
expected = ` :[ ]
Difference with key: 2
Value in first : Map\\(\\[\\["a", 42\\]\\]\\)
Value in second: Map\\(\\[\\["a", 43\\]\\]\\)`;
m1 = new Map([[1, 'a'], [2, new Map([['a', 42]])]]);
m2 = new Map([[1, 'a'], [2, new Map([['a', 43]])]]);
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual(m1, m2, 'assert under test');
},
'nested different'
);
}
testEqualSet() {
let s1 = new Set();
let s2 = new Set();
let expected = '';
this.assertEqual(this.getEqualFunc(s1, s2), this.equalSet, 'equalFunc');
this.assertEqual(s1, s2, 'empty');
s1 = new Set([1, 'a']);
s2 = new Set(['a', 1]);
this.assertEqual(s1, s2, 'different order');
s1 = new Set([1, new Set(['a', 42])]);
s2 = new Set([1, new Set(['a', 42])]);
this.assertEqual(s1, s2, 'nested');
s1 = new Set([1, 'a', 'xyz']);
s2 = new Set([1, 'a']);
this.assertNotEqual(s1, s2, 'first has more');
this.assertNotEqual(s2, s1, 'second has more');
s1 = new Set([1]);
s2 = new Set([2]);
this.assertNotEqual(s1, s2, 'different values');
s1 = new Set([1, 2]);
s2 = new Set([2, 'three']);
expected = `Set\\(\\[1, 2\\]\\) !== Set\\(\\[2, "three"\\]\\) :[ ]
Value missing from first : "three"
Value missing from second: 1`;
this.assertRaisesRegExp(
TestCase.Fail,
RegExp(expected, 'u'),
() => {
this.assertEqual(s1, s2);
},
'bit of everything'
);
}
testAssertTrue() {
this.assertTrue(true, 'boolean');
this.assertTrue(1, 'one');
this.assertTrue(' ', 'single space');
this.assertTrue({}, 'empty object');
this.assertTrue([], 'empty array');
this.assertTrue(Symbol('true'), 'symbol');
this.assertRaisesRegExp(
TestCase.Fail,
/false is not true/u,
() => {
this.assertTrue(false, 'assert under test');
}, 'testing false'
);
this.assertRaisesRegExp(
TestCase.Fail,
/0 is not true/u,
() => {
this.assertTrue(0, 'assert under test');
}, 'testing zero'
);
this.assertRaisesRegExp(
TestCase.Fail,
/^0 is not true : xyzzy$/u,
() => {
this.assertTrue(0, 'xyzzy');
},
'testing with description as string'
);
this.assertRaisesRegExp(
TestCase.Fail,
/^undefined is not true : Symbol\(xyzzy\)$/u,
() => {
this.assertTrue(undefined, Symbol('xyzzy'));
},
'testing with description as symbol'
);
this.assertRaisesRegExp(
TestCase.Fail,
/^null is not true$/u,
() => {
this.assertTrue(null, false);
},
'testing with description as boolean'
);
}
testAssertFalse() {
this.assertFalse(false, 'boolean');
this.assertFalse(0, 'zero');
this.assertFalse('', 'empty string');
this.assertRaisesRegExp(
TestCase.Fail,
/true is not false/u,
() => {
this.assertFalse(true, 'assert under test');
},
'testing true'
);
this.assertRaisesRegExp(
TestCase.Fail,
/-1 is not false/u,
() => {
this.assertFalse(-1, 'assert under test');
},
'testing -1'
);
this.assertRaisesRegExp(
TestCase.Fail,
/\{\} is not false/u,
() => {
this.assertFalse({}, 'assert under test');
},
'testing Boolean({})'
);
this.assertRaisesRegExp(
TestCase.Fail,
/^\[\] is not false : abc123$/u,
() => {
this.assertFalse([], 'abc123');
},
'testing array'
);
this.assertRaisesRegExp(
TestCase.Fail,
/Symbol\(bar\) is not false/u,
() => {
this.assertFalse(Symbol('bar'), 'assert under test');
},
'testing symbol'
);
}
testAssertRaises() {
this.assertRaises(
Error,
() => {
throw new Error();
},
'empty Error'
);
this.assertRaises(
Error,
() => {
throw new Error('with a message');
},
'Error with message'
);
this.assertRaisesRegExp(
TestCase.Fail,
/caught nothing/u,
() => {
this.assertRaises(Error, () => {}, 'assert under test');
},
'caught nothing'
);
this.assertRaisesRegExp(
TestCase.Fail,
/TypeError.* Error/u,
() => {
this.assertRaises(
TypeError,
() => {
throw new Error();
},
'assert under test'
);
},
'regexp, empty Error, no description'
);
this.assertRaisesRegExp(
TestCase.Fail,
/ : hovercraft/u,
() => {
this.assertRaises(TypeError,
() => {
throw new Error();
},
'hovercraft full of eels');
},
'regexp, empty error, with eels'
);
}
testAssertNoRaises() {
this.assertNoRaises(() => {
});
this.assertRaisesRegExp(
TestCase.Fail,
/^Unexpected exception:.*threw an error/u,
() => {
this.assertNoRaises(() => {
throw new Error('This function threw an error');
}, 'assert under test');
},
'basic error'
);
this.assertRaisesRegExp(
TestCase.Fail,
/^Unexpected exception:.*: custom text/u,
() => {
this.assertNoRaises(() => {
throw new Error('Bad function. No cookie.');
}, 'custom text');
},
'with descriptive text'
);
}
testAssertRaisesRegExp() {
this.assertRaisesRegExp(
Error,
/xyzzy/u,
() => {
throw new Error('xyzzy');
},
'match text in exception'
);
this.assertRaisesRegExp(
TestCase.Fail,
/caught nothing/u,
() => {
this.assertRaisesRegExp(
Error,
/.*/u,
() => {},
'assert under test'
);
},
'no error raise, caught nothing'
);
this.assertRaisesRegExp(
TestCase.Fail,
/ : my message/u,
() => {
this.assertRaisesRegExp(
Error,
/.*/u,
() => {},
'my message'
);
},
'matched description'
);
this.assertRaisesRegExp(
TestCase.Fail,
/Expected TypeError/u,
() => {
this.assertRaisesRegExp(
TypeError,
/message/u,
() => {
throw new Error('message');
},
'assert under test'
);
},
'wrong exception thrown'
);
this.assertRaisesRegExp(
TestCase.Fail,
/did not match regular expression/u,
() => {
this.assertRaisesRegExp(
Error,
/message/u,
() => {
throw new Error('xyzzy');
},
'assert under test'
);
},
'wrong regexp'
);
}
testAssertRegExp() {
this.assertRegExp('abc', /ab./u, 'basic match');
this.assertRaisesRegExp(
TestCase.Fail,
/Target.*did not match regular expression/u,
() => {
this.assertRegExp('abc', /ab.d/u, 'assert under test');
},
'should not match'
);
this.assertRaisesRegExp(
TestCase.Fail,
/ : what do you expect/u,
() => {
this.assertRegExp('abc', /xyz/u, 'what do you expect');
},
'testing descriptive message'
);
}
}
/* eslint-enable */
testing.testCases.push(TestCaseTestCase);
/* eslint-disable max-lines-per-function */
/* eslint-disable no-magic-numbers */
/* eslint-disable require-jsdoc */
class TestResultTestCase extends TestCase {
setUp() {
this.result = new TestResult();
}
testAddSuccess() {
this.assertEqual(this.result.successes, [], 'paranoia check');
// Act
this.result.addSuccess('TestClass.testMethod');
this.result.addSuccess('TestClass.testMethod');
// Assert
this.assertEqual(
this.result.successes,
['TestClass.testMethod', 'TestClass.testMethod'],
'real check'
);
}
testAddError() {
this.assertEqual(this.result.errors, [], 'paranoia check');
// Act
this.result.addError('name1', new Error('first message'));
this.result.addError('name2', new TypeError('second message'));
this.result.addError('name3', new Error('third message'));
// Assert
const actual = this.result.errors;
const expected = [
{name: 'name1', error: 'Error', message: 'first message'},
{name: 'name2', error: 'TypeError', message: 'second message'},
{name: 'name3', error: 'Error', message: 'third message'},
];
this.assertEqual(actual, expected, 'real check');
}
testAddFailure() {
this.assertEqual(this.result.failures, [], 'paranoia check');
// Act
this.result.addFailure('method1', 'a message');
this.result.addFailure('method2', 'another message');
// Assert
const actual = this.result.failures;
const expected = [
{name: 'method1', message: 'a message'},
{name: 'method2', message: 'another message'},
];
this.assertEqual(actual, expected, 'real check');
}
testAddSkip() {
this.assertEqual(this.result.skipped, [], 'paranoia check');
// Act
this.result.addSkip('Skip.Skip', 'skip to my lou');
this.result.addSkip('Skip.Skip', 'skip to my lou');
this.result.addSkip('Skip.ToMyLou', 'my darling');
// Assert
const actual = this.result.skipped;
const expected = [
{name: 'Skip.Skip', message: 'skip to my lou'},
{name: 'Skip.Skip', message: 'skip to my lou'},
{name: 'Skip.ToMyLou', message: 'my darling'},
];
this.assertEqual(actual, expected, 'real check');
}
testStartStop() {
// Act
this.result.startTest('Foo.testSomething');
this.result.startTest('Foo.testOrTheOther');
this.result.stopTest('Foo.testSomething');
// Assert
this.assertEqual(this.result.tests.size, 2, 'tests ran');
this.assertTrue(this.result.tests.get('Foo.testSomething').start,
'first start');
this.assertTrue(this.result.tests.get('Foo.testSomething').stop,
'first stop');
this.assertTrue(this.result.tests.get('Foo.testOrTheOther').start,
'second start');
this.assertFalse(this.result.tests.get('Foo.testOrTheOther').stop,
'second stop');
}
testWasSuccessful() {
this.assertTrue(this.result.wasSuccessful(), 'no results is a pass');
this.result.addSuccess('Class.method');
this.assertTrue(this.result.wasSuccessful(), 'new success is a pass');
this.result.addSkip('Class.differentMethod', 'rocks');
this.assertTrue(this.result.wasSuccessful(), 'a skip is a pass');
this.result.addError('NewClass.method', new Error());
this.assertFalse(this.result.wasSuccessful(), 'an error is not a pass');
const result = new TestResult();
this.assertTrue(result.wasSuccessful(), 'paranoia check');
result.addFailure('NewClass.failedMethod', 'oops');
this.assertFalse(result.wasSuccessful(), 'a failure is not a pass');
}
testSummary() {
const result = new TestResult();
this.assertEqual(
result.summary(),
[
'total : 0',
'successes : 0',
'skipped : 0',
'errors : 0',
'failures : 0',
],
'empty, no formatting'
);
this.assertEqual(
result.summary(true),
[
'total : 0',
'successes : 0',
'skipped : 0',
'errors : 0',
'failures : 0',
],
'empty, with formatting'
);
for (let i = 0; i < 100; i += 1) {
const currentMethod = `test-${i}`;
result.startTest(currentMethod);
if (i % 17 === 0) {
result.addError(currentMethod, new Error(`oops-${i}`));
} else if (i % 19 === 0) {
result.addFailure(currentMethod, `failed ${i}`);
} else if (i % 37 === 0) {
result.addSkip(currentMethod, `skip ${i}`);
} else {
result.addSuccess(currentMethod);
}
result.stopTest(currentMethod);
}
this.assertEqual(
result.summary(),
[
'total : 100',
'successes : 87',
'skipped : 2',
'errors : 6',
'failures : 5',
],
'full, no formatting'
);
this.assertEqual(
result.summary(true),
[
'total : 100',
'successes : 87',
'skipped : 2',
'errors : 6',
'failures : 5',
],
'full, with formatting'
);
for (let i = 0; i < 1000; i += 1) {
const currentMethod = `test-${i}`;
result.addFailure(currentMethod, `failed group 2 ${i}`);
}
this.assertEqual(
result.summary(),
[
'total : 100',
'successes : 87',
'skipped : 2',
'errors : 6',
'failures : 1005',
],
'extra failures, no formatting'
);
this.assertEqual(
result.summary(true),
[
'total : 100',
'successes : 87',
'skipped : 2',
'errors : 6',
'failures : 1005',
],
'extra, with formatting'
);
}
}
/* eslint-enable */
testing.testCases.push(TestResultTestCase);
/** Assembles and drives execution of {@link TestCase}s. */
class TestRunner {
/** @param {function(): TestCase} tests - TestCases to execute. */
constructor(tests) {
const badKlasses = [];
const testMethods = [];
for (const klass of tests) {
if (klass.prototype instanceof TestCase) {
testMethods.push(...this.#extractTestMethods(klass));
} else {
badKlasses.push(klass);
}
}
if (badKlasses.length) {
const msg = `Bad class count: ${badKlasses.length}`;
for (const klass of badKlasses) {
// eslint-disable-next-line no-console
console.error('Not a TestCase:', klass);
}
throw new TypeError(`Bad classes: ${msg}`);
}
this.#tests = testMethods;
}
/**
* Run each test method in turn.
* @returns {TestResult} - Collected results.
*/
runTests() {
const result = new TestResult();
let lastKlass = null;
let doRunTests = true;
for (const {klass, method} of this.#tests) {
if (klass !== lastKlass) {
this.#doClassCleanups(lastKlass, result);
doRunTests = this.#doSetUpClass(klass, result);
}
lastKlass = klass;
if (doRunTests) {
this.#doRunTestMethod(klass, method, result);
}
}
this.#doClassCleanups(lastKlass, result);
return result;
}
#tests
/** @param {function(): TestCase} klass - TestCase to process. */
#extractTestMethods = function *extractTestMethods(klass) {
let obj = klass;
while (obj) {
if (obj.prototype instanceof TestCase) {
for (const prop of Object.getOwnPropertyNames(obj.prototype)) {
if (prop.startsWith('test')) {
yield {klass: klass, method: prop};
}
}
}
obj = Object.getPrototypeOf(obj);
}
}
/**
* @param {function(): TestCase} klass - TestCase to process.
* @param {TestResult} result - Result to use if any errors.
*/
#doClassCleanups = (klass, result) => {
if (klass) {
const currentMethod = `${klass.name}.doClassCleanups`;
try {
klass.doClassCleanups();
} catch (e) {
result.addError(currentMethod, e);
}
}
}
/**
* @param {function(): TestCase} klass - TestCase to process.
* @param {TestResult} result - Result to use if any errors.
* @returns {boolean} - Indicates success of calling setUpClass().
*/
#doSetUpClass = (klass, result) => {
const currentMethod = `${klass.name}.setUpClass`;
try {
klass.setUpClass();
} catch (e) {
if (e instanceof TestCase.Skip) {
result.addSkip(currentMethod, e.message);
} else {
result.addError(currentMethod, e);
}
return false;
}
return true;
}
/**
* @param {function(): TestCase} Klass - TestCase to process.
* @param {string} methodName - Name of the test method to execute.
* @param {TestResult} result - Result of the execution.
*/
#doRunTestMethod = (Klass, methodName, result) => {
const instance = new Klass(methodName);
instance.run(result);
}
}
/* eslint-disable class-methods-use-this */
/* eslint-disable no-empty-function */
/* eslint-disable require-jsdoc */
/**
* TestCases require at least one test method to get instantiated by {@link
* TestRunner}
*/
class DummyMethodTestCase extends TestCase {
testDummy() {}
}
/* eslint-enable */
/* eslint-disable class-methods-use-this */
/* eslint-disable max-lines-per-function */
/* eslint-disable no-empty-function */
/* eslint-disable no-magic-numbers */
/* eslint-disable no-new */
/* eslint-disable require-jsdoc */
class TestRunnerTestCase extends TestCase {
testNoClasses() {
// Assemble
const runner = new TestRunner([]);
// Act
const result = runner.runTests();
// Assert
this.assertTrue(result.wasSuccessful());
}
testBadClasses() {
this.assertRaisesRegExp(TypeError, /Bad class count: 2$/u, () => {
new TestRunner([Error, TestRunnerTestCase, TypeError]);
});
}
testStrangeClassSetup() {
// Assemble
class ClassSetupErrorTestCase extends DummyMethodTestCase {
static setUpClass() {
throw new Error('erroring');
}
}
class ClassSetupFailTestCase extends DummyMethodTestCase {
static setUpClass() {
throw new this.Fail('failing');
}
}
class ClassSetupSkipTestCase extends DummyMethodTestCase {
static setUpClass() {
throw new this.Skip('skipping');
}
}
const classes = [
DummyMethodTestCase,
ClassSetupErrorTestCase,
ClassSetupFailTestCase,
ClassSetupSkipTestCase,
];
const runner = new TestRunner(classes);
// Act
const result = runner.runTests();
// Assert
this.assertFalse(result.wasSuccessful());
// In setUpClass, TestCase.Fail should count as an error
this.assertEqual(
result.successes,
['DummyMethodTestCase.testDummy'],
'successes'
);
this.assertEqual(
result.errors,
[
{
name: 'ClassSetupErrorTestCase.setUpClass',
error: 'Error',
message: 'erroring',
},
{
name: 'ClassSetupFailTestCase.setUpClass',
error: 'TestCase.Fail',
message: 'failing',
},
],
'errors'
);
this.assertEqual(result.failures, [], 'failures');
this.assertEqual(
result.skipped,
[{name: 'ClassSetupSkipTestCase.setUpClass', message: 'skipping'}],
'skipped'
);
}
testStrangeClassCleanups() {
// Assemble
class BaseClassCleanupTestCase extends DummyMethodTestCase {
static setUpClass() {
this.addClassCleanup(this.cleanupFunc);
}
static cleanupFunc() {}
}
class CleanupErrorTestCase extends BaseClassCleanupTestCase {
static cleanupFunc() {
throw new Error('cleanup error');
}
}
class CleanupFailTestCase extends BaseClassCleanupTestCase {
static cleanupFunc() {
throw new this.Fail('cleanup fail');
}
}
class CleanupSkipTestCase extends BaseClassCleanupTestCase {
static cleanupFunc() {
throw new this.Skip('cleanup skip');
}
}
const classes = [
BaseClassCleanupTestCase,
CleanupErrorTestCase,
CleanupFailTestCase,
CleanupSkipTestCase,
];
const runner = new TestRunner(classes);
// Act
const result = runner.runTests();
// Assert
this.assertFalse(result.wasSuccessful());
// In doClassCleanups, TestCase.{Fail,Skip} should count as errors,
// however, the test *also* passed already, so we get extra counts. Not
// sure if this is a bug or a feature.
this.assertEqual(
result.successes,
[
'BaseClassCleanupTestCase.testDummy',
'CleanupErrorTestCase.testDummy',
'CleanupFailTestCase.testDummy',
'CleanupSkipTestCase.testDummy',
],
'successes'
);
this.assertEqual(
result.errors,
[
{
name: 'CleanupErrorTestCase.doClassCleanups',
error: 'Error',
message: 'cleanup error',
},
{
name: 'CleanupFailTestCase.doClassCleanups',
error: 'TestCase.Fail',
message: 'cleanup fail',
},
{
name: 'CleanupSkipTestCase.doClassCleanups',
error: 'TestCase.Skip',
message: 'cleanup skip',
},
],
'errors'
);
this.assertEqual(result.failures, [], 'failures');
this.assertEqual(result.skipped, [], 'skipped');
}
testFindsTestMethods() {
// Assemble
class One extends TestCase {
test() {}
test_() {}
_test() {
this.fail('_test');
}
testOne() {
this.skip('One');
}
notATest() {
this.fail('notATest');
}
}
class Two extends TestCase {
alsoNotATest() {
this.fail('alsoNotATest');
}
testTwo() {
this.skip('Two');
}
}
const runner = new TestRunner([One, Two]);
// Act
const result = runner.runTests();
// Assert
this.assertTrue(result.wasSuccessful());
this.assertEqual(
result.successes, ['One.test', 'One.test_'], 'successes'
);
this.assertEqual(result.errors, [], 'errors');
this.assertEqual(result.failures, [], 'failures');
this.assertEqual(
result.skipped,
[
{name: 'One.testOne', message: 'One'},
{name: 'Two.testTwo', message: 'Two'},
],
'skipped'
);
}
testAccumulatesResults() {
class FooTestCase extends TestCase {
testFail() {
this.fail('Fail failed');
}
testNotEqual() {
this.assertEqual(1, 2);
}
testPass() {}
testError() {
throw new Error('Oh, dear!');
}
testSkip() {
this.skip('Skip skipped');
}
}
const runner = new TestRunner([FooTestCase]);
// Act
const result = runner.runTests();
// Assert
this.assertFalse(result.wasSuccessful(), 'had failures');
this.assertEqual(
result.errors,
[
{
name: 'FooTestCase.testError',
error: 'Error',
message: 'Oh, dear!',
},
],
'errors'
);
this.assertEqual(
result.failures,
[
{name: 'FooTestCase.testFail', message: 'Fail failed'},
{name: 'FooTestCase.testNotEqual', message: '1 !== 2'},
],
'failures'
);
this.assertEqual(
result.skipped,
[{name: 'FooTestCase.testSkip', message: 'Skip skipped'}],
'skipped'
);
this.assertEqual(
result.successes,
['FooTestCase.testPass'],
'successes'
);
}
}
/* eslint-enable */
testing.testCases.push(TestRunnerTestCase);
/**
* Run registered TestCases.
* @returns {TestResult} - Accumulated results of these tests.
*/
function runTests() {
const runner = new TestRunner(testing.testCases);
return runner.runTests();
}
return {
version: version,
testing: testing,
TestCase: TestCase,
runTests: runTests,
};
}());