This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user