diff --git a/frontend/src/components/downloadRow.dom.test.tsx b/frontend/src/components/downloadRow.dom.test.tsx new file mode 100644 index 00000000..be5d62b3 --- /dev/null +++ b/frontend/src/components/downloadRow.dom.test.tsx @@ -0,0 +1,87 @@ +// @vitest-environment jsdom +// +// Library-row metadata (E7·S1): the completed row surfaces the concrete container/format alongside the +// already-shown size. The format chip reads the raw `asset.container` (uppercased in the UI), so it must +// render when present and be absent when the asset has no container yet (queued/running before the worker +// fills it). i18n is key-passthrough here. +import { describe, it, expect, afterEach, vi } from "vitest"; +import { render, screen, cleanup } from "@testing-library/react"; + +vi.mock("react-i18next", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, useTranslation: () => ({ t: (key: string) => key }) }; +}); + +import { DownloadRow } from "./downloadRow"; +import type { DownloadJob } from "../lib/api"; + +const baseJob: DownloadJob = { + id: 1, + status: "done", + progress: 100, + speed_bps: null, + eta_s: null, + phase: null, + error: null, + error_code: null, + queue_pos: 0, + created_at: null, + source_kind: "youtube", + source_ref: "abc", + source_url: null, + poster_url: null, + profile_id: null, + spec: { + mode: "av", + max_height: null, + container: null, + audio_format: null, + vcodec: null, + embed_subs: false, + embed_chapters: false, + embed_thumbnail: false, + sponsorblock: false, + }, + display_name: "Clip", + display_uploader: null, + display_uploader_url: null, + extra_links: [], + can_download: true, + expired: false, + asset: null, +}; + +const withAsset = (container: string | null): DownloadJob => ({ + ...baseJob, + asset: { + id: 10, + status: "ready", + title: "Clip", + uploader: null, + uploader_url: null, + upload_date: null, + duration_s: 90, + size_bytes: 148_800_000, + width: 1920, + height: 1080, + container, + thumbnail_url: null, + expires_at: null, + }, +}); + +afterEach(cleanup); + +describe("DownloadRow format chip", () => { + it("shows the uppercased container next to the size when ready", () => { + render(); + // Raw container is stored lowercase; the row uppercases it via CSS, so the text node stays "mp4". + expect(screen.getByText(/·\s*mp4/i)).toBeTruthy(); + expect(screen.getByText(/142 MB/)).toBeTruthy(); + }); + + it("omits the format chip when the asset has no container yet", () => { + const { container } = render(); + expect(container.textContent).not.toMatch(/·\s*(mp4|m4a|webm)/i); + }); +}); diff --git a/frontend/src/components/downloadRow.tsx b/frontend/src/components/downloadRow.tsx index 53f00711..45a918f7 100644 --- a/frontend/src/components/downloadRow.tsx +++ b/frontend/src/components/downloadRow.tsx @@ -215,6 +215,11 @@ export function DownloadRow({
+ {job.asset?.container ? ( + // Concrete container (mp4 / m4a / webm …) — reads as the file format, and its audio-vs-video + // extension is the video/audio distinction the running-only phase pill can't show at rest. + · {job.asset.container} + ) : null} {job.asset?.size_bytes ? ( · {formatBytes(job.asset.size_bytes)} ) : null}