From 5282d552143854830773b4b1de7e6254b4abad09 Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sun, 27 Sep 2026 15:53:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(panel):=20=E4=BE=A7=E6=A0=8F=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=8D=A1=E6=8C=89=E7=95=8C=E9=9D=A2=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E8=A7=92=E8=89=B2=E5=90=8D=EF=BC=8C=E8=BA=AB?= =?UTF-8?q?=E4=BB=BD=E6=9C=AA=E8=AF=BB=E5=88=B0=E6=97=B6=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=A8=AA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- panel/src/components/AppShell.test.tsx | 63 ++++++++++++++++++++++++++ panel/src/components/AppShell.tsx | 11 +++-- panel/src/components/RoleBadge.tsx | 5 +- 3 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 panel/src/components/AppShell.test.tsx diff --git a/panel/src/components/AppShell.test.tsx b/panel/src/components/AppShell.test.tsx new file mode 100644 index 0000000..2821172 --- /dev/null +++ b/panel/src/components/AppShell.test.tsx @@ -0,0 +1,63 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, afterEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import i18next from "i18next"; +import { AppShell } from "./AppShell"; +import type { Identity } from "@/lib/types"; + +const tier = vi.hoisted(() => ({ + identity: null as Identity | null, + isAdmin: false, + isOwner: false, + loading: false, + refresh: () => Promise.resolve(), +})); +vi.mock("@/lib/tier", () => ({ useTier: () => tier })); +vi.mock("@/lib/config", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, loadConfig: () => Promise.resolve({}) }; +}); + +afterEach(() => { + tier.identity = null; + return i18next.changeLanguage("en-US"); +}); + +function renderShell() { + render( + + + }> + page

} /> +
+
+
, + ); +} + +// The line under the email in the sidebar's user card. +function roleLine(email: string) { + return screen.getByText(email).nextElementSibling?.textContent; +} + +describe("AppShell user card", () => { + it.each([ + ["en-US", "owner", "Owner"], + ["en-US", "user", "User"], + ["zh-CN", "admin", "管理员"], + ["zh-CN", "user", "普通用户"], + ] as const)("names the role in %s: %s is %s", async (lang, role, shown) => { + await i18next.changeLanguage(lang); + tier.identity = { user_id: "u-1", email: "a@example.test", role } as Identity; + renderShell(); + expect(roleLine("a@example.test")).toBe(shown); + }); + + it("names no role before it has read one", () => { + renderShell(); + const dashes = screen.getAllByText("—"); + expect(dashes).toHaveLength(2); + expect(screen.queryByText(/^user$/i)).toBeNull(); + }); +}); diff --git a/panel/src/components/AppShell.tsx b/panel/src/components/AppShell.tsx index a416558..8fe7d08 100644 --- a/panel/src/components/AppShell.tsx +++ b/panel/src/components/AppShell.tsx @@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button"; import { ConfigBanner, VersionBadge } from "@/components/RuntimeStatus"; import { ErrorBoundary } from "@/components/ErrorBoundary"; import { Loading } from "@/components/States"; +import { ROLE_LABEL_KEY } from "@/components/RoleBadge"; function SectionGroup({ section, @@ -71,9 +72,9 @@ const FOOT_ICON_BTN = function UserStrip() { // The sidebar foot identifies the principal and exposes one action — sign out. - // identity?.email is the only display-safe field (user_id is a UUID, role is - // server-truth not display). While /me is loading or has failed we render a - // muted placeholder rather than a broken row, so the strip never flashes empty. + // It shows the email and the role by its name in the UI language (user_id is a + // UUID). While /me is loading or has failed both lines hold a muted dash, so + // the strip never flashes empty and never names a role it has not read. const { identity, refresh } = useTier(); const { t } = useTranslation("account"); const [signingOut, setSigningOut] = useState(false); @@ -99,8 +100,8 @@ function UserStrip() { > {identity?.email ?? —} -
- {identity?.role ?? "user"} +
+ {identity ? t(`admin:${ROLE_LABEL_KEY[identity.role]}`) : —}