151 lines
3.3 KiB
TypeScript
151 lines
3.3 KiB
TypeScript
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 };
|
|
}
|