fix(downloads): the quota editor opens at once and says it is loading or failed

It rendered nothing until its quota arrived and nothing at all when the load failed, so the
click looked dead. The first field still takes focus when the form arrives.
This commit is contained in:
2026-10-03 21:53:06 +02:00
parent 2ab9ff6a42
commit 2e2d6f5746
2 changed files with 145 additions and 54 deletions
@@ -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<typeof import("react-i18next")>();
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) => <QueryClientProvider client={qc}>{ui}</QueryClientProvider>;
const open = (onClose = () => {}) =>
render(wrap(<QuotaModal userId={4} email="a@b.c" onClose={onClose} />));
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));
});
});
+19 -3
View File
@@ -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,14 +76,27 @@ function QuotaModal({
onSuccess: done,
meta: { failTitle: "errors.writeFailed.downloadQuotaReset" },
});
if (!state) return null;
const upd = (p: Partial<typeof state>) => setForm({ ...state, ...p });
const upd = (p: Partial<NonNullable<typeof state>>) => {
if (state) setForm({ ...state, ...p });
};
return (
<Modal
initialFocus="field"
title={t("downloads.admin.quotaTitle", { email })}
onClose={onClose}
>
{/* The modal opens at once: while the quota loads it says so, and a failed load says THAT,
with a retry — it used to render nothing at all, so the click looked dead (E32 S2). The
first field still takes focus when the form arrives (Modal's initialFocus="field"). */}
{!state ? (
q.isError ? (
<StateMessage tone="error" onRetry={() => void q.refetch()}>
{t("common.loadError")}
</StateMessage>
) : (
<LoadingState />
)
) : (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<label className="text-sm">
@@ -134,6 +149,7 @@ function QuotaModal({
</Button>
</ModalFooter>
</div>
)}
</Modal>
);
}