evOo-local/apps/web/e2e/media-review.spec.ts
Claude bab398cb52
wip(media): editor e2e and player fixes (unverified)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLSCTnhge4umMnemGXWr1w
2026-10-03 19:29:59 +00:00

118 lines
5.6 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { mediaBackend, PROJECT_ID } from "./fixtures/media-editor-data";
import { captureScreenshot } from "./helpers";
const reviewUrl = (renderId: string) =>
`/e2e/fixtures/media-editor.html?view=review&projectId=${PROJECT_ID}&renderId=${renderId}`;
test("review lists gate blockers that jump the player and shows coverage and auto-fix", async ({
page,
}, testInfo) => {
const backend = mediaBackend();
await backend.install(page);
await page.goto(reviewUrl("render-paid-draft"));
const review = page.getByTestId("media-review");
await expect(review.getByTestId("media-gate-status")).toHaveAttribute("data-status", "blocked");
await expect(review.getByTestId("media-autofix")).toContainText("Auto-fix 1/3");
await expect(review.getByTestId("media-autofix")).toContainText("widened the face mask");
// Issues in timeline order, untimed last.
const issues = review.getByTestId("media-gate-issue");
await expect(issues).toHaveCount(5);
await expect(issues.nth(0)).toContainText("0:02.0");
await expect(issues.nth(0)).toContainText("Explicit nudity above the profile limit.");
await expect(issues.nth(1)).toContainText("0:05.0");
await expect(issues.nth(4)).toContainText("Fewer hashtags would read better.");
await expect(issues.nth(4).getByRole("button")).toBeDisabled();
// A blocked render has no output; the plan plays from the sources.
const player = review.getByTestId("media-source-player");
await expect(review.getByRole("button", { name: "After" })).toHaveCount(0);
await issues.nth(1).getByRole("button").click();
await expect(player).toHaveAttribute("data-position-ms", "5000");
await issues.nth(2).getByRole("button").click();
await expect(player).toHaveAttribute("data-position-ms", "9000");
// Coverage per person and mark; a segment seeks too.
const coverage = review.getByTestId("media-coverage");
await expect(coverage.getByText("Alex")).toBeVisible();
await expect(coverage.getByText("Sam")).toBeVisible();
await expect(coverage.getByText("Tattoo")).toBeVisible();
await coverage.getByRole("button", { name: /0:10\.0/ }).click();
await expect(player).toHaveAttribute("data-position-ms", "10000");
await captureScreenshot(page, testInfo, "media-review");
});
test("review edits people, marks, and masks, then re-renders the draft", async ({
page,
}, testInfo) => {
const backend = mediaBackend();
await backend.install(page);
await page.goto(reviewUrl("render-paid-draft"));
const review = page.getByTestId("media-review");
await expect(review.getByTestId("media-gate-issue")).toHaveCount(5);
// Per person: anonymization mode; consent only where the gate asks for it.
const people = review.getByTestId("media-review-person");
await expect(people).toHaveCount(2);
await review.getByLabel("Anonymization for Alex").selectOption("pixelate");
await expect
.poll(() => backend.calls.find((call) => call.procedure === "people/update")?.input)
.toEqual({ personId: "person-a", anonymizeByDefault: "pixelate" });
await expect(people.nth(0).getByRole("button", { name: "Record consent" })).toHaveCount(0);
await expect(people.nth(1).getByRole("button", { name: "Record consent" })).toBeVisible();
// Per mark: a low quality score offers the suggested fallback.
const mark = review.getByTestId("media-review-mark");
await expect(mark).toContainText("42%");
await mark.getByRole("button", { name: "Use Cover" }).click();
await expect(mark.getByLabel("Treatment")).toHaveValue("cover");
await expect(mark.getByRole("button", { name: "Use Cover" })).toHaveCount(0);
// Mark a spot on the frame.
await review.getByRole("button", { name: "Mark", exact: true }).click();
await review.getByRole("button", { name: "Mark this spot" }).click({ position: { x: 20, y: 20 } });
await expect
.poll(() => backend.calls.find((call) => call.procedure === "marks/create")?.input)
.toMatchObject({ kind: "custom", assetId: "asset-1", treatment: "blur" });
// Draw a mask and drag it.
await review.getByRole("button", { name: "Mask", exact: true }).click();
const mask = review.getByTestId("media-mask");
await expect(mask).toBeVisible();
const box = await mask.boundingBox();
if (!box) throw new Error("mask not laid out");
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 2 + 40, box.y + box.height / 2 + 10, { steps: 4 });
await page.mouse.up();
await captureScreenshot(page, testInfo, "media-review-mask");
await review.getByRole("button", { name: "Save" }).click();
await expect(review.getByRole("button", { name: "Draft" })).toBeVisible();
const submitted = backend.calls.find((call) => call.procedure === "plans/submit")?.input as {
edl: {
redactions: {
regions: Array<{ clipId: string; keyframes: Array<{ box: { x: number } }> }>;
};
};
};
const region = submitted.edl.redactions.regions[0];
expect(region?.clipId).toBe("clip-1");
expect(region?.keyframes[0]?.box.x).toBeGreaterThan(0.4);
// Re-rendering opens the new render, which polls while it runs.
backend.holdRenders(1);
await review.getByRole("button", { name: "Draft" }).click();
await expect(review.getByText(/Rendering \d+%/)).toBeVisible();
await expect(review.getByTestId("media-gate-status")).toHaveAttribute("data-status", "pass", {
timeout: 15_000,
});
// Consent is recorded on the people screen, never here.
await page.goto(reviewUrl("render-paid-draft"));
await page.getByRole("button", { name: "Record consent" }).click();
await expect(page.getByTestId("media-other-view")).toHaveAttribute("data-view", "people");
});