Files
siftlode/frontend/src/components/downloadRow.dom.test.tsx
T
peter 078194731b feat(files): the Downloads page goes — the tray, a Storage tab and a Downloads settings tab take its place
- The Downloads module, its rail icon and badge are gone. A bookmark, the remembered page and an old
  history entry that name it land on Files (urlState.resolvePage); `g d` goes to Files too.
- Users page: a Storage tab — the disk in numbers, every user's use against their own limit with
  their file count, every user's downloads; aligned tables, one menu per row (edit / reset quota,
  open source, copy, the owner's quota). The admin storage rows carry quota + file count, the admin
  job list its owner.
- Settings: a Downloads tab — the download formats in one list (duplicate a built-in, edit or delete
  your own) and the site sign-ins, moved from Account.
- "View" on an added download, a clip or a converting upload opens the tray on that row (unfolded,
  back if closed; a finished one opens as a file; under a player it opens without taking focus).
- The download dialog opens on the format used last.
- DataTable: `pager={false}` for a short list, `cardRow` to give a lone "…" its own card line.
- Welcome and README speak of Files; a new Files screenshot.
2026-10-11 19:37:13 +02:00

108 lines
3.1 KiB
TypeScript

// @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<typeof import("react-i18next")>();
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<NonNullable<DownloadJob["asset"]>> = {},
): 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(<JobThumb job={withAsset("mp4")} />);
expect(container.querySelector("img")).toBeNull();
expect(container.querySelector("svg.lucide-film")).not.toBeNull();
unmount();
const { container: audio } = render(
<JobThumb job={withAsset("m4a", { media_kind: "audio" })} />,
);
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(
<JobThumb job={withAsset("mp4", { thumbnail_url: "https://cdn.example/dead.jpg" })} />,
);
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();
});
});