Files
Epic-The-Musical-Timeline/tests/frieze.test.ts
T
Lucas WintherandClaude Opus 5.5 ac017787b5 feat(app): 3D world, landings and friezes
Nine stylised places on one route, ship and ribbons, the cave, arena and
hall landings, nine saga friezes and the curtain.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:08:11 +01:00

86 lines
3.6 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { sagas, songsInSaga } from "../src/data/album";
import { friezeSvg } from "../src/app/frieze/sagas";
const escapeXml = (s: string) =>
s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]!);
describe("saga friezes", () => {
test.each(sagas.map((s) => [s.id, s] as const))("%s: names the saga and inscribes every song", (_id, saga) => {
const svg = friezeSvg(saga.id);
expect(svg.startsWith("<svg")).toBe(true);
expect(svg).toContain(escapeXml(saga.name));
for (const song of songsInSaga(saga.id)) expect(svg).toContain(escapeXml(song.title));
});
test("titles with apostrophes are escaped, never raw inside markup", () => {
const svg = friezeSvg("wisdom");
expect(svg).toContain("We&#39;ll Be Fine");
expect(svg).not.toContain("We'll Be Fine");
});
test("every saga has its own frieze", () => {
const all = sagas.map((s) => friezeSvg(s.id));
expect(new Set(all).size).toBe(9);
});
test("landing sagas have a doorway to go ashore through; the others do not", () => {
for (const s of sagas) {
const has = friezeSvg(s.id).includes('class="door-fill ');
expect(has).toBe(s.id === "cyclops" || s.id === "wisdom" || s.id === "ithaca");
}
});
});
describe("the curtain (happy-dom, reduced motion: 200ms crossfades)", () => {
const { GlobalRegistrator } = require("@happy-dom/global-registrator") as typeof import("@happy-dom/global-registrator");
let createCurtain: typeof import("../src/app/frieze/curtain").createCurtain;
let createWorld: typeof import("../src/app/world/world").createWorld;
beforeAll(async () => {
GlobalRegistrator.register();
window.matchMedia = ((q: string) => ({ matches: q.includes("reduce"), media: q, addEventListener() {}, removeEventListener() {} })) as unknown as typeof window.matchMedia;
({ createCurtain } = await import("../src/app/frieze/curtain"));
({ createWorld } = await import("../src/app/world/world"));
});
afterAll(() => GlobalRegistrator.unregister());
test("drop shows the saga's frieze; lift clears it", async () => {
const layer = document.createElement("div");
document.body.append(layer);
const c = createCurtain(layer);
await c.drop("cyclops", { landing: "cave" });
const host = layer.querySelector<HTMLElement>(".fz-host")!;
expect(host.hidden).toBe(false);
expect(host.innerHTML).toContain("The Cyclops Saga");
await c.lift();
expect(host.hidden).toBe(true);
expect(Number.isFinite(c.groundY())).toBe(true);
});
test("rapid calls never leave the curtain stuck: the last request wins", async () => {
const layer = document.createElement("div");
const c = createCurtain(layer);
const all = [c.drop("troy"), c.fade(), c.drop("ocean"), c.drop("circe")];
const last = c.lift();
await Promise.all([...all, last]);
expect(layer.querySelector<HTMLElement>(".fz-host")!.hidden).toBe(true);
expect(layer.querySelector<HTMLElement>(".fz-cover")!.hidden).toBe(true);
});
test("fade covers the stage in the curtain colour; lift uncovers it", async () => {
const layer = document.createElement("div");
const c = createCurtain(layer);
await c.fade();
const cover = layer.querySelector<HTMLElement>(".fz-cover")!;
expect(cover.hidden).toBe(false);
await c.lift();
expect(cover.hidden).toBe(true);
});
test("createWorld returns null without WebGL", () => {
const canvas = document.createElement("canvas");
canvas.getContext = (() => null) as typeof canvas.getContext;
expect(createWorld(canvas)).toBeNull();
});
});