// @vitest-environment jsdom // // The share dialog's link row says two things with colour alone: a PROTECTED link's lock is lit, and // Revoke is the destructive one. Both colours once sat on the button, where the ghost variant's own // colour class outranks them (clsx does not merge Tailwind), so a password-protected link looked // unprotected and Revoke hovered neutral. They live on the ICON now; this pins them there. import { describe, it, expect, afterEach, beforeEach, vi } from "vitest"; import { render, screen, cleanup, fireEvent, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { ReactNode } from "react"; // Partial: the import graph reaches `src/i18n`, which initialises against the real // `initReactI18next`, so a wholesale mock throws before any test runs. vi.mock("react-i18next", async (importOriginal) => ({ ...(await importOriginal()), useTranslation: () => ({ t: (k: string) => k, i18n: { language: "en" } }), })); vi.mock("./ui/Modal", () => ({ default: ({ title, children }: { title: string; children: ReactNode }) => (

{title}

{children}
), })); vi.mock("./confirmContext", () => ({ useConfirm: () => async () => h.confirm })); const h = vi.hoisted(() => ({ confirm: false, revokeDownloadLink: vi.fn(), links: null as unknown[] | null, folderLinks: vi.fn(), createFolderLink: vi.fn(), createDownloadLink: vi.fn(), })); vi.mock("../lib/api", () => ({ api: { shareRecipients: async () => [], folderLinks: h.folderLinks, createFolderLink: h.createFolderLink, createDownloadLink: h.createDownloadLink, revokeDownloadLink: h.revokeDownloadLink, downloadLinks: async () => h.links ?? [ { id: 1, token: "a", url: "/watch/a", allow_download: false, has_password: true, expires_at: null, view_count: 0, created_at: null, }, { id: 2, token: "b", url: "/watch/b", allow_download: false, has_password: false, expires_at: null, view_count: 0, created_at: null, }, ], }, })); import ShareDialog from "./ShareDialog"; import type { DownloadJob } from "../lib/api"; afterEach(cleanup); beforeEach(() => { h.links = null; h.confirm = false; }); const iconClass = (button: HTMLElement) => button.querySelector("svg")?.getAttribute("class") ?? ""; describe("ShareDialog · a link row's state colours", () => { it("lights the lock of a protected link only, and paints Revoke as danger — on the icon", async () => { const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render( {}} /> , ); const protectedLock = await screen.findByRole("button", { name: "downloads.share.changePassword", }); const openLock = screen.getByRole("button", { name: "downloads.share.setPassword" }); expect(iconClass(protectedLock)).toContain("text-accent"); expect(iconClass(openLock)).not.toContain("text-accent"); for (const revoke of screen.getAllByRole("button", { name: "downloads.share.revoke" })) expect(iconClass(revoke)).toContain("text-danger"); }); }); describe("ShareDialog · a link whose address cannot be shown again", () => { it("says so in place of the address, with nothing to copy", async () => { const link = (id: number, url: string | null) => ({ id, token: url && url.slice("/watch/".length), url, allow_download: false, has_password: false, expires_at: null, view_count: 0, created_at: null, }); h.links = [link(3, null), link(2, "/watch/b")]; const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render( {}} /> , ); expect(await screen.findByText("downloads.share.urlHidden")).toBeTruthy(); // One row has an address to show and copy; the hidden one shows none (not "…/null"). expect(screen.getAllByRole("button", { name: "downloads.share.copy" })).toHaveLength(1); const shown = screen.getAllByRole("textbox").map((el) => (el as HTMLInputElement).value); expect(shown).toContain(`${window.location.origin}/watch/b`); expect(shown.some((v) => v.includes("null"))).toBe(false); // Its other controls stay: it can still be protected, made downloadable, or revoked. expect(screen.getAllByRole("button", { name: "downloads.share.revoke" })).toHaveLength(2); }); }); describe("ShareDialog · a folder", () => { const link = (id: number) => ({ id, token: `f${id}`, url: `/watch/f${id}`, allow_download: true, has_password: false, expires_at: null, view_count: 0, created_at: null, }); it("shares by link only, says what a folder link shows, and makes it for the folder", async () => { h.folderLinks.mockReset().mockResolvedValue([link(4)]); h.createFolderLink.mockReset().mockResolvedValue(link(5)); const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render( {}} /> , ); expect(await screen.findByDisplayValue(`${window.location.origin}/watch/f4`)).toBeTruthy(); expect(h.folderLinks).toHaveBeenCalledWith(7); expect(screen.getByRole("heading").textContent).toBe("downloads.share.folderTitle"); expect(screen.getByText("downloads.share.folderLinkHint")).toBeTruthy(); // No sharing with a user: "Shared with me" lists files, not folders. expect(screen.queryByText("downloads.share.userSection")).toBeNull(); // A stream-only link, then the form is back at its default for the next one: downloadable. fireEvent.click(screen.getByRole("button", { name: "downloads.share.newLink" })); // The form's own checkbox, under the link rows' (each row has its own). const allow = () => { const boxes = screen.getAllByRole("checkbox", { name: "downloads.share.allowDownload" }); return boxes[boxes.length - 1]!; }; fireEvent.click(allow()); expect((allow() as HTMLInputElement).checked).toBe(false); fireEvent.click(screen.getByRole("button", { name: "downloads.share.createLink" })); await waitFor(() => expect(h.createFolderLink).toHaveBeenCalledWith( 7, expect.objectContaining({ allow_download: false, }), ), ); expect(h.createDownloadLink).not.toHaveBeenCalled(); fireEvent.click(await screen.findByRole("button", { name: "downloads.share.newLink" })); expect((allow() as HTMLInputElement).checked).toBe(true); }); it("makes a link on opening with none, but never right after the last one was revoked", async () => { h.folderLinks.mockReset().mockResolvedValue([]); h.createFolderLink.mockReset().mockResolvedValue(link(6)); const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render( {}} /> , ); await waitFor(() => expect(h.createFolderLink).toHaveBeenCalledTimes(1)); view.unmount(); h.confirm = true; h.folderLinks .mockReset() .mockResolvedValueOnce([link(4)]) .mockResolvedValue([]); h.createFolderLink.mockReset(); h.revokeDownloadLink.mockReset().mockResolvedValue({ ok: true, deleted: 4 }); const qc2 = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render( {}} /> , ); fireEvent.click(await screen.findByRole("button", { name: "downloads.share.revoke" })); await waitFor(() => expect(h.revokeDownloadLink).toHaveBeenCalledWith(4)); await screen.findByText("downloads.share.noLinks"); expect(h.createFolderLink).not.toHaveBeenCalled(); }); });