feat(app): voyage transitions and the stage controller
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b3385185b5
commit
27d624fa44
3 files changed
+234
No files matched your search
@@ -0,0 +1,82 @@
|
||||
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<void>((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);
|
||||
});
|
||||
Reference in new issue
Block a user