class TestRunner {
constructor() {
this.tests = [];
this.passed = 0;
this.failed = 0;
this.failures = [];
this.currentSuite = null;
this.testCount = 0;
this.suites = new Map(); }
describe(name, fn) {
const parentSuite = this.currentSuite;
const suite = {
name,
tests: [],
beforeEachCallbacks: [],
afterEachCallbacks: [],
parent: parentSuite,
};
this.suites.set(suite, {
beforeEach: [],
afterEach: [],
});
this.currentSuite = suite;
this.tests.push(suite);
try {
fn();
} finally {
this.currentSuite = parentSuite;
}
}
beforeEach(fn) {
if (!this.currentSuite) {
throw new Error("beforeEach must be called within a describe block");
}
this.suites.get(this.currentSuite).beforeEach.push(fn);
}
afterEach(fn) {
if (!this.currentSuite) {
throw new Error("afterEach must be called within a describe block");
}
this.suites.get(this.currentSuite).afterEach.push(fn);
}
async runTest(suite, test) {
const beforeEachCallbacks = [];
let currentSuite = suite;
while (currentSuite) {
const hooks = this.suites.get(currentSuite);
beforeEachCallbacks.unshift(...hooks.beforeEach);
currentSuite = currentSuite.parent;
}
const afterEachCallbacks = [];
currentSuite = suite;
while (currentSuite) {
const hooks = this.suites.get(currentSuite);
afterEachCallbacks.push(...hooks.afterEach);
currentSuite = currentSuite.parent;
}
try {
for (const callback of beforeEachCallbacks) {
await callback();
}
const testPromise = new Promise(async (resolve, reject) => {
try {
if (test.fn.length > 0) {
let doneCallCount = 0;
let settled = false;
let settleTimer = null;
const done = (error) => {
doneCallCount += 1;
if (doneCallCount > 1) {
if (!settled) {
settled = true;
if (settleTimer && typeof clearTimeout === "function")
clearTimeout(settleTimer);
reject(new Error("done() called multiple times"));
} else {
console.error(
"done() called multiple times (after test already settled)",
);
}
return;
}
if (error) {
if (!settled) {
settled = true;
if (settleTimer && typeof clearTimeout === "function")
clearTimeout(settleTimer);
reject(error);
}
return;
}
if (typeof setTimeout === "function") {
settleTimer = setTimeout(() => {
if (settled) return;
settled = true;
resolve();
}, 0);
} else {
if (!settled) {
settled = true;
resolve();
}
}
};
const result = test.fn(done);
if (result && typeof result.then === "function") {
if (!settled) {
settled = true;
if (settleTimer && typeof clearTimeout === "function")
clearTimeout(settleTimer);
reject(new Error("Test uses both promise and done callback"));
}
}
} else {
const result = test.fn();
if (result && typeof result.then === "function") {
await result;
}
resolve();
}
} catch (e) {
reject(e);
}
});
if (typeof setTimeout === "function") {
let timeoutId = null;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(
() =>
reject(new Error(`Test timeout after 30 seconds: ${test.name}`)),
30000,
);
});
try {
await Promise.race([testPromise, timeoutPromise]);
} finally {
if (timeoutId != null && typeof clearTimeout === "function") {
clearTimeout(timeoutId);
}
}
} else {
await testPromise;
}
this.passed++;
console.log(` ✓ Passed`);
} catch (e) {
this.failed++;
this.failures.push({
suite: suite?.name ?? "<unknown suite>",
test: test?.name ?? "<unknown test>",
message: e?.message ?? String(e),
stack: e?.stack ?? null,
});
console.log(` ✗ Failed`);
if (e.message) console.log(` Error: ${e.message}`);
if (e.stack) console.log(e.stack.split("\n").slice(1).join("\n"));
} finally {
for (const callback of afterEachCallbacks) {
try {
await callback();
} catch (e) {
console.error("Error in afterEach:", e);
}
}
}
}
async runTests() {
const limit =
typeof globalThis.__RONG_TEST_LIMIT__ === "number"
? globalThis.__RONG_TEST_LIMIT__
: null;
const filter =
typeof globalThis.__RONG_TEST_FILTER__ === "string" &&
globalThis.__RONG_TEST_FILTER__.length > 0
? new RegExp(globalThis.__RONG_TEST_FILTER__)
: null;
for (const suite of this.tests) {
console.log(`\nRunning suite: ${suite.name}`);
for (const test of suite.tests) {
if (limit != null && test.number > limit) {
console.log(`\nTest Results:`);
console.log(` Passed: ${this.passed}`);
console.log(` Failed: ${this.failed}`);
return this.failed === 0;
}
if (filter && !filter.test(`${suite.name} ${test.name}`)) {
continue;
}
console.log(`${test.number}. ${test.name}...`);
await this.runTest(suite, test);
}
}
console.log(`\nTest Results:`);
console.log(` Passed: ${this.passed}`);
console.log(` Failed: ${this.failed}`);
return this.failed === 0;
}
it(name, fn) {
if (!this.currentSuite) {
throw new Error("it() must be called within a describe() block");
}
this.testCount++;
this.currentSuite.tests.push({ name, fn, number: this.testCount });
}
expect(value) {
const matchers = {
toBe: (expected) => {
if (value !== expected) {
throw new Error(`Expected ${expected}, got ${value}`);
}
},
toContain: (expected) => {
if (Array.isArray(value)) {
if (!value.includes(expected)) {
throw new Error(`Expected array to contain ${expected}`);
}
} else if (typeof value === "string") {
if (!value.includes(expected)) {
throw new Error(
`Expected string to contain "${expected}", got "${value}"`,
);
}
} else {
throw new Error("toContain can only be used with arrays or strings");
}
},
toBeTruthy: () => {
if (!value) {
throw new Error(`Expected value to be truthy, got ${value}`);
}
},
toBeFalsy: () => {
if (value) {
throw new Error(`Expected value to be falsy, got ${value}`);
}
},
toEqual: (expected) => {
if (value instanceof Uint8Array && expected instanceof Uint8Array) {
if (value.length !== expected.length) {
throw new Error(
`Expected Uint8Array length ${expected.length}, but got ${value.length}`,
);
}
for (let i = 0; i < value.length; i++) {
if (value[i] !== expected[i]) {
throw new Error(
`Expected Uint8Array element at index ${i} to be ${expected[i]}, but got ${value[i]}`,
);
}
}
} else if (Array.isArray(value) && Array.isArray(expected)) {
if (value.length !== expected.length) {
throw new Error(
`Expected array length ${expected.length}, but got ${value.length}`,
);
}
for (let i = 0; i < value.length; i++) {
if (value[i] !== expected[i]) {
throw new Error(
`Expected array element at index ${i} to be ${expected[i]}, but got ${value[i]}`,
);
}
}
} else if (value !== expected) {
throw new Error(`Expected ${expected}, but got ${value}`);
}
},
toThrow: (expectedReasonOrType) => {
try {
value();
return {
pass: false,
message: () => `Expected function to throw, but it did not.`,
};
} catch (error) {
if (
typeof expectedReasonOrType === "function" &&
expectedReasonOrType.prototype instanceof Error
) {
const pass = error instanceof expectedReasonOrType;
return {
pass,
message: () =>
pass
? `Expected function not to throw ${expectedReasonOrType.name}, but it threw: ${error}`
: `Expected function to throw ${expectedReasonOrType.name}, but it threw: ${error}`,
};
}
if (expectedReasonOrType === undefined) {
return {
pass: true,
message: () =>
`Expected function not to throw, but it threw: ${error}`,
};
}
let pass = false;
try {
assert.equal(error, expectedReasonOrType);
pass = true;
} catch (e) {
pass = false;
}
return {
pass,
message: () =>
pass
? `Expected function not to throw ${expectedReasonOrType}, but it threw: ${error}`
: `Expected function to throw ${expectedReasonOrType}, but it threw: ${error}`,
};
}
},
toBeUndefined: () => {
if (value !== undefined) {
throw new Error(`Expected value to be undefined, got ${value}`);
}
},
toBeInstanceOf(expected) {
if (!(value instanceof expected)) {
throw new Error(
`Expected value to be an instance of ${expected.name}, but got ${value.constructor.name}`,
);
}
},
};
const negatedMatchers = {
toBe: (expected) => {
if (value === expected) {
throw new Error(`Expected ${value} not to be ${expected}`);
}
},
toContain: (expected) => {
if (Array.isArray(value)) {
if (value.includes(expected)) {
throw new Error(`Expected array not to contain ${expected}`);
}
} else if (typeof value === "string") {
if (value.includes(expected)) {
throw new Error(
`Expected string not to contain "${expected}", got "${value}"`,
);
}
} else {
throw new Error("toContain can only be used with arrays or strings");
}
},
toThrow: (expected) => {
try {
value();
} catch (e) {
throw new Error(
`Expected function not to throw, but it threw ${e.message}`,
);
}
},
};
return {
...matchers,
not: {
...negatedMatchers,
},
};
}
}
const runner = new TestRunner();
globalThis.describe = (name, fn) => runner.describe(name, fn);
globalThis.it = (name, fn) => runner.it(name, fn);
globalThis.expect = (value) => runner.expect(value);
globalThis.afterEach = (fn) => runner.afterEach(fn);
globalThis.beforeEach = (fn) => runner.beforeEach(fn);
globalThis.runTests = () => runner.runTests();