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
80 lines
3.3 KiB
TypeScript
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");
|
|
});
|