diff --git a/frontend/src/components/downloadAdmin.dom.test.tsx b/frontend/src/components/downloadAdmin.dom.test.tsx new file mode 100644 index 00000000..1fcd3a56 --- /dev/null +++ b/frontend/src/components/downloadAdmin.dom.test.tsx @@ -0,0 +1,75 @@ +// @vitest-environment jsdom +// +// E32 S2: the admin's download-quota editor rendered NOTHING while its quota loaded and nothing when +// the load failed, so the click looked dead. It now opens at once and says which it is. The save +// turns the GB field back into bytes; the reset drops the override. +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { ReactNode } from "react"; +import { api } from "../lib/api"; +import { QuotaModal } from "./downloadAdmin"; + +vi.mock("react-i18next", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }), + }; +}); + +const GiB = 1024 ** 3; +const QUOTA = { max_bytes: 5 * GiB, max_jobs: 10, max_concurrent: 2, unlimited: false }; +let qc: QueryClient; +const wrap = (ui: ReactNode) => {ui}; +const open = (onClose = () => {}) => + render(wrap()); + +beforeEach(() => { + qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); +}); +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +describe("the download-quota editor", () => { + it("opens at once and says it is loading", () => { + vi.spyOn(api, "adminGetDownloadQuota").mockReturnValue(new Promise(() => {}) as never); + open(); + expect(screen.getByText("downloads.admin.quotaTitle")).toBeTruthy(); + expect(screen.getAllByRole("status").length).toBeGreaterThan(0); + }); + + it("says a failed load failed, and retries it", async () => { + const get = vi + .spyOn(api, "adminGetDownloadQuota") + .mockRejectedValueOnce(new Error("down")) + .mockResolvedValueOnce(QUOTA as never); + open(); + expect((await screen.findByRole("alert")).textContent).toContain("common.loadError"); + fireEvent.click(screen.getByText("common.retry")); + await screen.findByDisplayValue("5"); + expect(get).toHaveBeenCalledTimes(2); + }); + + it("saves the GB field as bytes, and the reset drops the override", async () => { + vi.spyOn(api, "adminGetDownloadQuota").mockResolvedValue(QUOTA as never); + const set = vi.spyOn(api, "adminSetDownloadQuota").mockResolvedValue(QUOTA as never); + const reset = vi.spyOn(api, "adminResetDownloadQuota").mockResolvedValue(QUOTA as never); + const onClose = vi.fn(); + open(onClose); + fireEvent.change(await screen.findByDisplayValue("5"), { target: { value: "1.5" } }); + fireEvent.click(screen.getByText("downloads.admin.save")); + await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1)); + expect(set).toHaveBeenCalledWith(4, { + max_bytes: Math.round(1.5 * GiB), + max_jobs: 10, + max_concurrent: 2, + unlimited: false, + }); + + fireEvent.click(screen.getByText("downloads.admin.reset")); + await waitFor(() => expect(reset).toHaveBeenCalledWith(4)); + }); +}); diff --git a/frontend/src/components/downloadAdmin.tsx b/frontend/src/components/downloadAdmin.tsx index 4ed7ecc9..30af39a8 100644 --- a/frontend/src/components/downloadAdmin.tsx +++ b/frontend/src/components/downloadAdmin.tsx @@ -13,6 +13,7 @@ import { jobTitle } from "./downloadTitle"; import { IconButton } from "./ui/IconButton"; import { Input } from "./ui/Input"; import { CheckRow } from "./ui/NativeControls"; +import { LoadingState, StateMessage } from "./ui/QueryState"; import { SURFACE } from "./ui/surfaces"; import UserPicker from "./UserPicker"; @@ -21,7 +22,8 @@ import UserPicker from "./UserPicker"; const GiB = 1024 ** 3; -function QuotaModal({ +/** @internal exported for downloadAdmin.dom.test.tsx */ +export function QuotaModal({ userId, email, onClose, @@ -74,66 +76,80 @@ function QuotaModal({ onSuccess: done, meta: { failTitle: "errors.writeFailed.downloadQuotaReset" }, }); - if (!state) return null; - const upd = (p: Partial) => setForm({ ...state, ...p }); + const upd = (p: Partial>) => { + if (state) setForm({ ...state, ...p }); + }; return ( -
-
- - - -
- upd({ unlimited: checked })} - className="text-sm" - > - {t("downloads.admin.unlimited")} - - - - - -
+ {t("downloads.admin.unlimited")} + + + + + + + )}
); }