evOo-local/apps/web/e2e/media-pipeline.spec.ts
Claude 979034a8a6
feat(web): play video and audio inline and stream uploads and downloads
Media sent in a conversation had nowhere to go: the composer capped every
file at 10 MiB and pushed it through a base64 RPC, video and audio blocks
did not render, and downloads decoded whole files in the page.

- Video and audio blocks play inline through MediaPlayer, which loads
  nothing until the player is near the viewport, sizes the frame from the
  block's dimensions (vertical clips stay within the column) and uses the
  poster when one exists. Files the browser cannot play fall back to their
  name with a download action.
- Images load straight from the artifact route; the lightbox gains a
  download action.
- File cards, the Markdown preview and downloads read from the artifact
  route instead of decoding base64.
- The composer accepts each kind up to its cap and names the computed
  limit when it skips a file; video, audio and anything above the inline
  cap upload as a raw stream, small files keep the inline RPC.
- MediaPlayer and artifactUrl are what the media card builds on.

Tests: MediaPlayer laziness, sizing and fallback, the upload helper, and an
E2E that uploads a tiny MP4 fixture (VP9, since Playwright's Chromium has
no H.264 decoder), plays it from the artifact route and captures
media-video-block, plus the image lightbox download.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:50:02 +00:00

80 lines
3.3 KiB
TypeScript

import path from "node:path";
import { fileURLToPath } from "node:url";
import { expect, test } from "@playwright/test";
import { captureScreenshot, completeOnboarding, signup } from "./helpers";
test.describe.configure({ mode: "serial" });
// VP9 in MP4: Playwright's Chromium has no H.264 decoder, real browsers play both.
const clip = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures", "media-clip.mp4");
const pixel = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
"base64",
);
test("an uploaded video streams from the artifact route and plays inline", async ({
page,
}, testInfo) => {
const stamp = Date.now();
await signup(page, `media-video-${stamp}@rakazo.test`, "password12", "Media Video");
await completeOnboarding(page);
const upload = page.waitForResponse(
(response) =>
response.request().method() === "POST" && /\/api\/artifacts\?/.test(response.url()),
);
await page.locator('input[type="file"]').setInputFiles(clip);
await expect(page.getByRole("button", { name: "Remove media-clip.mp4" })).toBeVisible();
await page.getByRole("combobox", { name: /Message/ }).fill("here is the clip");
await page.keyboard.press("Enter");
expect((await upload).status()).toBe(201);
const video = page.getByTestId("media-video").locator("video");
await expect(video).toBeVisible({ timeout: 30_000 });
await expect(video).toHaveAttribute("src", /^\/api\/artifacts\/[^/?]+$/);
await expect
.poll(() => video.evaluate((element: HTMLVideoElement) => element.readyState), {
timeout: 15_000,
})
.toBeGreaterThanOrEqual(1);
expect(await video.evaluate((element: HTMLVideoElement) => element.duration)).toBeCloseTo(1, 0);
await video.evaluate(async (element: HTMLVideoElement) => {
element.muted = true;
await element.play();
});
await expect
.poll(() => video.evaluate((element: HTMLVideoElement) => element.currentTime))
.toBeGreaterThan(0);
await video.evaluate((element: HTMLVideoElement) => {
element.pause();
element.currentTime = 0.5;
element.scrollIntoView({ block: "center" });
});
await captureScreenshot(page, testInfo, "media-video-block");
});
test("the image lightbox downloads the original from the artifact route", async ({ page }) => {
const stamp = Date.now();
await signup(page, `media-image-${stamp}@rakazo.test`, "password12", "Media Image");
await completeOnboarding(page);
await page
.locator('input[type="file"]')
.setInputFiles({ name: "pixel.png", mimeType: "image/png", buffer: pixel });
await page.getByRole("combobox", { name: /Message/ }).fill("a pixel");
await page.keyboard.press("Enter");
const thumbnail = page.getByRole("img", { name: "pixel.png" });
await expect(thumbnail).toBeVisible({ timeout: 30_000 });
await expect(thumbnail).toHaveAttribute("src", /^\/api\/artifacts\/[^/?]+$/);
await thumbnail.click();
const dialog = page.getByRole("dialog", { name: "pixel.png" });
const download = dialog.getByRole("link", { name: "Download pixel.png" });
await expect(download).toHaveAttribute("href", /\/api\/artifacts\/[^/?]+\?download=1$/);
const saved = page.waitForEvent("download");
await download.click();
expect((await saved).suggestedFilename()).toBe("pixel.png");
});