import { test, expect } from "@playwright/test"; import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; // --------------------------------------------------------------------------- // Takes: alternative renders of part of a cut, side by side, judged. // // takes-fixture WRITES takes/verdicts.json. Two groups, a take with no // preview yet, one skipped take.json, a work dir that is // not a take (make-fixture.mjs says which is which) // report-fixture no takes/ at all -- the empty state // --------------------------------------------------------------------------- const HERE = path.dirname(fileURLToPath(import.meta.url)); const PROJECT_DIR = path.join(HERE, "..", ".e2e-song", "reports", "takes-fixture"); const VERDICTS = path.join(PROJECT_DIR, "takes", "verdicts.json"); const PAGE = "/browse/reports/takes-fixture/takes"; test("a project with no takes/ says so, and its page has no takes link", async ({ page }) => { const res = await page.goto("/browse/reports/report-fixture/takes"); expect(res?.status()).toBe(200); await expect(page.locator("[data-takes-empty]")).toContainText("No takes yet"); await page.goto("/browse/reports/report-fixture"); await expect(page.locator("[data-takes-link]")).toHaveCount(0); }); test("takes are grouped and ordered; the skipped one says why; a work dir is not a take", async ({ page }) => { await page.goto("/browse/reports/takes-fixture"); const link = page.locator("[data-takes-link]"); await expect(link).toHaveAttribute("data-takes-link", "4"); await link.click(); await expect(page).toHaveURL(new RegExp(`${PAGE}$`)); const groups = page.locator("[data-group]"); await expect(groups).toHaveCount(2); await expect(groups.nth(0)).toHaveAttribute("data-group", "finale"); await expect(groups.nth(1)).toHaveAttribute("data-group", "opening"); const finale = page.locator("[data-group='finale'] [data-take]"); expect(await finale.evaluateAll((els) => els.map((e) => e.getAttribute("data-take")))).toEqual([ "ref", "slow", "hard-cut", ]); await expect(page.locator("[data-take='ref']")).toHaveAttribute("data-kind", "reference"); await expect(page.locator("[data-take='slow']")).toContainText("beat 1.35 s (was 1.05)"); await expect(page.locator("[data-take='hard-cut'] [data-no-preview]")).toBeVisible(); await expect(page.locator("[data-take='hard-cut'] video")).toHaveCount(0); await expect(page.locator("[data-skipped='bad-kind']")).toContainText("kind must be one of"); await expect(page.locator("[data-skipped='current']")).toHaveCount(0); await expect(page.locator("[data-take='current']")).toHaveCount(0); }); test("a preview is served with byte ranges, and only for a listed take", async ({ request }) => { const q = (take: string) => `/api/report/takes/preview?project=reports/takes-fixture&take=${take}`; const whole = await request.get(q("ref")); expect(whole.status()).toBe(200); expect(whole.headers()["content-type"]).toBe("video/mp4"); const size = Number(whole.headers()["content-length"]); expect(size).toBeGreaterThan(100); const part = await request.get(q("ref"), { headers: { range: "bytes=0-99" } }); expect(part.status()).toBe(206); expect(part.headers()["content-range"]).toBe(`bytes 0-99/${size}`); expect((await part.body()).length).toBe(100); expect((await request.get(q("bad-kind"))).status()).toBe(404); expect((await request.get(q("current"))).status()).toBe(404); expect((await request.get(q("hard-cut"))).status()).toBe(404); expect((await request.get(q("..%2Fref"))).status()).toBe(400); }); test("verdicts and notes land in takes/verdicts.json in the agreed shape", async ({ page, request }) => { await page.goto(PAGE); const slow = page.locator("[data-take='slow']"); const like = slow.locator("[data-verdict-button='like']"); await expect(like).toBeEnabled(); await like.click(); await expect(slow).toHaveAttribute("data-verdict", "like"); const note = slow.locator("[data-take-note]"); await expect(async () => { await note.fill("the wait is right"); await expect(note).toHaveValue("the wait is right"); }).toPass(); await note.press("Enter"); await expect .poll(() => { try { return JSON.parse(readFileSync(VERDICTS, "utf8")).slow?.note; } catch { return null; } }) .toBe("the wait is right"); const onDisk = JSON.parse(readFileSync(VERDICTS, "utf8")); expect(Object.keys(onDisk.slow).sort()).toEqual(["at", "note", "verdict"]); expect(onDisk.slow.verdict).toBe("like"); // Survives a reload; pressing the lit verdict again clears it but keeps the note. await page.reload(); await expect(slow).toHaveAttribute("data-verdict", "like"); await expect(note).toHaveValue("the wait is right"); await slow.locator("[data-verdict-button='like']").click(); await expect(slow).toHaveAttribute("data-verdict", ""); await expect.poll(() => JSON.parse(readFileSync(VERDICTS, "utf8")).slow?.verdict).toBeNull(); // A verdict on a take that is not listed is refused, not written. const bad = await request.post("/api/report/takes/verdict", { data: { project: "reports/takes-fixture", take: "bad-kind", verdict: "like" }, }); expect(bad.status()).toBe(404); expect(JSON.parse(readFileSync(VERDICTS, "utf8"))["bad-kind"]).toBeUndefined(); }); test("listen picks the one audio source; play all starts the group from zero", async ({ page }) => { await page.goto(PAGE); const muted = (id: string) => page.locator(`[data-take='${id}'] video`).evaluate((v: HTMLVideoElement) => v.muted); await page.locator("[data-take='slow'] [data-action='listen']").click(); await expect(page.locator("[data-take='slow'] [data-action='listen']")).toHaveAttribute("aria-pressed", "true"); await expect.poll(() => muted("slow")).toBe(false); expect(await muted("ref")).toBe(true); expect(await muted("intro-a")).toBe(true); await page.locator("[data-take='ref'] [data-action='listen']").click(); await expect.poll(() => muted("ref")).toBe(false); expect(await muted("slow")).toBe(true); await page.locator("[data-group='finale'] [data-action='play-all']").click(); const playing = (id: string) => page.locator(`[data-take='${id}'] video`).evaluate((v: HTMLVideoElement) => !v.paused && v.currentTime > 0); await expect.poll(() => playing("ref")).toBe(true); await expect.poll(() => playing("slow")).toBe(true); // The other group was not started, and play all did not move the audio. expect(await playing("intro-a")).toBe(false); expect(await muted("ref")).toBe(false); expect(await muted("slow")).toBe(true); });