evOo-local/apps/web/src/components/media/MediaPlayer.test.tsx
Claude c17445874d
style: use top-level type imports in the media changes
AGENTS.md asks for top-level `import type` instead of inline `type`
specifiers; this applies it to the import statements the media work added
or changed.

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

101 lines
3.6 KiB
TypeScript

// @vitest-environment jsdom
import { act } from "react";
import type { Root } from "react-dom/client";
import { createRoot } from "react-dom/client";
import { renderToStaticMarkup } from "react-dom/server";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@lingui/react/macro", () => {
const t = (parts: TemplateStringsArray, ...values: unknown[]) =>
parts.reduce(
(text, part, index) => text + part + (index < values.length ? values[index] : ""),
"",
);
return { useLingui: () => ({ t }) };
});
import { MediaPlayer } from "./MediaPlayer";
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
});
afterEach(async () => {
await act(async () => root.unmount());
container.remove();
vi.unstubAllGlobals();
});
describe("MediaPlayer", () => {
it("loads nothing before it is near the viewport and fits vertical video to the column", () => {
const html = renderToStaticMarkup(
<MediaPlayer artifactId="art-1" kind="video" name="clip.mp4" width={1080} height={1920} />,
);
expect(html).toContain('aria-label="clip.mp4"');
expect(html).not.toContain("src=");
expect(html).toContain("width:270px");
expect(html).toContain("aspect-ratio:270 / 480");
});
it("streams video with its poster from the artifact route once visible", async () => {
vi.stubGlobal(
"IntersectionObserver",
class {
constructor(private readonly callback: IntersectionObserverCallback) {}
observe() {
this.callback([{ isIntersecting: true } as IntersectionObserverEntry], this as never);
}
disconnect() {}
},
);
await act(async () =>
root.render(
<MediaPlayer artifactId="art-1" kind="video" name="clip.mp4" posterArtifactId="art-2" />,
),
);
const video = container.querySelector("video")!;
expect(video.getAttribute("src")).toBe("/api/artifacts/art-1");
expect(video.getAttribute("poster")).toBe("/api/artifacts/art-2");
expect(video.getAttribute("preload")).toBe("metadata");
expect(video.hasAttribute("controls")).toBe(true);
});
it("sizes the frame from the file when the block has no dimensions", async () => {
await act(async () =>
root.render(<MediaPlayer artifactId="art-4" kind="video" name="up.mp4" />),
);
const frame = container.querySelector<HTMLDivElement>('[data-testid="media-video"]')!;
expect(frame.style.width).toBe("420px");
const video = container.querySelector("video")!;
Object.defineProperty(video, "videoWidth", { value: 1080 });
Object.defineProperty(video, "videoHeight", { value: 1920 });
await act(async () => {
video.dispatchEvent(new Event("loadedmetadata"));
});
expect(frame.style.width).toBe("270px");
});
it("falls back to a download link when the browser cannot play the file", async () => {
await act(async () =>
root.render(<MediaPlayer artifactId="art-3" kind="audio" name="memo.ogg" />),
);
const audio = container.querySelector("audio")!;
expect(audio.getAttribute("src")).toBe("/api/artifacts/art-3");
await act(async () => {
audio.dispatchEvent(new Event("error"));
});
expect(container.querySelector("audio")).toBeNull();
const link = container.querySelector("a")!;
expect(link.getAttribute("href")).toBe("/api/artifacts/art-3?download=1");
expect(link.getAttribute("download")).toBe("memo.ogg");
expect(link.getAttribute("aria-label")).toBe("Download memo.ogg");
});
});