Files
2026-09-16 19:59:18 +02:00

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);
}
}