import { expect, test } from "bun:test"; import { transitionFor } from "../src/app/voyage"; import { createController, type ControllerWorld, type ControllerCurtain } from "../src/app/controller"; import { createStore, DEFAULT_STATE } from "../src/app/state"; test("within a saga the ship sails, and landing songs land", () => { expect(transitionFor("polyphemus", "survive")).toEqual({ transition: { kind: "sail" }, landing: "cave" }); expect(transitionFor("survive", "remember-them")).toEqual({ transition: { kind: "sail" }, landing: null }); }); test("into the next or previous saga the curtain drops with that saga's frieze", () => { expect(transitionFor("warrior-of-the-mind", "polyphemus")).toEqual({ transition: { kind: "curtain", sagaId: "cyclops" }, landing: "cave" }); expect(transitionFor("polyphemus", "warrior-of-the-mind")).toEqual({ transition: { kind: "curtain", sagaId: "troy" }, landing: null }); }); test("jumps across two or more sagas fade", () => { expect(transitionFor("storm", "god-games")).toEqual({ transition: { kind: "fade" }, landing: "arena" }); }); test("the same song is a no-op sail", () => { expect(transitionFor("storm", "storm")).toEqual({ transition: { kind: "sail" }, landing: null }); }); function fakes() { const log: string[] = []; let release: (() => void)[] = []; const later = (what: string) => new Promise((r) => { log.push(what); release.push(r); }); const world: ControllerWorld = { sailTo: (id) => later(`sail:${id}`), setLanding: (id) => later(`land:${id}`), setRibbons: () => {}, }; const curtain: ControllerCurtain = { drop: (saga) => later(`drop:${saga}`), lift: () => later("lift"), fade: () => later("fade"), }; const flush = async () => { for (let i = 0; i < 50; i++) { const r = release; release = []; r.forEach((f) => f()); await Promise.resolve(); await Promise.resolve(); } }; return { log, world, curtain, flush }; } test("rapid navigation ends at the last requested song with the curtain lifted", async () => { const f = fakes(); const store = createStore(DEFAULT_STATE, { history: false }); const ctl = createController({ store, world: f.world, curtain: f.curtain, reducedMotion: false }); for (const id of ["just-a-man", "polyphemus", "storm", "puppeteer", "god-games"]) store.set({ songId: id }); await f.flush(); expect(ctl.settledSongId()).toBe("god-games"); const lastSail = f.log.filter((l) => l.startsWith("sail:")).at(-1); expect(lastSail).toBe("sail:god-games"); const drops = f.log.filter((l) => l.startsWith("drop:") || l === "fade").length; const lifts = f.log.filter((l) => l === "lift").length; expect(lifts).toBeGreaterThanOrEqual(drops === 0 ? 0 : 1); expect(f.log.at(-1)).toBe("land:arena"); expect(ctl.busy()).toBe(false); }); test("without a world the controller still follows the songs (frieze-only browsing)", async () => { const f = fakes(); const store = createStore(DEFAULT_STATE, { history: false }); const ctl = createController({ store, world: null, curtain: f.curtain, reducedMotion: false }); store.set({ songId: "polyphemus" }); await f.flush(); expect(ctl.settledSongId()).toBe("polyphemus"); expect(f.log).toContain("drop:cyclops"); }); test("reduced motion turns every scene change into a fade", async () => { const f = fakes(); const store = createStore(DEFAULT_STATE, { history: false }); createController({ store, world: f.world, curtain: f.curtain, reducedMotion: true }); store.set({ songId: "polyphemus" }); await f.flush(); expect(f.log).toContain("fade"); expect(f.log.some((l) => l.startsWith("drop:"))).toBe(false); });