Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLSCTnhge4umMnemGXWr1w
145 lines
7.3 KiB
TypeScript
145 lines
7.3 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import type { edl } from "./fixtures/media-editor-data";
|
|
import { mediaBackend, PAID, PROJECT_ID, SHORTS, TIKTOK } from "./fixtures/media-editor-data";
|
|
import { captureScreenshot } from "./helpers";
|
|
|
|
const projectUrl = `/e2e/fixtures/media-editor.html?view=project&projectId=${PROJECT_ID}`;
|
|
|
|
test("project variants plan, approve into a final render, and publish", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const backend = mediaBackend();
|
|
await backend.install(page);
|
|
await page.goto(projectUrl);
|
|
|
|
const project = page.getByTestId("media-project");
|
|
await expect(project.getByRole("heading", { name: "Harbor teaser" })).toBeVisible();
|
|
const shorts = page.locator(`[data-testid="media-variant"][data-profile="${SHORTS}"]`);
|
|
const paid = page.locator(`[data-testid="media-variant"][data-profile="${PAID}"]`);
|
|
const tiktok = page.locator(`[data-testid="media-variant"][data-profile="${TIKTOK}"]`);
|
|
await expect(shorts.getByTestId("media-gate-status")).toHaveAttribute("data-status", "pass");
|
|
await expect(paid.getByTestId("media-gate-status")).toHaveAttribute("data-status", "blocked");
|
|
await expect(paid.getByText("Auto-fix 1/3")).toBeVisible();
|
|
await expect(tiktok.getByText("No plan yet")).toBeVisible();
|
|
await expect(tiktok.getByText("Unverified")).toBeVisible();
|
|
// A blocked draft cannot be approved.
|
|
await expect(paid.getByRole("button", { name: "Approve" })).toHaveCount(0);
|
|
await captureScreenshot(page, testInfo, "media-project-variants");
|
|
|
|
// Auto-plan fills the profile without a plan.
|
|
await project.getByRole("button", { name: "Plan" }).click();
|
|
await expect(tiktok.getByRole("button", { name: "Edit" })).toBeVisible();
|
|
await expect(project.getByRole("button", { name: "Plan" })).toHaveCount(0);
|
|
|
|
// Approving a passed draft starts the final render; the card polls until it is done.
|
|
backend.holdRenders(2);
|
|
await shorts.getByRole("button", { name: "Approve" }).click();
|
|
await expect(shorts.getByText(/Rendering \d+%/)).toBeVisible();
|
|
const start = backend.calls.find((call) => call.procedure === "renders/start");
|
|
expect(start?.input).toEqual({ planId: "plan-shorts-1", quality: "final" });
|
|
await expect(shorts.getByTestId("media-publish")).toBeVisible({ timeout: 15_000 });
|
|
await expect(shorts.getByRole("button", { name: "Download" })).toBeVisible();
|
|
// Polling stops once nothing renders.
|
|
const polls = backend.calls.filter((call) => call.procedure === "projects/get").length;
|
|
await page.waitForTimeout(2_500);
|
|
expect(backend.calls.filter((call) => call.procedure === "projects/get").length).toBe(polls);
|
|
|
|
// Thumbnails, copy, and the bundle step appear in order.
|
|
const publish = shorts.getByTestId("media-publish");
|
|
await publish.getByRole("button", { name: "Thumbnails" }).click();
|
|
await expect(publish.getByRole("button", { name: /thumb-/ })).toHaveCount(3);
|
|
await expect(publish.getByRole("button", { name: "thumb-a" })).toHaveAttribute(
|
|
"aria-pressed",
|
|
"true",
|
|
);
|
|
await publish.getByRole("button", { name: "thumb-b" }).click();
|
|
await publish.getByRole("button", { name: "Write copy" }).click();
|
|
await expect(publish.getByRole("textbox", { name: "Title" })).toHaveValue("Harbor at dawn");
|
|
await expect(publish.getByRole("textbox", { name: "Hashtags" })).toHaveValue(
|
|
"#harbor #sunrise",
|
|
);
|
|
await expect(publish.getByRole("button", { name: "Bundle" })).toBeVisible();
|
|
await captureScreenshot(page, testInfo, "media-project-publish");
|
|
});
|
|
|
|
test("the editor trims and moves clips, edits captions and overlays, and shows EDL errors", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const backend = mediaBackend();
|
|
await backend.install(page);
|
|
backend.submitErrors.push([
|
|
{
|
|
code: "overlap",
|
|
path: "tracks.video[1].atMs",
|
|
message: "clip-2 overlaps clip-3 on the video track",
|
|
},
|
|
{ code: "duration_above_max", path: "output", message: "duration is above the limit" },
|
|
]);
|
|
await page.goto(projectUrl);
|
|
|
|
const shorts = page.locator(`[data-testid="media-variant"][data-profile="${SHORTS}"]`);
|
|
await shorts.getByRole("button", { name: "Edit" }).click();
|
|
const editor = page.getByTestId("media-editor");
|
|
await expect(editor).toBeVisible();
|
|
const clips = editor.getByTestId("media-clip");
|
|
await expect(clips).toHaveCount(3);
|
|
await expect(editor.getByRole("button", { name: "Save" })).toBeDisabled();
|
|
|
|
// Trim the first clip's end to the right with the pointer.
|
|
const trimEnd = clips.first().getByTestId("media-trim-end");
|
|
const handle = await trimEnd.boundingBox();
|
|
if (!handle) throw new Error("trim handle not laid out");
|
|
await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.move(handle.x + 40, handle.y + handle.height / 2, { steps: 5 });
|
|
await page.mouse.move(handle.x + 60, handle.y + handle.height / 2, { steps: 5 });
|
|
await page.mouse.up();
|
|
|
|
// Move the third clip later.
|
|
const third = clips.nth(2).getByRole("button", { name: "Clip clip-3" });
|
|
const box = await third.boundingBox();
|
|
if (!box) throw new Error("clip 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 + 30, box.y + box.height / 2, { steps: 5 });
|
|
await page.mouse.move(box.x + box.width / 2 + 50, box.y + box.height / 2, { steps: 5 });
|
|
await page.mouse.up();
|
|
|
|
// Captions: switch to the subtitle file and highlight words.
|
|
await editor.getByRole("button", { name: "Captions", exact: true }).click();
|
|
const inspector = editor.getByTestId("media-inspector");
|
|
await inspector.getByLabel("Captions").selectOption("both");
|
|
await inspector.getByRole("switch", { name: "Highlight words" }).click();
|
|
|
|
// A new text overlay at the playhead.
|
|
await editor.getByRole("button", { name: "Text", exact: true }).click();
|
|
await inspector.getByLabel("Text").fill("Link in bio");
|
|
|
|
await editor.getByRole("button", { name: "Save" }).click();
|
|
const errors = editor.getByTestId("media-edl-errors");
|
|
await expect(errors.getByText("clip-2 overlaps clip-3 on the video track")).toBeVisible();
|
|
await expect(errors.getByText("duration is above the limit")).toBeVisible();
|
|
await expect(clips.nth(1)).toHaveAttribute("data-invalid", "true");
|
|
await captureScreenshot(page, testInfo, "media-editor-errors");
|
|
|
|
const submitted = backend.calls.find((call) => call.procedure === "plans/submit")?.input as {
|
|
projectId: string;
|
|
edl: ReturnType<typeof edl>;
|
|
};
|
|
expect(submitted.projectId).toBe(PROJECT_ID);
|
|
const [first, , moved] = submitted.edl.tracks.video;
|
|
expect(first?.outMs).toBeGreaterThan(4_000);
|
|
expect(first?.outMs).toBeLessThanOrEqual(6_000);
|
|
expect(moved?.atMs).toBeGreaterThan(8_000);
|
|
expect(submitted.edl.captions).toMatchObject({ mode: "both", wordHighlight: true });
|
|
expect(submitted.edl.overlays.at(-1)).toMatchObject({ kind: "text", text: "Link in bio" });
|
|
|
|
// Selecting an error selects the clip; the next save is accepted as a new version.
|
|
await errors.getByText("clip-2 overlaps clip-3 on the video track").click();
|
|
await expect(inspector.getByRole("spinbutton", { name: "In", exact: true })).toBeVisible();
|
|
await editor.getByRole("button", { name: "Save" }).click();
|
|
await expect(editor.getByRole("heading", { name: `${SHORTS} · v2` })).toBeVisible();
|
|
await expect(errors).toHaveCount(0);
|
|
await captureScreenshot(page, testInfo, "media-editor");
|
|
});
|