import type { DataInit, EachFn, Fn, Hooks, HooksFn, ModuleFn, TestEachFn, TestFn, TestOrEachFn } from "./qunit.d"; import type { TestContext } from "bun-test"; import { inspect, deepEquals } from "bun"; import { Assert } from "./assert"; type Status = "todo" | "skip" | "only" | undefined; type Module = { name: string; status: Status; before: Fn[]; beforeEach: Fn[]; afterEach: Fn[]; after: Fn[]; addHooks(hooks?: Hooks | HooksFn): void; addTest(name: string, status: Status, fn?: Fn): void; addTests(name: string, status: Status, data: DataInit, fn?: EachFn): void; }; function newModule(context: TestContext, moduleName: string, moduleStatus?: Status): Module { const before: Fn[] = []; const beforeEach: Fn[] = []; const afterEach: Fn[] = []; const after: Fn[] = []; let tests = 0; const addTest = (name: string, status: Status, fn?: Fn) => { const runTest = async () => { if (fn === undefined) { return; } const assert = new Assert(context.expect); if (tests++ === 1) { for (const fn of before) { await fn(assert); } } for (const fn of beforeEach) { await fn(assert); } try { await fn(assert); } finally { for (const fn of afterEach) { await fn(assert); } // TODO: need a way to know when module is done if (false) { for (const fn of after) { await fn(assert); } } // TODO: configurable timeout await assert.close(100); } }; hideFromStack(runTest); const addTest = () => { if (moduleStatus !== undefined) { status = moduleStatus; } if (status === undefined) { context.test(name, runTest); } else if (status === "skip" || status === "todo") { context.test.skip(name, runTest); } else { context.test.only(name, runTest); } }; hideFromStack(addTest); if (moduleName) { context.describe(moduleName, addTest); } else { addTest(); } }; hideFromStack(addTest); if (moduleStatus === "skip" || moduleStatus === "todo") { context.test.skip(moduleName, () => {}); } return { name: moduleName, status: moduleStatus, before, beforeEach, afterEach, after, addHooks(hooks) { if (hooks === undefined) { return; } if (typeof hooks === "object") { if (hooks.before !== undefined) { before.push(hooks.before); } if (hooks.beforeEach !== undefined) { beforeEach.push(hooks.beforeEach); } if (hooks.afterEach !== undefined) { afterEach.push(hooks.afterEach); } if (hooks.after !== undefined) { after.push(hooks.after); } } else { hooks({ before(fn) { before.push(fn); }, beforeEach(fn) { beforeEach.push(fn); }, afterEach(fn) { afterEach.push(fn); }, after(fn) { after.push(fn); }, }); } }, addTest, addTests(name, status, data, fn) { let entries: [string, unknown][]; if (Array.isArray(data)) { entries = data.map(value => [inspect(value), value]); } else { entries = Object.entries(data); } for (const [key, value] of entries) { context.describe(name, () => { addTest(key, status, fn ? assert => fn(assert, value) : undefined); }); } }, }; } hideFromStack(newModule); function hideFromStack(object: any): void { if (typeof object === "function") { Object.defineProperty(object, "name", { value: "::bunternal::", }); return; } for (const name of Object.getOwnPropertyNames(object)) { Object.defineProperty(object[name], "name", { value: "::bunternal::", }); } } function todo(name: string) { const todo = () => { throw new Error(`Not implemented: QUnit.${name}`); }; hideFromStack(todo); return todo; } function newCallable(callable: C, object: O): C & O { // @ts-expect-error return Object.assign(callable, object); } function newQUnit(context: TestContext): import("./qunit.d").QUnit { let module: Module = newModule(context, ""); let modules: Module[] = [module]; const addModule = (name: string, status?: Status, hooks?: Hooks | HooksFn, fn?: HooksFn) => { module = newModule(context, name, status); modules.push(module); module.addHooks(hooks); module.addHooks(fn); }; hideFromStack(addModule); return { assert: Assert.prototype, hooks: { beforeEach(fn) { for (const module of modules) { module.beforeEach.push(fn); } }, afterEach(fn) { for (const module of modules) { module.afterEach.push(fn); } }, }, start() {}, module: newCallable< ModuleFn, { skip: ModuleFn; todo: ModuleFn; only: ModuleFn; } >( (name, hooks, fn) => { addModule(name, undefined, hooks, fn); }, { skip(name, hooks, fn) { addModule(name, "skip", hooks, fn); }, todo(name, hooks, fn) { addModule(name, "todo", hooks, fn); }, only(name, hooks, fn) { addModule(name, "only", hooks, fn); }, }, ), test: newCallable< TestFn, { each: TestEachFn; skip: TestOrEachFn; todo: TestOrEachFn; only: TestOrEachFn; } >( (name, fn) => { module.addTest(name, undefined, fn); }, { each: (name, data, fn) => { module.addTests(name, undefined, data, fn); }, skip: newCallable< TestFn, { each: TestEachFn; } >( (name, fn) => { module.addTest(name, "skip", fn); }, { each(name, data, fn) { module.addTests(name, "skip", data, fn); }, }, ), todo: newCallable< TestFn, { each: TestEachFn; } >( (name, fn) => { module.addTest(name, "todo", fn); }, { each(name, data, fn) { module.addTests(name, "todo", data, fn); }, }, ), only: newCallable< TestFn, { each: TestEachFn; } >( (name, fn) => { module.addTest(name, "only", fn); }, { each(name, data, fn) { module.addTests(name, "only", data, fn); }, }, ), }, ), skip(name, fn) { module.addTest(name, "skip", fn); }, todo(name, fn) { module.addTest(name, "todo", fn); }, only(name, fn) { module.addTest(name, "only", fn); }, dump: { maxDepth: Infinity, parse(data) { return inspect(data); }, }, extend(target: any, mixin) { return Object.assign(target, mixin); }, equiv(a, b) { return deepEquals(a, b); }, config: {}, testDone: todo("testDone"), testStart: todo("testStart"), moduleDone: todo("moduleDone"), moduleStart: todo("moduleStart"), begin: todo("begin"), done: todo("done"), log: todo("log"), onUncaughtException: todo("onUncaughtException"), push: todo("push"), stack: todo("stack"), on: todo("on"), }; } const { expect, describe, test, beforeAll, beforeEach, afterEach, afterAll } = Bun.jest(import.meta.path); export const QUnit = newQUnit({ expect, describe, test, beforeAll, beforeEach, afterEach, afterAll, }); export { Assert }; // @ts-expect-error globalThis.QUnit = QUnit;