// @vitest-environment jsdom // // A job's picture (the Files list and grid): with none at all, a quiet glyph of what it is; a remote // thumbnail that fails falls back to the same glyph. i18n is key-passthrough here. import { describe, it, expect, afterEach, vi } from "vitest"; import { render, cleanup, fireEvent } from "@testing-library/react"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }), }; }); import { JobThumb } 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, error_detail: 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, folder_id: null, kept: false, expires_at: null, asset: null, }; const withAsset = ( container: string | null, over: Partial> = {}, ): 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, media_kind: "video", thumbnail_url: null, ...over, }, }); afterEach(cleanup); // No picture at all: a quiet glyph of what it is instead of an empty box (the owner, 2026-09-27). describe("JobThumb · no picture", () => { it("draws a film glyph for a video, a note for audio", () => { const { container, unmount } = render(); expect(container.querySelector("img")).toBeNull(); expect(container.querySelector("svg.lucide-film")).not.toBeNull(); unmount(); const { container: audio } = render( , ); expect(audio.querySelector("svg.lucide-music")).not.toBeNull(); }); // The remote thumbnail was the last resort with no fallback of its own — when it // failed too (a dead CDN link), the row drew the browser's broken-image mark. it("falls back to the glyph when the remote picture fails too", () => { const { container } = render( , ); const img = container.querySelector("img")!; expect(img.getAttribute("src")).toBe("https://cdn.example/dead.jpg"); fireEvent.error(img); expect(container.querySelector("img")).toBeNull(); expect(container.querySelector("svg.lucide-film")).not.toBeNull(); }); });