import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { ContentLoader, ContentRegistry, spriteSchema, soundSchema, type SoundDef, type SpriteDef, } from "../src/index.js"; const SPRITE_YAML = [ "id: parsnip-mature", "frames:", " - name: idle", " durationTicks: 1", " layers:", " - { shape: rect, x: 8, y: 8, w: 16, h: 16, fill: '#2e7d32' }", " - { shape: circle, x: 16, y: 8, r: 6, fill: '#66bb6a' }", "", ].join("\n"); const SOUND_YAML = [ "id: coin", "synth:", " wave: sine", " freq: 880", " durationMs: 120", " gain: 0.5", " slideTo: 1320", "", ].join("\n"); async function setupLoader(): Promise<{ registry: ContentRegistry; loader: ContentLoader; tmp: string }> { const tmp = await mkdtemp(join(tmpdir(), "farm-sprite-sound-")); await mkdir(join(tmp, "sprites"), { recursive: true }); await mkdir(join(tmp, "sounds"), { recursive: true }); await writeFile(join(tmp, "sprites", "parsnip-mature.yaml"), SPRITE_YAML); await writeFile(join(tmp, "sounds", "coin.yaml"), SOUND_YAML); const registry = new ContentRegistry(); const loader = new ContentLoader(registry); return { registry, loader, tmp }; } describe("sprite content kind", () => { it("loads and validates sprite YAML from content/sprites/", async () => { const { registry, loader, tmp } = await setupLoader(); try { const report = await loader.load(tmp); expect(report.ok).toBe(true); expect(registry.ids("sprite")).toEqual(["parsnip-mature"]); const sprite = registry.get("sprite", "parsnip-mature"); expect(sprite).toEqual({ id: "parsnip-mature", frames: [ { name: "idle", durationTicks: 1, layers: [ { shape: "rect", x: 8, y: 8, w: 16, h: 16, fill: "#2e7d32" }, { shape: "circle", x: 16, y: 8, r: 6, fill: "#66bb6a" }, ], }, ], }); } finally { await rm(tmp, { recursive: true, force: true }); } }); it("rejects an invalid sprite def (bad fill, unknown shape, strict unknown keys)", () => { expect( spriteSchema.safeParse({ id: "bad", frames: [{ layers: [{ shape: "rect", x: 0, y: 0, w: 4, h: 4, fill: "green" }] }], }).success, ).toBe(false); expect( spriteSchema.safeParse({ id: "bad", frames: [{ layers: [{ shape: "triangle", x: 0, y: 0, fill: "#ffffff" }] }], }).success, ).toBe(false); expect( spriteSchema.safeParse({ id: "bad", frames: [{ layers: [{ shape: "rect", x: 0, y: 0, w: 4, h: 4, fill: "#ffffff", extra: true }] }], }).success, ).toBe(false); expect(spriteSchema.safeParse({ id: "bad", frames: [] }).success).toBe(false); }); it("reports a bad sprite file as a loader error without crashing siblings", async () => { const { registry, loader, tmp } = await setupLoader(); try { await writeFile(join(tmp, "sprites", "broken.yaml"), "id: broken\nframes: []\n"); const report = await loader.load(tmp); expect(report.ok).toBe(false); const broken = report.items.find((i) => i.id === "broken"); expect(broken?.status).toBe("error"); expect(broken?.errors?.join(" ")).toMatch(/frames/); expect(registry.get("sprite", "parsnip-mature")).toBeDefined(); expect(registry.get("sprite", "broken")).toBeUndefined(); } finally { await rm(tmp, { recursive: true, force: true }); } }); }); describe("sound content kind", () => { it("loads and validates sound YAML from content/sounds/", async () => { const { registry, loader, tmp } = await setupLoader(); try { const report = await loader.load(tmp); expect(report.ok).toBe(true); expect(registry.ids("sound")).toEqual(["coin"]); const sound = registry.get("sound", "coin"); expect(sound).toEqual({ id: "coin", mode: "sfx", synth: { wave: "sine", freq: 880, durationMs: 120, gain: 0.5, slideTo: 1320 }, }); } finally { await rm(tmp, { recursive: true, force: true }); } }); it("rejects an invalid sound def (neither synth nor src, bad wave, strict keys)", () => { expect(soundSchema.safeParse({ id: "silent" }).success).toBe(false); expect(soundSchema.safeParse({ id: "bad", synth: { wave: "noise", freq: 1, durationMs: 1 } }).success).toBe(false); expect( soundSchema.safeParse({ id: "bad", synth: { wave: "sine", freq: 1, durationMs: 1, extra: true } }).success, ).toBe(false); }); it("accepts a src-only def and a synth+src def", () => { expect(soundSchema.safeParse({ id: "file-only", src: "sfx/coin.ogg" }).success).toBe(true); expect( soundSchema.safeParse({ id: "both", synth: { wave: "square", freq: 440, durationMs: 50 }, src: "sfx/coin.ogg", }).success, ).toBe(true); }); });