import { test, expect } from "@playwright/test"; import { caps, NO_SONG_DATA } from "./capabilities"; // THE WHOLE FILE IS ABOUT PLAN FILES, and every plan in the fixture is // candidate-derived: make-fixture writes `fixture-build.plan.json` from the // first COPIED cand2 entry, and it copies one only when its wav is present. // With no song bulk data the index is empty and every assertion here is // about a placement that was never written. test.skip(!caps.song, NO_SONG_DATA); // "Used in" — and the confirmation before dropping something that is. // // A verdict here can change a deliverable: dropping a clip that is placed in a // rendered plan means the next arrange cannot pick it, so a video that exists // stops being reproducible from the palette that made it. Nothing said so. // // The fixture writes FOUR plan-shaped files naming the first candidate, so both // halves are testable: a clip that is used, and clips that are not. Three of // them are the walk fix: `videos/alpha/plan/` is three directories down, and // `alpha-body.json` does not carry the `.plan.json` suffix -- between them that // is the exact shape the index used to miss, and missing it meant a live clip // could be dropped with no warning at all. async function plannedKey(request: import("@playwright/test").APIRequestContext) { const list = await (await request.get("/api/clips?mode=sort&limit=200")).json(); for (const c of list.clips as { key: string }[]) { const u = await (await request.get(`/api/usage?key=${encodeURIComponent(c.key)}`)).json(); if (u.uses.length) return { key: c.key, uses: u.uses, builds: u.builds }; } return null; } test("the index is built from the plan files", async ({ request }) => { const r = await request.get("/api/usage"); expect(r.ok()).toBe(true); const j = await r.json(); expect(j.builds).toBeGreaterThan(0); expect(j.clips).toBeGreaterThan(0); }); test("a planned clip reports its build and every placement", async ({ request }) => { const hit = await plannedKey(request); expect(hit, "the fixture writes a plan naming a reachable candidate").not.toBeNull(); expect(hit!.builds).toContain("fixture-build"); // fixture-build places it twice, so placements outnumber builds -- which is // the common case: 1,335 of 1,898 clips in the real reports directory are // used in more than one build. expect(hit!.uses.length).toBe(5); expect(hit!.builds.length).toBe(4); }); // THE regression this fix is for. Both of these are inside `videos//plan/` // -- one level deeper than the walk used to go -- and `alpha-body` has no // `.plan.json` suffix, so the old suffix filter would have skipped it even at // the right depth. Measured on the real tree, that combination hid 12 of the // 271 notes in a shipped song's plan behind "used in NO build". test("a plan three directories down, without the suffix, still counts", async ({ request }) => { const hit = await plannedKey(request); expect(hit!.builds).toContain("videos/alpha/plan/alpha-body"); expect(hit!.builds).toContain("videos/alpha/plan/alpha"); }); // The widening is scoped by SHAPE, not by name: plan/ also holds bare arrays of // overlay descriptions, and those must not be parsed as arrangements. test("a bare array in plan/ is not a plan", async ({ request }) => { const hit = await plannedKey(request); expect(hit!.builds).not.toContain("videos/alpha/plan/overlays"); }); test("an unplanned clip reports nothing, so the confirm cannot fire on it", async ({ request }) => { const list = await (await request.get("/api/clips?mode=sort&limit=200")).json(); const hit = await plannedKey(request); const other = (list.clips as { key: string }[]).find((c) => c.key !== hit?.key); const u = await (await request.get(`/api/usage?key=${encodeURIComponent(other!.key)}`)).json(); expect(u.uses).toEqual([]); }); test("dropping a clip that is in a build asks first, and Esc keeps it", async ({ page, request }) => { const hit = await plannedKey(request); test.skip(!hit, "no planned clip in the fixture"); // ?only= reaches a named clip whatever pile it is in. await page.goto(`/sort?only=${encodeURIComponent(hit!.key)}`); // Derived rather than pinned: the count is the fixture's, and pinning it here // would break every time the fixture grows a plan. const n = hit!.builds.length; await expect(page.getByText(new RegExp(`used in ${n} builds?`))).toBeVisible(); const before = await (await request.get("/api/clips?mode=sort&limit=50")).json(); // "not an um" -> reasons -> a code. The confirm must appear INSTEAD of // writing, and the verdict row must be gone while it is up. await page.locator('[data-verdict="0"]').click(); await page.locator("[data-code]").first().click(); await expect(page.getByRole("button", { name: /drop it anyway/ })).toBeVisible(); await expect(page.getByText(new RegExp(`sounding in ${n} finished builds?`))).toBeVisible(); await expect(page.locator('[data-verdict="0"]')).toBeHidden(); // Nothing was written while the question was open. const during = await (await request.get("/api/clips?mode=sort&limit=50")).json(); expect(during.counts.rejected).toBe(before.counts.rejected); await page.keyboard.press("Escape"); await expect(page.getByRole("button", { name: /drop it anyway/ })).toHaveCount(0); await expect(page.locator('[data-verdict="0"]')).toBeVisible(); const after = await (await request.get("/api/clips?mode=sort&limit=50")).json(); expect(after.counts.rejected).toBe(before.counts.rejected); }); test("confirming the drop writes it through", async ({ page, request }) => { const hit = await plannedKey(request); test.skip(!hit, "no planned clip in the fixture"); await page.goto(`/sort?only=${encodeURIComponent(hit!.key)}`); const before = await (await request.get("/api/clips?mode=sort&limit=50")).json(); await page.locator('[data-verdict="0"]').click(); await page.locator("[data-code]").first().click(); await page.locator("[data-confirm-drop]").click(); await expect .poll(async () => (await (await request.get("/api/clips?mode=sort&limit=50")).json()).counts.rejected) .toBe(before.counts.rejected + 1); });