import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; import type { Song } from "../src/data/types"; beforeAll(() => GlobalRegistrator.register()); afterAll(() => GlobalRegistrator.unregister()); const base: Song = { id: "x", title: "X", sagaId: "troy", track: 1, storyTime: { yearsAfterTroy: 0, label: "t" }, odyssey: null, parts: [{ name: "Verse 1", singers: ["A"], lines: ["l"], motifIds: [] }], facts: ["f"], videos: [{ title: "v1", url: "https://example.com/1" }, { title: "v2", url: "https://example.com/2" }], }; describe("credits", () => { test("FAN_NOTICE has required phrases", async () => { const { FAN_NOTICE } = await import("../src/shared/credits"); for (const p of ["unofficial", "not affiliated", "Jorge Rivera-Herrans"]) expect(FAN_NOTICE).toContain(p); }); test("mountCredits puts the notice in document.body", async () => { const { FAN_NOTICE, mountCredits } = await import("../src/shared/credits"); mountCredits(); expect(document.body.textContent).toContain(FAN_NOTICE); }); test("mountCredits fills a [data-fan-credits] target", async () => { const { FAN_NOTICE, mountCredits } = await import("../src/shared/credits"); document.body.innerHTML = ''; const el = mountCredits(); expect(el.hasAttribute("data-fan-credits")).toBe(true); expect(document.querySelectorAll("[data-fan-credits]")).toHaveLength(1); expect(el.textContent).toContain(FAN_NOTICE); }); }); describe("createAudioSlot", () => { test("createAudioSlot empty state", async () => { const { createAudioSlot } = await import("../src/shared/audio-slot"); const el = createAudioSlot(base); expect(el.classList.contains("audio-slot")).toBe(true); expect(el.classList.contains("audio-slot--empty")).toBe(true); expect(el.querySelector("audio")).toBeNull(); }); test("with audio and startSec renders player and seek button", async () => { const { createAudioSlot } = await import("../src/shared/audio-slot"); const song: Song = { ...base, audio: "/a.mp3", parts: [{ ...base.parts[0]!, startSec: 30 }] }; const el = createAudioSlot(song); expect(el.classList.contains("audio-slot--empty")).toBe(false); expect(el.querySelectorAll("audio[src]")).toHaveLength(1); expect(el.querySelectorAll('button[data-start="30"]')).toHaveLength(1); }); test("seek button sets currentTime and swallows a rejected play()", async () => { const { createAudioSlot } = await import("../src/shared/audio-slot"); const song: Song = { ...base, audio: "/a.mp3", parts: [{ ...base.parts[0]!, startSec: 30 }] }; const el = createAudioSlot(song); const audio = el.querySelector("audio") as HTMLAudioElement; let played = 0; audio.play = () => { played++; return Promise.reject(new Error("blocked")); }; (el.querySelector("button.audio-slot__seek") as HTMLButtonElement).click(); await Promise.resolve(); expect(audio.currentTime).toBe(30); expect(played).toBe(1); }); }); describe("createVideoCards", () => { test("one safe link per video", async () => { const { createVideoCards } = await import("../src/shared/video-cards"); const el = createVideoCards(base); const links = el.querySelectorAll("a.video-card"); expect(links).toHaveLength(2); for (const a of links) { expect(a.getAttribute("rel")).toContain("noopener"); expect(a.getAttribute("target")).toBe("_blank"); } }); });