diff --git a/.changeset/old-mugs-stare.md b/.changeset/old-mugs-stare.md new file mode 100644 index 0000000..9214c5e --- /dev/null +++ b/.changeset/old-mugs-stare.md @@ -0,0 +1,5 @@ +--- +"@bomb.sh/tools": patch +--- + +Adds `@bomb.sh/tools/test-utils` export with `createFixture()` diff --git a/package.json b/package.json index 4b859eb..263ef5e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,10 @@ ".": { "import": "./dist/bin.mjs" }, + "./test-utils": { + "types": "./dist/test-utils.d.mts", + "import": "./dist/test-utils.mjs" + }, "./*": "./dist/*", "./package.json": "./package.json", "./tsconfig.json": "./tsconfig.json" diff --git a/src/test-utils.test.ts b/src/test-utils.test.ts new file mode 100644 index 0000000..01672f2 --- /dev/null +++ b/src/test-utils.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from "vitest"; +import { existsSync } from "node:fs"; +import { readFile, writeFile } from "node:fs/promises"; +import { createFixture } from "./test-utils.ts"; + +describe("createFixture", () => { + it("creates files on disk from inline tree", async () => { + const fixture = await createFixture({ + "hello.txt": "hello world", + }); + const content = await readFile(fixture.resolve("hello.txt"), "utf8"); + expect(content).toBe("hello world"); + }); + + it("creates nested directories from slash-separated keys", async () => { + const fixture = await createFixture({ + "src/index.ts": "export const x = 1", + "src/utils/helpers.ts": "export function help() {}", + }); + expect(existsSync(fixture.resolve("src/index.ts"))).toBe(true); + expect(existsSync(fixture.resolve("src/utils/helpers.ts"))).toBe(true); + }); + + it("resolve returns absolute path within fixture root", async () => { + const fixture = await createFixture({ "a.txt": "" }); + expect(fixture.resolve("a.txt").toString()).toContain(fixture.root.toString()); + }); + + it("readFile reads the actual file", async () => { + const fixture = await createFixture({ "a.txt": "Empty" }); + expect(await fixture.readFile("a.txt")).toEqual("Empty"); + await writeFile(fixture.resolve("a.txt"), "Hello world!", { encoding: "utf-8" }); + expect(await fixture.readFile("a.txt")).toEqual("Hello world!"); + }); + + it("cleanup removes the temp directory", async () => { + const fixture = await createFixture({ "a.txt": "" }); + const path = fixture.root; + expect(existsSync(path)).toBe(true); + await fixture.cleanup(); + expect(existsSync(path)).toBe(false); + }); +}); diff --git a/src/test-utils.ts b/src/test-utils.ts new file mode 100644 index 0000000..09f9e78 --- /dev/null +++ b/src/test-utils.ts @@ -0,0 +1,40 @@ +import type { PathLike } from "node:fs"; +import { mkdtemp, mkdir, writeFile, rm, readFile as fsReadFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { sep } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { onTestFinished } from "vitest"; + +export interface Fixture { + root: URL; + resolve: (...segments: string[]) => URL; + readFile: (file: PathLike) => Promise; + cleanup: () => Promise; +} + +export async function createFixture(files: Record): Promise { + const root = new URL(`bsh-`, `file://${tmpdir()}/`); + const path = await mkdtemp(fileURLToPath(root)); + const base = pathToFileURL(path + sep); + + for (const [name, content] of Object.entries(files)) { + const url = new URL(name, base); + const dir = new URL("./", url); + await mkdir(dir, { recursive: true }); + await writeFile(url, content, "utf8"); + } + + const cleanup = () => rm(path, { recursive: true, force: true }); + onTestFinished(cleanup); + + const resolve = (...segments: string[]) => new URL(`./${segments.join("/")}`, base); + const readFile = (file: PathLike) => + fsReadFile(new URL(`./${file}`, base), { encoding: "utf-8" }); + + return { + root: pathToFileURL(path), + resolve, + readFile, + cleanup, + }; +}