import { createClient, type ApiClient, type ApiResponse } from './client.js' import { type TestResult, printSuiteHeader, printTestResult } from './reporter.js' export interface TestContext { api: ApiClient test: (name: string, optsOrFn: { tags?: string[] } | (() => Promise), maybeFn?: () => Promise) => void assert: { status: (res: ApiResponse, expected: number) => void equal: (actual: T, expected: T, msg?: string) => void notEqual: (actual: T, expected: T, msg?: string) => void ok: (value: unknown, msg?: string) => void truthy: (value: unknown, msg?: string) => void falsy: (value: unknown, msg?: string) => void includes: (arr: unknown[], value: unknown, msg?: string) => void contains: (str: string, sub: string, msg?: string) => void greaterThan: (actual: number, expected: number, msg?: string) => void throws: (fn: () => unknown, msg?: string) => void } } class AssertionError extends Error { constructor(message: string) { super(message) this.name = 'AssertionError' } } function makeAssert() { return { status(res: ApiResponse, expected: number) { if (res.status !== expected) { const body = typeof res.data === 'object' ? JSON.stringify(res.data) : String(res.data) throw new AssertionError(`Expected status ${expected}, got ${res.status}: ${body.slice(0, 200)}`) } }, equal(actual: T, expected: T, msg?: string) { if (actual !== expected) { throw new AssertionError(msg ?? `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`) } }, notEqual(actual: T, expected: T, msg?: string) { if (actual === expected) { throw new AssertionError(msg ?? `Expected value to not equal ${JSON.stringify(expected)}`) } }, ok(value: unknown, msg?: string) { if (!value) throw new AssertionError(msg ?? `Expected truthy, got ${JSON.stringify(value)}`) }, truthy(value: unknown, msg?: string) { if (!value) throw new AssertionError(msg ?? `Expected truthy, got ${JSON.stringify(value)}`) }, falsy(value: unknown, msg?: string) { if (value) throw new AssertionError(msg ?? `Expected falsy, got ${JSON.stringify(value)}`) }, includes(arr: unknown[], value: unknown, msg?: string) { if (!arr.includes(value)) { throw new AssertionError(msg ?? `Expected array to include ${JSON.stringify(value)}`) } }, contains(str: string, sub: string, msg?: string) { if (!str.includes(sub)) { throw new AssertionError(msg ?? `Expected "${str}" to contain "${sub}"`) } }, greaterThan(actual: number, expected: number, msg?: string) { if (actual <= expected) { throw new AssertionError(msg ?? `Expected ${actual} > ${expected}`) } }, throws(fn: () => unknown, msg?: string) { try { fn() throw new AssertionError(msg ?? 'Expected function to throw') } catch (e) { if (e instanceof AssertionError) throw e } }, } } interface RegisteredTest { name: string tags: string[] fn: () => Promise } export interface SuiteDefinition { name: string tags: string[] setup: (ctx: TestContext) => void | Promise } const registeredSuites: SuiteDefinition[] = [] export function suite( name: string, optsOrFn: { tags?: string[] } | ((ctx: TestContext) => void | Promise), maybeFn?: (ctx: TestContext) => void | Promise, ) { const opts = typeof optsOrFn === 'function' ? {} : optsOrFn const fn = typeof optsOrFn === 'function' ? optsOrFn : maybeFn! registeredSuites.push({ name, tags: opts.tags ?? [], setup: fn }) } export function getSuites(): SuiteDefinition[] { return registeredSuites } export async function runSuite( suiteDef: SuiteDefinition, baseUrl: string, token: string, filterTags?: string[], ): Promise { const tests: RegisteredTest[] = [] const api = createClient(baseUrl, token) const ctx: TestContext = { api, assert: makeAssert(), test(name, optsOrFn, maybeFn) { const opts = typeof optsOrFn === 'function' ? {} : optsOrFn const fn = typeof optsOrFn === 'function' ? optsOrFn : maybeFn! tests.push({ name, tags: (opts as { tags?: string[] }).tags ?? [], fn }) }, } await suiteDef.setup(ctx) printSuiteHeader(suiteDef.name) const results: TestResult[] = [] for (const test of tests) { // Tag filtering if (filterTags && filterTags.length > 0) { const allTags = [...suiteDef.tags, ...test.tags] if (!filterTags.some((t) => allTags.includes(t))) continue } const start = performance.now() try { await test.fn() const duration = Math.round(performance.now() - start) const result: TestResult = { name: test.name, suite: suiteDef.name, tags: test.tags, passed: true, duration } results.push(result) printTestResult(result) } catch (err) { const duration = Math.round(performance.now() - start) const error = err instanceof Error ? err.message : String(err) const result: TestResult = { name: test.name, suite: suiteDef.name, tags: test.tags, passed: false, duration, error } results.push(result) printTestResult(result) } } console.log('') return results }