import { expect, type Page, test } from "@playwright/test"; import { activeBotId, captureScreenshot, completeOnboarding, fetchRpcEnvelope, fulfillFetchedRpc, rpc, signup, } from "./helpers"; type MediaBlock = Record & { kind: "media" }; type Snapshot = { threadId: string; messages: Array<{ seq: number } & Record>; }; /** One second of a quiet 440 Hz tone as 8 kHz mono 16-bit WAV, small enough to inline. */ function toneWavBase64(): string { const rate = 8000; const samples = rate; const buffer = Buffer.alloc(44 + samples * 2); buffer.write("RIFF", 0); buffer.writeUInt32LE(36 + samples * 2, 4); buffer.write("WAVEfmt ", 8); buffer.writeUInt32LE(16, 16); buffer.writeUInt16LE(1, 20); buffer.writeUInt16LE(1, 22); buffer.writeUInt32LE(rate, 24); buffer.writeUInt32LE(rate * 2, 28); buffer.writeUInt16LE(2, 32); buffer.writeUInt16LE(16, 34); buffer.write("data", 36); buffer.writeUInt32LE(samples * 2, 40); for (let index = 0; index < samples; index += 1) { buffer.writeInt16LE( Math.round(Math.sin((2 * Math.PI * 440 * index) / rate) * 3000), 44 + index * 2, ); } return buffer.toString("base64"); } /** A small grey gradient still, drawn by the browser so no binary fixture is needed. */ async function stillPngBase64(page: Page): Promise { return page.evaluate(() => { const canvas = document.createElement("canvas"); canvas.width = 320; canvas.height = 180; const context = canvas.getContext("2d")!; const gradient = context.createLinearGradient(0, 0, 320, 180); gradient.addColorStop(0, "rgb(40, 40, 40)"); gradient.addColorStop(1, "rgb(200, 200, 200)"); context.fillStyle = gradient; context.fillRect(0, 0, 320, 180); return canvas.toDataURL("image/png").split(",")[1]!; }); } /** * Media jobs run on a GPU server or ffmpeg that CI does not have, so the card is seeded: real * artifacts are uploaded, and the thread snapshot carries a bot message with the job's card. */ async function seedMediaMessage(page: Page, botId: string, block: () => MediaBlock) { await page.route("**/rpc/threads/get", async (route) => { const fetched = await fetchRpcEnvelope(route); const snapshot = fetched.body.json as (Snapshot & { botId?: string }) | undefined; if (snapshot?.botId === botId) { const seq = Math.max(0, ...snapshot.messages.map((message) => message.seq)) + 1; snapshot.messages.push({ id: `media-message-${seq}`, threadId: snapshot.threadId, seq, role: "bot", botId, blocks: [block()], createdAt: new Date().toISOString(), }); } await fulfillFetchedRpc(route, fetched); }); } test("renders a media job card while it runs and when it is done", async ({ page }, testInfo) => { const stamp = Date.now(); await signup(page, `media-card-${stamp}@rakazo.test`, "password12", "Media Card"); await completeOnboarding(page); const botId = activeBotId(page); const still = await rpc<{ id: string; size: number }>(page, "artifacts/create", { botId, name: "still.png", mimeType: "image/png", contentBase64: await stillPngBase64(page), }); const voice = await rpc<{ id: string; size: number }>(page, "artifacts/create", { botId, name: "voice-note.wav", mimeType: "audio/wav", contentBase64: toneWavBase64(), }); let state: "running" | "done" = "running"; await seedMediaMessage(page, botId, () => state === "running" ? { kind: "media", jobId: "media-job-1", operation: "edit_video", status: "running", title: "Cut the pauses and reframe for 9:16", progress: 0.4, outputs: [], } : { kind: "media", jobId: "media-job-1", operation: "edit_video", status: "done", title: "Cut the pauses and reframe for 9:16", outputs: [ { artifactId: still.id, kind: "image", mimeType: "image/png", name: "still.png", size: still.size, width: 320, height: 180, }, { artifactId: voice.id, kind: "audio", mimeType: "audio/wav", name: "voice-note.wav", size: voice.size, durationMs: 1000, }, ], }, ); await page.reload(); const card = page.getByTestId("media-card"); await expect(card).toBeVisible({ timeout: 30_000 }); await expect(card).toHaveAttribute("data-status", "running"); await expect(card).toContainText("Cut the pauses and reframe for 9:16"); await expect(card).toContainText("Working…"); await expect(card.getByRole("progressbar")).toHaveAttribute("aria-valuenow", "40"); await captureScreenshot(page, testInfo, "media-card-running"); state = "done"; await page.reload(); await expect(card).toHaveAttribute("data-status", "done", { timeout: 30_000 }); await expect(card).toContainText("Done"); await expect(card.getByRole("progressbar")).toHaveCount(0); await expect(card.getByRole("img", { name: "still.png" })).toBeVisible(); await expect(card.locator("audio")).toHaveCount(1); await expect(card.getByRole("link", { name: "Download voice-note.wav" })).toHaveAttribute( "download", "voice-note.wav", ); await captureScreenshot(page, testInfo, "media-card-done"); });