The /watch/{token} SPA gets a third branch: media_kind==='file' renders an
info card (name / TYPE · size) + a Download button, no player. The <video>/
<audio> branches now guard on media_kind!=='file' so legacy links
(media_kind null) still play. Download is gated on the link's allow_download,
mirroring the media branch; the backend serves a file force-download
(attachment + octet-stream) regardless, so the gate only reflects owner ACL.
FE-only (public_meta already emits media_kind/size_bytes/container since S5a).
i18n HU/EN/DE 'file' fallback label. Two DOM tests: file -> info card +
Download, no player; view-only file -> no Download.
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// S3 item 5a: the public watch page must render a native <audio> element for an audio-only asset
|
|
// (mp3) and a <video> for everything else. WebKit (all iOS browsers) refuses to play an audio-only
|
|
// mp3 inside a <video>, which is exactly why the branch exists — this locks it so a refactor can't
|
|
// silently drop the audio player and reintroduce the iPad-broken behavior.
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { render, waitFor, cleanup } from "@testing-library/react";
|
|
import WatchPage from "./WatchPage";
|
|
|
|
function mockMeta(meta: Record<string, unknown>) {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => ({ ok: true, json: async () => meta })),
|
|
);
|
|
}
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe("WatchPage media element (S3 · 5a)", () => {
|
|
it("renders a native <audio> for an audio-only asset", async () => {
|
|
window.history.replaceState({}, "", "/watch/tok-audio");
|
|
mockMeta({ needs_password: false, audio: true, file_url: "/f.mp3", title: "A song" });
|
|
const { container } = render(<WatchPage />);
|
|
await waitFor(() => expect(container.querySelector("audio")).not.toBeNull());
|
|
expect(container.querySelector("video")).toBeNull();
|
|
});
|
|
|
|
it("renders a <video> for a normal video asset", async () => {
|
|
window.history.replaceState({}, "", "/watch/tok-video");
|
|
mockMeta({ needs_password: false, audio: false, file_url: "/f.mp4", title: "A clip" });
|
|
const { container } = render(<WatchPage />);
|
|
await waitFor(() => expect(container.querySelector("video")).not.toBeNull());
|
|
expect(container.querySelector("audio")).toBeNull();
|
|
});
|
|
|
|
it("renders an info card + Download for a generic file, no player (S5d)", async () => {
|
|
window.history.replaceState({}, "", "/watch/tok-file");
|
|
mockMeta({
|
|
needs_password: false,
|
|
media_kind: "file",
|
|
title: "report.pdf",
|
|
container: "pdf",
|
|
size_bytes: 2_400_000,
|
|
allow_download: true,
|
|
file_url: "/f.pdf",
|
|
});
|
|
const { container, findByText } = render(<WatchPage />);
|
|
await findByText("report.pdf");
|
|
expect(container.querySelector("video")).toBeNull();
|
|
expect(container.querySelector("audio")).toBeNull();
|
|
// Type · size chip and a Download link to the file.
|
|
await findByText(/PDF · 2\.3 MB/);
|
|
const dl = container.querySelector('a[href="/f.pdf"]');
|
|
expect(dl).not.toBeNull();
|
|
});
|
|
|
|
it("omits the Download for a view-only file link (S5d)", async () => {
|
|
window.history.replaceState({}, "", "/watch/tok-file-noview");
|
|
mockMeta({
|
|
needs_password: false,
|
|
media_kind: "file",
|
|
title: "secret.zip",
|
|
container: "zip",
|
|
size_bytes: 1000,
|
|
allow_download: false,
|
|
file_url: "/f.zip",
|
|
});
|
|
const { container, findByText } = render(<WatchPage />);
|
|
await findByText("secret.zip");
|
|
expect(container.querySelector('a[href="/f.zip"]')).toBeNull();
|
|
});
|
|
});
|