// @vitest-environment jsdom // // The admin Users page's Storage tab: every user's footprint against their OWN limit (or // "Unlimited") with their file count, and every user's downloads — each list's row has one menu. // A user's: Edit quota, Reset (behind a confirm). A download's: Open source, the copies, and Edit // this user's quota — which opens the quota of the download's OWNER. 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, type AdminStorage, type DownloadJob } from "../lib/api"; import { qk } from "../lib/queryKeys"; const mocks = vi.hoisted(() => ({ storage: undefined as unknown, jobs: undefined as unknown, confirm: vi.fn(async (_o?: unknown) => true), })); vi.mock("react-i18next", async (importOriginal) => ({ ...(await importOriginal()), useTranslation: () => ({ i18n: { language: "en" }, t: (key: string, o?: { size?: string; email?: string }) => `${key}${o?.size ? `:${o.size}` : ""}${o?.email ? `:${o.email}` : ""}`, }), })); vi.mock("./confirmContext", () => ({ useConfirm: () => mocks.confirm })); vi.mock("../lib/useLiveQuery", () => ({ useLiveQuery: (key: readonly unknown[]) => ({ data: key[0] === qk.adminStorage()[0] ? mocks.storage : mocks.jobs, isError: false, isLoading: false, refetch: vi.fn(), }), })); import { StorageTab } from "./downloadAdmin"; const GiB = 1024 ** 3; const storage = (): AdminStorage => ({ files_on_disk: 3, total_bytes: 3 * GiB, total_assets: 3, total_cap_bytes: 0, per_user: [ { user_id: 4, email: "anna@x", footprint_bytes: GiB, files: 27, max_bytes: 5 * GiB, unlimited: false, }, { user_id: 1, email: "peter@x", footprint_bytes: 2 * GiB, files: 180, // Unlimited wins over the limit the row still carries. max_bytes: 10 * GiB, unlimited: true, }, ], }); const job = (id: number, extra: Partial = {}) => ({ id, status: "done", display_name: `Clip ${id}`, source_url: `https://example.com/${id}`, user_id: 4, user_email: "anna@x", asset: { size_bytes: 1024 }, ...extra, }) as unknown as DownloadJob; afterEach(() => { cleanup(); vi.restoreAllMocks(); mocks.confirm.mockClear(); }); function mount() { mocks.storage = storage(); mocks.jobs = [job(7), job(8, { source_url: null, user_id: 1, user_email: "peter@x" })]; render( , ); } const rowOf = (text: string) => screen.getAllByText(text)[0]!.closest("[data-nav-index]")!; const menuRows = () => screen.getAllByRole("menuitem", { hidden: true }).map((b) => b.textContent ?? ""); const pick = (label: string) => fireEvent.click( screen.getAllByRole("menuitem", { hidden: true }).find((b) => b.textContent === label)!, ); async function openMenu(row: HTMLElement) { act(() => { fireEvent.contextMenu(row); }); await waitFor(() => expect(screen.getAllByRole("menuitem", { hidden: true }).length).toBeGreaterThan(0), ); } describe("StorageTab", () => { it("names each user's use against their own limit, and their files", () => { mount(); const anna = rowOf("anna@x"); expect(anna.textContent).toMatch(/downloads\.admin\.ofQuota:5(\.0)? GB/); expect(anna.textContent).toContain("27"); expect(anna.querySelector('[role="meter"]')?.getAttribute("aria-valuenow")).toBe("20"); expect(rowOf("peter@x").textContent).toContain("downloads.admin.unlimitedShort"); }); it("a user's menu edits their quota, and resets it only through the confirm", async () => { vi.spyOn(api, "adminGetDownloadQuota").mockReturnValue(new Promise(() => {}) as never); const reset = vi.spyOn(api, "adminResetDownloadQuota").mockResolvedValue({} as never); mount(); await openMenu(rowOf("anna@x")); expect(menuRows()).toEqual(["downloads.admin.editQuota", "downloads.admin.reset"]); mocks.confirm.mockResolvedValueOnce(false); pick("downloads.admin.reset"); await waitFor(() => expect(mocks.confirm).toHaveBeenCalledTimes(1)); expect(reset).not.toHaveBeenCalled(); await openMenu(rowOf("anna@x")); pick("downloads.admin.reset"); await waitFor(() => expect(reset).toHaveBeenCalledWith(4)); await openMenu(rowOf("anna@x")); pick("downloads.admin.editQuota"); expect(await screen.findByText("downloads.admin.quotaTitle:anna@x")).toBeTruthy(); expect(api.adminGetDownloadQuota).toHaveBeenCalledWith(4); }); it("a download's menu opens its source, copies, and opens its OWNER's quota", async () => { vi.spyOn(api, "adminGetDownloadQuota").mockReturnValue(new Promise(() => {}) as never); const open = vi.spyOn(window, "open").mockReturnValue(null); mount(); await openMenu(rowOf("Clip 7")); expect(menuRows()).toEqual([ "downloads.admin.openSource", "common.copy.title", "common.copy.link", "downloads.admin.editUserQuota", ]); pick("downloads.admin.openSource"); expect(open).toHaveBeenCalledWith("https://example.com/7", "_blank", "noopener,noreferrer"); // One with no source link offers neither the link nor its copy. await openMenu(rowOf("Clip 8")); expect(menuRows()).toEqual(["common.copy.title", "downloads.admin.editUserQuota"]); pick("downloads.admin.editUserQuota"); await waitFor(() => expect(api.adminGetDownloadQuota).toHaveBeenCalledWith(1)); }); });