commit 4fbd9ed6ef40ded8de52f9ec93ec7c2553a70ce4
parent 7e34cc17dedda9f9843e3489bebaefaf325327ca
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sun, 20 Sep 2026 17:45:48 -0400
e2e: assert the walk where the fixture holds still
The whole-cut walk test moves onto walk-fixture, which can state what the walk
skips and why: w01 to w04 by link and by key, over one clip with nothing to
play and one already judged; both of those still open by URL and say which
they are; ready 2 of 3 on the project page and in the bench header; and y
advancing over the judged one.
The two bench-fixture tests that walk now say what state they need out loud.
c04 is the only other clip there the walk can visit, so whether it carries a
verdict decided where they landed -- inherited from whichever test ran first.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
1 file changed, 112 insertions(+), 18 deletions(-)
diff --git a/umtool/e2e/clip-bench.spec.ts b/umtool/e2e/clip-bench.spec.ts
@@ -36,11 +36,16 @@ const FIXTURE = path.join(UMTOOL, ".e2e-song");
// index and decision specs assert what report-fixture's windows are -- sharing
// one project made the suite pass or fail on which spec file ran first.
const PROJECT = "reports/bench-fixture";
+// The WALK's fixture, and read-only: see the block below, and the comment on
+// walk-fixture in make-fixture.mjs.
+const WALK = "reports/walk-fixture";
const MANIFEST = path.join(FIXTURE, "reports", "bench-fixture", "video.manifest.json");
const bench = (clip: string) => `/browse/${PROJECT}/clip/${clip}`;
+const walkBench = (clip: string) => `/browse/${WALK}/clip/${clip}`;
-const readClip = (id: string) => {
- const m = JSON.parse(readFileSync(MANIFEST, "utf8")) as {
+const readClipIn = (project: string, id: string) => {
+ const file = path.join(FIXTURE, ...project.split("/"), "video.manifest.json");
+ const m = JSON.parse(readFileSync(file, "utf8")) as {
timeline: {
id: string;
start: number;
@@ -56,6 +61,7 @@ const readClip = (id: string) => {
};
return m.timeline.find((e) => e.id === id)!;
};
+const readClip = (id: string) => readClipIn(PROJECT, id);
/**
* Type into an attribution field and let it save the way a blur does.
@@ -100,8 +106,12 @@ const keyboardLive = async (page: import("@playwright/test").Page) => {
await box.blur();
};
-const token = async (request: { get: (u: string) => Promise<{ json: () => Promise<unknown> }> }, clip: string) => {
- const r = await request.get(`/api/report/clip?project=${encodeURIComponent(PROJECT)}&clip=${clip}`);
+const token = async (
+ request: { get: (u: string) => Promise<{ json: () => Promise<unknown> }> },
+ clip: string,
+ project = PROJECT,
+) => {
+ const r = await request.get(`/api/report/clip?project=${encodeURIComponent(project)}&clip=${clip}`);
return (await r.json()) as { token: string };
};
@@ -337,23 +347,92 @@ test("a date that is not a real day is refused, and the bench says so", async ({
await expect(page.locator("[data-attrib-field=date]")).toHaveValue("2025-02-31");
});
-test("prev and next walk the cut, by link and by key", async ({ page }) => {
- await page.goto(bench("c02"));
+// ---------------------------------------------------------------------------
+// The walk, and the two things it skips.
+//
+// Reviewing a cut is watching every clip in order and answering one question
+// about each, so the walk goes to the clips that still NEED an answer and can
+// be WATCHED today. A clip with nothing cached is a dead end -- there is
+// nothing to judge and the only move is to press `n` again -- and one already
+// answered is the round trip the walk exists to remove.
+//
+// These run against reports/walk-fixture, which is read-only for exactly this
+// reason: every other test in this file writes verdicts into bench-fixture, so
+// "ready 2 of 3" would be true there only until one of them ran. Its four
+// clips are w01 (fetched, unjudged), w02 (nothing holds 9-12), w03 (fetched,
+// already confirmed) and w04 (fetched, unjudged).
+// ---------------------------------------------------------------------------
+
+test("the walk visits only the clips that need judgement and are fetched", async ({ page }) => {
+ await page.goto(walkBench("w01"));
+ // w02 has nothing to play and w03 has already been answered, so the one move
+ // forward from w01 is w04 -- named in the link, not just arrived at.
+ await expect(page.locator("[data-clip-nav=next]")).toHaveText(/w04/);
await page.locator("[data-clip-nav=next]").click();
- await expect(page.locator("[data-bench=c03]")).toBeVisible();
+ await expect(page.locator("[data-bench=w04]")).toBeVisible();
- // The same move from the keyboard. Reviewing a cut is watching every clip in
- // order, and the project page in between is a round trip to re-find your place.
+ // The same move from the keyboard, backwards over the same two clips.
+ await keyboardLive(page);
await page.locator("body").press("p");
- await expect(page.locator("[data-bench=c02]")).toBeVisible();
+ await expect(page.locator("[data-bench=w01]")).toBeVisible();
- // The ends say so rather than offering a link into nothing.
- await page.goto(bench("c01"));
+ // The ends of the WALK, which are not the ends of the cut.
await expect(page.locator("[data-clip-nav=prev]")).toHaveCount(0);
- await page.goto(bench("c04"));
+ await page.goto(walkBench("w04"));
await expect(page.locator("[data-clip-nav=next]")).toHaveCount(0);
});
+test("a clip off the walk still opens by URL, and says why it is off it", async ({ page }) => {
+ // Nothing fetched: it renders, it says so, and its links point at the
+ // neighbours on either side rather than at nothing.
+ await page.goto(walkBench("w02"));
+ await expect(page.locator("[data-bench=w02]")).toBeVisible();
+ await expect(page.locator("[data-clip-unfetched]")).toBeVisible();
+ await expect(page.getByText("nothing fetched for this clip yet")).toBeVisible();
+ await expect(page.locator("[data-clip-nav=prev]")).toHaveText(/w01/);
+ await expect(page.locator("[data-clip-nav=next]")).toHaveText(/w04/);
+
+ // Already judged: it opens too, with what the walk said about it, and it is
+ // fetched -- so no pill.
+ await page.goto(walkBench("w03"));
+ await expect(page.locator("[data-bench=w03]")).toHaveAttribute("data-verdict", "confirmed");
+ await expect(page.locator("[data-clip-unfetched]")).toHaveCount(0);
+});
+
+test("`ready N of M` counts the fetched clips that still need judgement", async ({ page }) => {
+ await page.goto(`/browse/${WALK}`);
+ await expect(page.locator("[data-ready-count]")).toHaveText("ready 2 of 3 needing judgement");
+ // The pill is the same question the walk asks, per row.
+ await expect(page.locator("[data-entry=w02]")).toHaveAttribute("data-fetched", "0");
+ await expect(page.locator("[data-entry=w02]")).toContainText("not fetched yet");
+ await expect(page.locator("[data-entry=w01]")).toHaveAttribute("data-fetched", "1");
+ // And the walk starts where the walk actually goes.
+ await expect(page.locator("[data-walk-start=w01]")).toHaveAttribute(
+ "href",
+ `/browse/${WALK}/clip/w01`,
+ );
+
+ await page.goto(walkBench("w01"));
+ await expect(page.locator("[data-ready-count]")).toHaveText("ready 2 of 3 needing judgement");
+});
+
+test("`y` walks on OVER a clip that has already been judged", async ({ page, request }) => {
+ await page.goto(walkBench("w01"));
+ await keyboardLive(page);
+ await page.locator("body").press("y");
+ // Not w02 (nothing to play) and not w03 (answered in the fixture): w04.
+ await expect(page.locator("[data-bench=w04]")).toBeVisible();
+ await expect.poll(() => readClipIn(WALK, "w01").verdict).toBe("confirmed");
+
+ // Put it back. This is the ONE test that writes to walk-fixture, and the
+ // counter above is only stable because it does.
+ const { token: t } = await token(request, "w01", WALK);
+ await request.put("/api/report/window", {
+ data: { project: WALK, clip: "w01", verdict: "", token: t },
+ });
+ await expect.poll(() => readClipIn(WALK, "w01").verdict).toBeUndefined();
+});
+
test("the segment route serves a built clip with ranges, and 404s when there is none", async ({
page,
request,
@@ -509,13 +588,22 @@ test("the bench says what the clip is supposed to be", async ({ page }) => {
});
test("`y` confirms the clip and walks on; the manifest says so", async ({ page, request }) => {
+ // c04 is the only OTHER clip here the walk can visit -- c02 and c03 have
+ // nothing cached that holds them -- so the state it is in decides where this
+ // lands. Said out loud rather than inherited from whichever test ran before.
+ const { token: t0 } = await token(request, "c04");
+ await request.put("/api/report/window", {
+ data: { project: PROJECT, clip: "c04", verdict: "", token: t0 },
+ });
+
await page.goto(bench("c01"));
await keyboardLive(page);
await page.locator("body").press("y");
- // Confirming ADVANCES: in the yes case the next clip is what you want, and
- // a walk of sixty clips is one finger.
- await expect(page.locator("[data-bench=c02]")).toBeVisible();
+ // Confirming ADVANCES: in the yes case the next clip you can judge is what
+ // you want, and a walk of sixty clips is one finger. Over c02 and c03, which
+ // are not fetched, to c04.
+ await expect(page.locator("[data-bench=c04]")).toBeVisible();
await expect.poll(() => readClip("c01").verdict).toBe("confirmed");
await page.goto(bench("c01"));
@@ -698,8 +786,14 @@ test("the playback speed is this browser's, and it survives a reload", async ({
.toBe(1.5);
});
-test("auto-audition plays the clip you walk onto", async ({ page }) => {
- // c03 -> c04, because c04 is the neighbour with cached material.
+test("auto-audition plays the clip you walk onto", async ({ page, request }) => {
+ // c03 -> c04: c03 itself has nothing cached (it is reached here by URL, which
+ // still works), and c04 is the next clip the walk can visit. Its verdict is
+ // set explicitly, because an answered clip is not one the walk goes to.
+ const { token: t } = await token(request, "c04");
+ await request.put("/api/report/window", {
+ data: { project: PROJECT, clip: "c04", verdict: "", token: t },
+ });
await page.goto(bench("c03"));
await page.locator("[data-auto-audition=off]").click();
await expect(page.locator("[data-auto-audition=on]")).toBeVisible();