Files
omniai-web/src/test/testHarness.ts
T

151 lines
3.3 KiB
TypeScript
Raw Normal View History

2026-06-09 11:34:56 +08:00
type TestFn = () => unknown | Promise<unknown>;
interface TestCase {
name: string;
run: TestFn;
afterEachFns: TestFn[];
}
const tests: TestCase[] = [];
const suiteStack: string[] = [];
const afterEachStack: TestFn[][] = [[]];
function formatValue(value: unknown): string {
return typeof value === "string" ? `"${value}"` : JSON.stringify(value);
}
function fail(message: string): never {
throw new Error(message);
}
function assertThrows(
actual: unknown,
expectedMessage?: string,
inverted = false,
): void {
if (typeof actual !== "function") {
fail("Expected value to be a function");
}
let thrown: unknown;
try {
actual();
} catch (error) {
thrown = error;
}
if (inverted) {
if (thrown)
fail(
`Expected function not to throw, but it threw ${thrown instanceof Error ? thrown.message : String(thrown)}`,
);
return;
}
if (!thrown) fail("Expected function to throw");
if (expectedMessage) {
const message = thrown instanceof Error ? thrown.message : String(thrown);
if (!message.includes(expectedMessage)) {
fail(
`Expected thrown message to include "${expectedMessage}", got "${message}"`,
);
}
}
}
export function describe(name: string, run: () => void): void {
suiteStack.push(name);
afterEachStack.push([]);
try {
run();
} finally {
afterEachStack.pop();
suiteStack.pop();
}
}
export function it(name: string, run: TestFn): void {
tests.push({
name: [...suiteStack, name].join(" > "),
run,
afterEachFns: afterEachStack.flat(),
});
}
export function afterEach(run: TestFn): void {
afterEachStack[afterEachStack.length - 1].push(run);
}
export function expect(actual: unknown) {
return {
toBe(expected: unknown): void {
if (!Object.is(actual, expected)) {
fail(`Expected ${formatValue(actual)} to be ${formatValue(expected)}`);
}
},
toEqual(expected: unknown): void {
const actualJson = JSON.stringify(actual);
const expectedJson = JSON.stringify(expected);
if (actualJson !== expectedJson) {
fail(`Expected ${actualJson} to equal ${expectedJson}`);
}
},
toThrow(expectedMessage?: string): void {
assertThrows(actual, expectedMessage);
},
not: {
toThrow(): void {
assertThrows(actual, undefined, true);
},
},
};
}
export async function runRegisteredTests(): Promise<{
passed: number;
failed: number;
total: number;
}> {
let passed = 0;
let failed = 0;
for (const test of tests) {
let failure: unknown;
try {
await test.run();
} catch (error) {
failure = error;
}
for (const hook of [...test.afterEachFns].reverse()) {
try {
await hook();
} catch (error) {
failure = failure || error;
}
}
if (failure) {
failed += 1;
console.error(`FAIL ${test.name}`);
console.error(
failure instanceof Error
? ` ${failure.message}`
: ` ${String(failure)}`,
);
} else {
passed += 1;
console.log(`PASS ${test.name}`);
}
}
const total = passed + failed;
if (total === 0) {
console.error("No unit tests were registered.");
return { passed, failed: 1, total };
}
console.log(`\n${passed} passed, ${failed} failed`);
return { passed, failed, total };
}