// @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 } 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: ({ children }: { children: ReactNode }) => children })); vi.mock("./confirmContext", () => ({ useConfirm: () => async () => false })); const h = vi.hoisted(() => ({ links: null as unknown[] | null })); vi.mock("../lib/api", () => ({ api: { shareRecipients: async () => [], 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; }); 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); }); });