Loading panel/src/components/players/BansSection.tsx +2 −1 Changes for panel/src/components/players/BansSection.tsx: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ import { cn } from "@/lib/utils"; import { CollapsibleSection, FeedbackLine, LoadError, MC_NAME, PagerFooter, PlayerField, Loading Loading @@ -175,7 +176,7 @@ export function BansSection({ name }: { name: string }) { <Loader2 className="h-3.5 w-3.5 animate-spin" /> {t("log_connecting")} </div> ) : error ? ( <p role="alert" className="text-xs text-destructive">{t("access_ban_load_error")}</p> <LoadError message={t("access_ban_load_error")} error={error} /> ) : players.length === 0 ? ( <div className="rounded-md border border-dashed border-border bg-muted/20 px-4 py-8 text-center"> <p className="text-sm text-muted-foreground">{t("access_ban_empty")}</p> Loading panel/src/components/players/LoadError.test.tsx 0 → 100644 +44 −0 Changes for panel/src/components/players/LoadError.test.tsx: 44 added lines, 0 removed lines. Original line number Diff line number Diff line // @vitest-environment jsdom import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; import type { ReactElement } from "react"; import i18next from "i18next"; import { humanizeError } from "@/lib/api"; import { OnlineSection } from "./OnlineSection"; import { WhitelistSection } from "./WhitelistSection"; import { BansSection } from "./BansSection"; import { WakeListSection } from "./WakeListSection"; const calls = vi.hoisted(() => ({ accessPlayers: vi.fn(), accessWhitelistList: vi.fn(), accessBanList: vi.fn(), serverAllowlist: vi.fn(), })); vi.mock("@/lib/api", async (importOriginal) => { const actual = await importOriginal<typeof import("@/lib/api")>(); return { ...actual, api: { ...actual.api, ...calls } }; }); beforeEach(() => { for (const fn of Object.values(calls)) fn.mockReset(); }); // A lapsed session and a server mid-restart want different next steps, so a // section whose list failed says which one it hit. describe("a player list that failed to load", () => { it.each<[string, keyof typeof calls, string, ReactElement]>([ ["online players", "accessPlayers", "servers:access_online_load_error", <OnlineSection name="lobby" />], ["whitelist", "accessWhitelistList", "servers:access_whitelist_load_error", <WhitelistSection name="lobby" />], ["ban list", "accessBanList", "servers:access_ban_load_error", <BansSection name="lobby" />], ["wake list", "serverAllowlist", "servers:wake_list_load_error", <WakeListSection name="lobby" policy="allowlist" />], ])("names why the %s did not load", async (_label, call, key, section) => { const outage = { status: 409, code: "not_running", message: "server is not running" }; calls[call].mockRejectedValue(outage); render(section); const alert = await screen.findByRole("alert"); expect(alert.textContent).toBe(`${i18next.t(key)} ${humanizeError(outage)}`); expect(humanizeError(outage)).not.toBe(""); }); }); panel/src/components/players/OnlineSection.tsx +2 −1 Changes for panel/src/components/players/OnlineSection.tsx: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ import { useAsync } from "@/lib/hooks"; import { CollapsibleSection, FeedbackLine, LoadError, PagerFooter, rconReply, SearchBox, Loading Loading @@ -112,7 +113,7 @@ export function OnlineSection({ name, defaultOpen = true }: { name: string; defa <Loader2 className="h-3.5 w-3.5 animate-spin" /> {t("log_connecting")} </div> ) : error ? ( <p role="alert" className="text-xs text-destructive">{t("access_online_load_error")}</p> <LoadError message={t("access_online_load_error")} error={error} /> ) : players.length === 0 ? ( <div className="rounded-md border border-dashed border-border bg-muted/20 px-4 py-8 text-center"> {namesUnavailable ? ( Loading panel/src/components/players/WakeListSection.test.tsx +3 −1 Changes for panel/src/components/players/WakeListSection.test.tsx: 3 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -103,7 +103,9 @@ describe("WakeListSection", () => { calls.serverAllowlist.mockRejectedValue({ status: 500, code: "internal", message: "db down" }); render(<WakeListSection name="lobby" policy="allowlist" defaultOpen />); expect((await screen.findByRole("alert")).textContent).toBe("Couldn't load the wake list."); expect((await screen.findByRole("alert")).textContent).toBe( "Couldn't load the wake list. The service is unavailable right now (it may be restarting or upgrading). Try again shortly.", ); }); it("finds a player by account name or by UUID once the list is long", async () => { Loading panel/src/components/players/WakeListSection.tsx +2 −4 Changes for panel/src/components/players/WakeListSection.tsx: 2 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -8,7 +8,7 @@ import { formatAbsolute, formatRelative } from "@/lib/format"; import { useAsync } from "@/lib/hooks"; import type { AllowlistEntry, AutostartPolicy } from "@/lib/types"; import { cn } from "@/lib/utils"; import { CollapsibleSection, FeedbackLine, PagerFooter, SearchBox, usePagedNames, type Feedback } from "./shared"; import { CollapsibleSection, FeedbackLine, LoadError, PagerFooter, SearchBox, usePagedNames, type Feedback } from "./shared"; /** WakeListSection shows who may wake the server while it sleeps under the "wake * list" autostart policy, and lets the owner take that right away or give it back. Loading Loading @@ -100,9 +100,7 @@ export function WakeListSection({ <Loader2 className="h-3.5 w-3.5 animate-spin" /> {t("log_connecting")} </div> ) : error ? ( <p role="alert" className="text-xs text-destructive"> {t("wake_list_load_error")} </p> <LoadError message={t("wake_list_load_error")} error={error} /> ) : entries.length === 0 ? ( <div className="rounded-md border border-dashed border-border bg-muted/20 px-4 py-8 text-center"> <p className="text-sm text-muted-foreground">{t("wake_list_empty")}</p> Loading Loading
panel/src/components/players/BansSection.tsx +2 −1 Changes for panel/src/components/players/BansSection.tsx: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ import { cn } from "@/lib/utils"; import { CollapsibleSection, FeedbackLine, LoadError, MC_NAME, PagerFooter, PlayerField, Loading Loading @@ -175,7 +176,7 @@ export function BansSection({ name }: { name: string }) { <Loader2 className="h-3.5 w-3.5 animate-spin" /> {t("log_connecting")} </div> ) : error ? ( <p role="alert" className="text-xs text-destructive">{t("access_ban_load_error")}</p> <LoadError message={t("access_ban_load_error")} error={error} /> ) : players.length === 0 ? ( <div className="rounded-md border border-dashed border-border bg-muted/20 px-4 py-8 text-center"> <p className="text-sm text-muted-foreground">{t("access_ban_empty")}</p> Loading
panel/src/components/players/LoadError.test.tsx 0 → 100644 +44 −0 Changes for panel/src/components/players/LoadError.test.tsx: 44 added lines, 0 removed lines. Original line number Diff line number Diff line // @vitest-environment jsdom import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; import type { ReactElement } from "react"; import i18next from "i18next"; import { humanizeError } from "@/lib/api"; import { OnlineSection } from "./OnlineSection"; import { WhitelistSection } from "./WhitelistSection"; import { BansSection } from "./BansSection"; import { WakeListSection } from "./WakeListSection"; const calls = vi.hoisted(() => ({ accessPlayers: vi.fn(), accessWhitelistList: vi.fn(), accessBanList: vi.fn(), serverAllowlist: vi.fn(), })); vi.mock("@/lib/api", async (importOriginal) => { const actual = await importOriginal<typeof import("@/lib/api")>(); return { ...actual, api: { ...actual.api, ...calls } }; }); beforeEach(() => { for (const fn of Object.values(calls)) fn.mockReset(); }); // A lapsed session and a server mid-restart want different next steps, so a // section whose list failed says which one it hit. describe("a player list that failed to load", () => { it.each<[string, keyof typeof calls, string, ReactElement]>([ ["online players", "accessPlayers", "servers:access_online_load_error", <OnlineSection name="lobby" />], ["whitelist", "accessWhitelistList", "servers:access_whitelist_load_error", <WhitelistSection name="lobby" />], ["ban list", "accessBanList", "servers:access_ban_load_error", <BansSection name="lobby" />], ["wake list", "serverAllowlist", "servers:wake_list_load_error", <WakeListSection name="lobby" policy="allowlist" />], ])("names why the %s did not load", async (_label, call, key, section) => { const outage = { status: 409, code: "not_running", message: "server is not running" }; calls[call].mockRejectedValue(outage); render(section); const alert = await screen.findByRole("alert"); expect(alert.textContent).toBe(`${i18next.t(key)} ${humanizeError(outage)}`); expect(humanizeError(outage)).not.toBe(""); }); });
panel/src/components/players/OnlineSection.tsx +2 −1 Changes for panel/src/components/players/OnlineSection.tsx: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ import { useAsync } from "@/lib/hooks"; import { CollapsibleSection, FeedbackLine, LoadError, PagerFooter, rconReply, SearchBox, Loading Loading @@ -112,7 +113,7 @@ export function OnlineSection({ name, defaultOpen = true }: { name: string; defa <Loader2 className="h-3.5 w-3.5 animate-spin" /> {t("log_connecting")} </div> ) : error ? ( <p role="alert" className="text-xs text-destructive">{t("access_online_load_error")}</p> <LoadError message={t("access_online_load_error")} error={error} /> ) : players.length === 0 ? ( <div className="rounded-md border border-dashed border-border bg-muted/20 px-4 py-8 text-center"> {namesUnavailable ? ( Loading
panel/src/components/players/WakeListSection.test.tsx +3 −1 Changes for panel/src/components/players/WakeListSection.test.tsx: 3 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -103,7 +103,9 @@ describe("WakeListSection", () => { calls.serverAllowlist.mockRejectedValue({ status: 500, code: "internal", message: "db down" }); render(<WakeListSection name="lobby" policy="allowlist" defaultOpen />); expect((await screen.findByRole("alert")).textContent).toBe("Couldn't load the wake list."); expect((await screen.findByRole("alert")).textContent).toBe( "Couldn't load the wake list. The service is unavailable right now (it may be restarting or upgrading). Try again shortly.", ); }); it("finds a player by account name or by UUID once the list is long", async () => { Loading
panel/src/components/players/WakeListSection.tsx +2 −4 Changes for panel/src/components/players/WakeListSection.tsx: 2 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -8,7 +8,7 @@ import { formatAbsolute, formatRelative } from "@/lib/format"; import { useAsync } from "@/lib/hooks"; import type { AllowlistEntry, AutostartPolicy } from "@/lib/types"; import { cn } from "@/lib/utils"; import { CollapsibleSection, FeedbackLine, PagerFooter, SearchBox, usePagedNames, type Feedback } from "./shared"; import { CollapsibleSection, FeedbackLine, LoadError, PagerFooter, SearchBox, usePagedNames, type Feedback } from "./shared"; /** WakeListSection shows who may wake the server while it sleeps under the "wake * list" autostart policy, and lets the owner take that right away or give it back. Loading Loading @@ -100,9 +100,7 @@ export function WakeListSection({ <Loader2 className="h-3.5 w-3.5 animate-spin" /> {t("log_connecting")} </div> ) : error ? ( <p role="alert" className="text-xs text-destructive"> {t("wake_list_load_error")} </p> <LoadError message={t("wake_list_load_error")} error={error} /> ) : entries.length === 0 ? ( <div className="rounded-md border border-dashed border-border bg-muted/20 px-4 py-8 text-center"> <p className="text-sm text-muted-foreground">{t("wake_list_empty")}</p> Loading