import { mkdirSync } from "node:fs"; import { join, resolve } from "node:path"; export interface TransactionalDatabase { tx(callback: (transaction: TransactionalDatabase) => Promise): Promise; } const ROLLBACK = Symbol("wrnexus-test-rollback"); /** Run test work in a real transaction and always force rollback. */ export async function withDatabaseRollback( db: TransactionalDatabase, run: (transaction: TransactionalDatabase) => T | Promise, ): Promise { let output!: T; try { await db.tx(async (transaction) => { output = await run(transaction); throw ROLLBACK; }); } catch (error) { if (error !== ROLLBACK) throw error; } return output; } export function createFactory>(build: (sequence: number) => T) { let sequence = 0; return { build(overrides: Partial = {}): T { return { ...build(++sequence), ...overrides }; }, buildMany(count: number, overrides: Partial = {}): T[] { if (!Number.isInteger(count) || count < 0 || count > 10_000) throw new RangeError("Factory count must be between 0 and 10000"); return Array.from({ length: count }, () => ({ ...build(++sequence), ...overrides })); }, reset(): void { sequence = 0; }, }; } export interface BrowserArtifactPage { screenshot(options: { path: string; fullPage?: boolean }): Promise; context(): { tracing?: { stop(options: { path: string }): Promise } }; } export async function captureBrowserArtifacts( page: BrowserArtifactPage, testName: string, options: { root?: string; screenshot?: boolean; trace?: boolean } = {}, ): Promise<{ screenshot?: string; trace?: string }> { const safe = testName.replace(/[^A-Za-z0-9_.-]+/g, "-").slice(0, 120) || "test"; const root = resolve(options.root ?? join("test-results", "wrnexus")); mkdirSync(root, { recursive: true }); const output: { screenshot?: string; trace?: string } = {}; if (options.screenshot !== false) { output.screenshot = join(root, `${safe}.png`); await page.screenshot({ path: output.screenshot, fullPage: true }); } if (options.trace && page.context().tracing) { output.trace = join(root, `${safe}.zip`); await page.context().tracing!.stop({ path: output.trace }); } return output; }