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; }; 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"); });