import { describe, expect, it } from "vitest"; import { createPlaytestSession, keydownInput, saveAllFlushes } from "../src/renderer/playtest-session.js"; describe("saveAllFlushes", () => { it("writes clean flushes and reports them as flushed", async () => { const written: string[] = []; const result = await saveAllFlushes( [ { path: "a.yaml", content: "x" }, { path: "b.yaml", content: "y" }, ], async (path, content) => { written.push(`${path}:${content}`); }, ); expect(result.flushed).toEqual(["a.yaml", "b.yaml"]); expect(result.failures).toEqual([]); expect(written).toEqual(["a.yaml:x", "b.yaml:y"]); }); it("records error flushes and rejecting writes as failures without throwing", async () => { const result = await saveAllFlushes( [ { path: "bad.yaml", error: "map invalid" }, { path: "throws.yaml", content: "z" }, { path: "ok.yaml", content: "ok" }, ], async (path) => { if (path === "throws.yaml") throw new Error("disk full"); }, ); expect(result.flushed).toEqual(["ok.yaml"]); expect(result.failures).toEqual([ { path: "bad.yaml", error: "map invalid" }, { path: "throws.yaml", error: "disk full" }, ]); }); it("writes nothing when there are no flushes", async () => { const result = await saveAllFlushes([], async () => { throw new Error("unreachable"); }); expect(result.flushed).toEqual([]); expect(result.failures).toEqual([]); }); }); describe("keydownInput", () => { it("maps arrow keys to raw inputs", () => { expect(keydownInput("ArrowUp")).toBe("key:ArrowUp"); expect(keydownInput("ArrowDown")).toBe("key:ArrowDown"); expect(keydownInput("ArrowLeft")).toBe("key:ArrowLeft"); expect(keydownInput("ArrowRight")).toBe("key:ArrowRight"); }); it("returns undefined for non-arrow keys", () => { expect(keydownInput("a")).toBeUndefined(); expect(keydownInput("Enter")).toBeUndefined(); expect(keydownInput(" ")).toBeUndefined(); }); }); describe("createPlaytestSession", () => { it("runs save-all → compile → boot → playing, then stop tears down", async () => { const phases: string[] = []; let booted: unknown = null; let tornDown = false; const session = createPlaytestSession({ collectFlushes: () => [{ path: "m.yaml", content: "m" }], write: async () => {}, compile: async () => ({ format: "nengine-bundle", version: 1, content: {} }), boot: async (bundle) => { booted = bundle; }, teardown: async () => { tornDown = true; }, onPhase: (phase) => phases.push(phase), }); const result = await session.start(); expect(result.ok).toBe(true); expect(result.saved).toEqual(["m.yaml"]); expect(booted).toEqual({ format: "nengine-bundle", version: 1, content: {} }); expect(session.phase).toBe("playing"); expect(phases).toEqual(["saving", "compiling", "booting", "playing"]); await session.stop(); expect(tornDown).toBe(true); expect(session.phase).toBe("idle"); expect(phases[phases.length - 1]).toBe("idle"); }); it("fails on an un-serializable model and never compiles", async () => { let compiled = false; const session = createPlaytestSession({ collectFlushes: () => [{ path: "bad.yaml", error: "map invalid" }], write: async () => {}, compile: async () => { compiled = true; throw new Error("nope"); }, boot: async () => {}, teardown: async () => {}, }); const result = await session.start(); expect(result.ok).toBe(false); expect(result.saved).toEqual([]); expect(result.error).toContain("bad.yaml"); expect(result.error).toContain("map invalid"); expect(compiled).toBe(false); expect(session.phase).toBe("idle"); }); it("fails on a write rejection", async () => { const session = createPlaytestSession({ collectFlushes: () => [{ path: "a.yaml", content: "a" }], write: async () => { throw new Error("disk full"); }, compile: async () => { throw new Error("unreachable"); }, boot: async () => {}, teardown: async () => {}, }); const result = await session.start(); expect(result.ok).toBe(false); expect(result.saved).toEqual([]); expect(result.error).toContain("disk full"); }); it("fails on a compile error with the compile message", async () => { let booted = false; const session = createPlaytestSession({ collectFlushes: () => [], write: async () => {}, compile: async () => { throw new Error("content validation failed"); }, boot: async () => { booted = true; }, teardown: async () => {}, }); const result = await session.start(); expect(result.ok).toBe(false); expect(result.error).toContain("content validation failed"); expect(booted).toBe(false); expect(session.phase).toBe("idle"); }); it("fails on a boot error with the boot message", async () => { const session = createPlaytestSession({ collectFlushes: () => [], write: async () => {}, compile: async () => ({ format: "nengine-bundle", version: 1, content: {} }), boot: async () => { throw new Error("no map content loaded"); }, teardown: async () => {}, }); const result = await session.start(); expect(result.ok).toBe(false); expect(result.error).toContain("no map content loaded"); expect(session.phase).toBe("idle"); }); it("rejects start while already running", async () => { let resolveBoot: (() => void) | undefined; const session = createPlaytestSession({ collectFlushes: () => [], write: async () => {}, compile: async () => ({ format: "nengine-bundle", version: 1, content: {} }), boot: async () => { await new Promise((resolve) => { resolveBoot = resolve; }); }, teardown: async () => {}, }); const first = session.start(); await Promise.resolve(); const second = await session.start(); expect(second.ok).toBe(false); expect(second.error).toBe("playtest already running"); resolveBoot?.(); await first; expect(session.phase).toBe("playing"); }); it("stop is a no-op when idle", async () => { let tornDown = false; const session = createPlaytestSession({ collectFlushes: () => [], write: async () => {}, compile: async () => ({ format: "nengine-bundle", version: 1, content: {} }), boot: async () => {}, teardown: async () => { tornDown = true; }, }); await session.stop(); expect(tornDown).toBe(false); expect(session.phase).toBe("idle"); }); });