134 lines
4.3 KiB
JavaScript
134 lines
4.3 KiB
JavaScript
// Tiny test framework: a TestCase subclass declares `test_*` methods, and the
|
|
// runner discovers and calls them. Tests may be async; the runner awaits each
|
|
// one.
|
|
|
|
export class TestCase {
|
|
assert(condition, message = "assertion failed") {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
assertTrue(value, message = "expected truthy") {
|
|
this.assert(!!value, message);
|
|
}
|
|
|
|
assertFalse(value, message = "expected falsy") {
|
|
this.assert(!value, message);
|
|
}
|
|
|
|
assertNull(value, message = "expected null") {
|
|
this.assert(value === null || value === undefined, message);
|
|
}
|
|
|
|
assertNotNull(value, message = "expected not null") {
|
|
this.assert(value !== null && value !== undefined, message);
|
|
}
|
|
|
|
assertEqual(actual, expected, message = "values differ") {
|
|
this.assert(deepEqual(actual, expected), `${message}: ${format(actual)} !== ${format(expected)}`);
|
|
}
|
|
|
|
assertNotEqual(actual, expected, message = "values unexpectedly equal") {
|
|
this.assert(!deepEqual(actual, expected), `${message}: both ${format(actual)}`);
|
|
}
|
|
|
|
assertApprox(actual, expected, epsilon = 1e-9, message = "not approximately equal") {
|
|
const ok = Math.abs(actual - expected) <= epsilon;
|
|
this.assert(ok, `${message}: ${actual} vs ${expected} (eps ${epsilon})`);
|
|
}
|
|
|
|
assertEmpty(value, message = "expected empty") {
|
|
this.assert(sizeOf(value) === 0, message);
|
|
}
|
|
|
|
assertNotEmpty(value, message = "expected not empty") {
|
|
this.assert(sizeOf(value) > 0, message);
|
|
}
|
|
|
|
assertSize(value, expected, message = "unexpected size") {
|
|
this.assert(sizeOf(value) === expected, `${message}: ${sizeOf(value)} !== ${expected}`);
|
|
}
|
|
|
|
assertHas(container, key, message = "missing key") {
|
|
this.assert(has(container, key), `${message}: ${String(key)}`);
|
|
}
|
|
|
|
assertGreater(actual, expected, message = "not greater") {
|
|
this.assert(actual > expected, `${message}: ${actual} <= ${expected}`);
|
|
}
|
|
|
|
assertLess(actual, expected, message = "not less") {
|
|
this.assert(actual < expected, `${message}: ${actual} >= ${expected}`);
|
|
}
|
|
|
|
assertGreaterOrEqual(actual, expected, message = "not greater or equal") {
|
|
this.assert(actual >= expected, `${message}: ${actual} < ${expected}`);
|
|
}
|
|
}
|
|
|
|
function sizeOf(value) {
|
|
if (value == null) return 0;
|
|
if (typeof value === "string") return value.length;
|
|
if (Array.isArray(value)) return value.length;
|
|
if (value instanceof Map || value instanceof Set) return value.size;
|
|
// Array-like objects (for example jQuery collections) expose a numeric length.
|
|
if (typeof value === "object" && typeof value.length === "number") return value.length;
|
|
if (typeof value === "object") return Object.keys(value).length;
|
|
return 0;
|
|
}
|
|
|
|
function has(container, key) {
|
|
if (container instanceof Map) return container.has(key);
|
|
if (container instanceof Set) return container.has(key);
|
|
if (container != null && typeof container === "object") {
|
|
return Object.prototype.hasOwnProperty.call(container, key);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function deepEqual(a, b) {
|
|
if (a === b) return true;
|
|
if (typeof a !== typeof b) return false;
|
|
if (a == null || b == null) return a === b;
|
|
if (typeof a === "number") return Number.isNaN(a) && Number.isNaN(b);
|
|
if (a instanceof Map || b instanceof Map) {
|
|
if (!(a instanceof Map) || !(b instanceof Map) || a.size !== b.size) return false;
|
|
for (const [key, value] of a) {
|
|
if (!b.has(key) || !deepEqual(value, b.get(key))) return false;
|
|
}
|
|
return true;
|
|
}
|
|
if (a instanceof Set || b instanceof Set) {
|
|
if (!(a instanceof Set) || !(b instanceof Set) || a.size !== b.size) return false;
|
|
for (const value of a) {
|
|
if (!b.has(value)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
if (Array.isArray(a) || Array.isArray(b)) {
|
|
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (!deepEqual(a[i], b[i])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
if (typeof a === "object") {
|
|
const keysA = Object.keys(a);
|
|
const keysB = Object.keys(b);
|
|
if (keysA.length !== keysB.length) return false;
|
|
for (const key of keysA) {
|
|
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
if (!deepEqual(a[key], b[key])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function format(value) {
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|