// @vitest-environment jsdom // // A failed user action is said once, and a user who is gone is not left listed (deep-review round 4). // setRole / suspend / delete had no onError at all — "the global error dialog says it" — but a 404 // (the user was deleted in another tab) gets no dialog, so the click did nothing and the row stayed. // And the invite approve/deny toasted unconditionally on top of the dialog `req` opens for a 409. import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, cleanup, screen, fireEvent, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }), }; }); vi.mock("../lib/useMe", () => ({ useMe: () => ({ id: 1, email: "admin@x", role: "admin", pending_invites: 0, pending_activations: 0, }), })); vi.mock("./ConfirmProvider", () => ({ useConfirm: () => async () => true })); const { notify } = vi.hoisted(() => ({ notify: vi.fn((_n: { level: string; message: string }) => 0), })); vi.mock("../lib/notifications", async (importOriginal) => ({ ...(await importOriginal()), notify, })); vi.mock("../lib/errorDialog", () => ({ reportError: vi.fn() })); import AdminUsers from "./AdminUsers"; import { api, type AdminUserRow } from "../lib/api"; import i18n from "../i18n"; const row = { id: 2, email: "gone@x", role: "user", is_suspended: false, is_demo: false, email_verified: true, is_active: true, } as unknown as AdminUserRow; function answer(status: number, detail: unknown) { return new Response(JSON.stringify({ detail }), { status, headers: { "content-type": "application/json" }, }); } beforeEach(() => { notify.mockClear(); localStorage.clear(); }); afterEach(() => { cleanup(); vi.restoreAllMocks(); }); function mount() { const users = vi.spyOn(api, "adminUsers").mockResolvedValue([row] as never); render( , ); return users; } describe("a user action that fails", () => { it("says a user who is gone, and refetches the list", async () => { const users = mount(); vi.spyOn(globalThis, "fetch").mockResolvedValue( answer(404, { code: "not_found", reason: "user" }), ); fireEvent.click(await screen.findByText("users.roles.makeAdmin")); await waitFor(() => expect(notify).toHaveBeenCalledWith({ level: "error", message: i18n.t("errors.notFound.user"), // the request layer localizes a structured detail }), ); await waitFor(() => expect(users).toHaveBeenCalledTimes(2)); }); it("is not toasted on top of the error dialog the request already opened", async () => { mount(); vi.spyOn(globalThis, "fetch").mockResolvedValue(answer(409, "Cannot demote the last admin")); fireEvent.click(await screen.findByText("users.roles.makeAdmin")); const { reportError } = await import("../lib/errorDialog"); await waitFor(() => expect(reportError).toHaveBeenCalled()); expect(notify.mock.calls.filter(([n]) => n.level === "error")).toEqual([]); }); });