/// import { afterAll, beforeAll, it } from "bun:test"; import path from "pathe"; import { alchemy } from "../alchemy.ts"; import { Scope } from "../scope.ts"; import type { TestOptions } from "./options.ts"; /** * Extend the Alchemy interface to include test functionality */ declare module "../alchemy.ts" { interface Alchemy { test: typeof test; } } /** * Add test functionality to alchemy instance */ alchemy.test = test; /** * Test function type definition with overloads */ type test = { /** * Create a test with default options * @param name Test name * @param fn Test function * @param timeout Optional timeout in milliseconds */ (name: string, fn: (scope: Scope) => Promise, timeout?: number): void; /** * Create a test with custom options * @param name Test name * @param options Test configuration options * @param fn Test function * @param timeout Optional timeout in milliseconds */ ( name: string, options: TestOptions, fn: (scope: Scope) => Promise, timeout?: number, ): void; /** * Skip test conditionally * @param condition If true, test will be skipped */ skipIf(condition: boolean): test; skip: { /** * Create a test with default options * @param name Test name * @param fn Test function * @param timeout Optional timeout in milliseconds */ (name: string, fn: (scope: Scope) => Promise, timeout?: number): void; /** * Create a test with custom options * @param name Test name * @param options Test configuration options * @param fn Test function * @param timeout Optional timeout in milliseconds */ ( name: string, options: TestOptions, fn: (scope: Scope) => Promise, timeout?: number, ): void; }; beforeAll(fn: (scope: Scope) => Promise, timeout?: number): void; afterAll(fn: (scope: Scope) => Promise, timeout?: number): void; /** * Current test scope */ scope: Scope; }; /** * Creates a test helper function that provides scoped resource management * * @param meta Import meta object from the test file * @param defaultOptions Default options to apply to all tests * @returns Test function with scope management * * @example * ```typescript * const test = alchemy.test(import.meta, { prefix: BRANCH_PREFIX }); * * describe("My Resource", () => { * test("create and delete", async (scope) => { * try { * const resource = await MyResource("test", { ... }); * expect(resource.id).toBeTruthy(); * } finally { * await alchemy.destroy(scope); * } * }); * }); * ``` */ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test { defaultOptions = defaultOptions ?? { quiet: true, }; // Add skipIf functionality test.skipIf = it.skipIf.bind(it); test.skip = it.skip.bind(it); // Create local test scope based on filename const scope = new Scope({ parent: undefined, scopeName: `${ defaultOptions.prefix ? `${defaultOptions.prefix}-` : "" }${path.basename(meta.filename)}`, // parent: globalTestScope, stateStore: defaultOptions?.stateStore, phase: "up", noTrack: true, local: defaultOptions.local, }); test.beforeAll = (fn: (scope: Scope) => Promise) => { return beforeAll(() => scope.run(() => fn(scope))); }; test.afterAll = (fn: (scope: Scope) => Promise) => { return afterAll(() => scope.run(() => fn(scope))); }; return test as any; function test( ...args: | [ name: string, options: TestOptions, fn: (scope: Scope) => Promise, ] | [name: string, fn: (scope: Scope) => Promise] ) { const testName = args[0]; const _options = typeof args[1] === "object" ? args[1] : undefined; const timeout = typeof args[args.length - 1] === "number" ? (args[args.length - 1] as number) : 150000; const spread = (obj: any) => obj && typeof obj === "object" ? Object.fromEntries( Object.entries(obj).flatMap(([k, v]) => v !== undefined ? [[k, v]] : [], ), ) : {}; // Merge options with defaults const options: TestOptions = { quiet: false, password: "test-password", ...spread(defaultOptions), ...spread(_options), }; console.log("options", options); const fn = typeof args[1] === "function" ? args[1] : args[2]!; return it( testName, () => alchemy.run( testName, { ...options, parent: scope, }, async (scope) => { await scope.run(() => fn(scope)); }, ), timeout, ); } }