// @vitest-environment jsdom // // The Files viewer: what each kind of file shows (an image, a PDF in a frame, text as text — HTML too, // never a page — or the "no preview" card), ← / → through the files it was given in their order, // and closing when the file it shows leaves the list. i18n is key-passthrough. import { afterEach, describe, expect, it, vi } from "vitest"; import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { api, HttpError, type LibraryFile } from "../lib/api"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ i18n: { language: "en" }, t: (key: string, o?: Record) => o && "n" in o ? `${key}:${String(o.n)}/${String(o.total)}` : key, }), }; }); import FilePreview from "./FilePreview"; const file = (id: number, preview: string | null, extra: Record = {}) => ({ id, status: "done", display_name: `File ${id}`, can_download: true, job_kind: "upload", shared: false, source_key: "upload", asset: { media_kind: "file", preview, container: "x", size_bytes: 4096, ...extra }, }) as unknown as LibraryFile; afterEach(() => { cleanup(); vi.restoreAllMocks(); }); function mount(files: LibraryFile[], fileId: number, onClose = vi.fn()) { const onFileChange = vi.fn(); const view = (list: LibraryFile[], id: number) => ( []} /> ); const r = render(view(files, fileId)); return { onFileChange, onClose, rerender: (l: LibraryFile[], id: number) => r.rerender(view(l, id)), }; } describe("FilePreview · what it shows", () => { it("an image as an image, from the sandboxed preview", () => { mount([file(1, "image")], 1); const img = screen.getByRole("img", { name: "File 1" }); expect(img.getAttribute("src")).toBe(api.previewUrl(1)); }); it("a PDF in a frame of the preview, the browser's own viewer", () => { mount([file(2, "pdf")], 2); const frame = document.querySelector("iframe")!; expect(frame.getAttribute("src")).toBe(api.previewUrl(2)); expect(frame.title).toBe("File 2"); }); it("a page's source as text, line by line — never as a page", async () => { vi.spyOn(api, "previewText").mockResolvedValue({ text: "

hi

\n", truncated: false, }); mount([file(3, "text")], 3); await screen.findByText("

hi

"); expect(screen.getByText("")).toBeTruthy(); expect(document.getElementById("x")).toBeNull(); expect(document.querySelector("script")).toBeNull(); expect(screen.getByText("files.preview.asText")).toBeTruthy(); }); it("numbers a file's lines, the newline that ends it making no line of its own", async () => { vi.spyOn(api, "previewText").mockResolvedValue({ text: "one\r\ntwo\n", truncated: false }); mount([file(8, "text")], 8); await screen.findByText("one"); expect([...document.querySelectorAll("li")].map((li) => li.textContent)).toEqual([ "1one", "2two", ]); }); it("says when it shows only the start of a long text", async () => { vi.spyOn(api, "previewText").mockResolvedValue({ text: "a", truncated: true }); mount([file(4, "text")], 4); expect(await screen.findByText("files.preview.truncated")).toBeTruthy(); }); it("a text the server refuses (binary after all) gets the no-preview card, not an error", async () => { vi.spyOn(api, "previewText").mockRejectedValue(new HttpError(415, "x", {})); mount([file(5, "text")], 5); expect(await screen.findByText("files.preview.none")).toBeTruthy(); expect(screen.queryByRole("alert")).toBeNull(); // A new tab would only show the refusal. expect(screen.queryByRole("link", { name: "files.preview.openInTab" })).toBeNull(); }); it("a text that fails to load says so, with a retry", async () => { vi.spyOn(api, "previewText").mockRejectedValue(new HttpError(500, "x", {})); mount([file(6, "text")], 6); expect(await screen.findByRole("alert", {}, { timeout: 3000 })).toBeTruthy(); expect(screen.getByRole("button", { name: "common.retry" })).toBeTruthy(); }); it("anything else: the card that offers the file itself", () => { mount([file(7, null)], 7); expect(screen.getByText("files.preview.none")).toBeTruthy(); const save = screen.getAllByRole("link", { name: "downloads.actions.download" }); expect(save.every((a) => a.getAttribute("href") === api.fileUrl(7))).toBe(true); // Nothing to open in a tab: the preview would refuse it. expect(screen.queryByRole("link", { name: "files.preview.openInTab" })).toBeNull(); }); }); describe("FilePreview · stepping", () => { const list = [file(1, "image"), file(2, "pdf"), file(3, null)]; it("← / → go to the files beside it, in the order given", () => { const { onFileChange } = mount(list, 2); expect(screen.getByText("files.preview.position:2/3")).toBeTruthy(); fireEvent.keyDown(window, { key: "ArrowRight" }); expect(onFileChange).toHaveBeenLastCalledWith(3); fireEvent.keyDown(window, { key: "ArrowLeft" }); expect(onFileChange).toHaveBeenLastCalledWith(1); }); it("stops at either end", () => { const { onFileChange } = mount(list, 1); fireEvent.keyDown(window, { key: "ArrowLeft" }); expect(onFileChange).not.toHaveBeenCalled(); expect( screen.getByRole("button", { name: "files.preview.prev" }).hasAttribute("disabled"), ).toBe(true); }); it("closes when the file it shows leaves the list", async () => { const { onClose, rerender } = mount(list, 2); expect(onClose).not.toHaveBeenCalled(); act(() => rerender([list[0]!, list[2]!], 2)); await waitFor(() => expect(onClose).toHaveBeenCalled()); }); });