import { describe, it, expect, vi, afterEach } from "vitest"; import { render, screen, cleanup, fireEvent, waitFor, } from "@testing-library/react"; const { createApiKeyMock } = vi.hoisted(() => ({ createApiKeyMock: vi.fn(), })); vi.mock("@/main-axios", async (importOriginal) => ({ ...(await importOriginal()), createApiKey: createApiKeyMock, })); vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key }), })); // Panel-level dependencies that are irrelevant to AccordionSection but would // otherwise be pulled in by importing the module. vi.mock("@/plugin-host/shell-bridge", () => ({ shell: { openRailView: vi.fn() }, })); vi.mock("@/i18n/i18n", () => ({ changeAppLanguage: vi.fn(), normalizeLanguageCode: (code: string) => code, })); vi.mock("@/components/theme-provider", () => ({ useTheme: () => ({ theme: "dark", setTheme: vi.fn() }), })); vi.mock("@/lib/electron", () => ({ isElectron: () => false })); import { AccordionSection, NewApiKeyDialog, } from "../../sidebar/UserProfilePanel"; afterEach(async () => { cleanup(); // Radix restores focus on a timer; let it finish before jsdom is torn down. await new Promise((resolve) => setTimeout(resolve, 0)); }); function renderSection(hidden: boolean, open = false, onToggle = vi.fn()) { render( } open={open} onToggle={onToggle} hidden={hidden} > , ); return onToggle; } describe("AccordionSection", () => { it("renders its header when visible, and its content once expanded", () => { renderSection(false); expect(screen.getByText("Security")).toBeTruthy(); expect(screen.queryByText("Change password")).toBeNull(); cleanup(); renderSection(false, true); expect(screen.getByText("Security")).toBeTruthy(); expect(screen.getByText("Change password")).toBeTruthy(); }); it("renders nothing at all when hidden, even expanded", () => { renderSection(true, true); expect(screen.queryByText("Security")).toBeNull(); // The children must not reach the DOM either: on the desktop build this // section holds password and 2FA controls for an account that signs in // automatically and has no login password, so merely collapsing it would // still imply a protection that is not there. expect(screen.queryByText("Change password")).toBeNull(); }); it("is visible by default when hidden is not passed", () => { render( } open={false} onToggle={vi.fn()} > body , ); expect(screen.getByText("Account")).toBeTruthy(); }); it("reports its expanded state and toggles on click", () => { const onToggle = renderSection(false, false); const header = screen.getByRole("button", { name: /Security/ }); expect(header.getAttribute("aria-expanded")).toBe("false"); expect(header.getAttribute("aria-controls")).toBe("security-content"); fireEvent.click(header); expect(onToggle).toHaveBeenCalledOnce(); }); it("cannot be toggled while hidden, since there is no header to click", () => { const onToggle = renderSection(true, false); expect(screen.queryByRole("button", { name: /Security/ })).toBeNull(); expect(onToggle).not.toHaveBeenCalled(); }); }); describe("NewApiKeyDialog", () => { it("keeps the one-time token visible until Done is clicked", async () => { createApiKeyMock.mockResolvedValue({ id: "key-1", name: "deploy", userId: "user-1", username: "user", tokenPrefix: "tmx_test", token: "tmx_test_secret", createdAt: new Date().toISOString(), expiresAt: null, lastUsedAt: null, isActive: true, }); const onOpenChange = vi.fn(); render( , ); fireEvent.change( screen.getByPlaceholderText( "newUi.sidebar.userProfile.apiKeyNamePlaceholder", ), { target: { value: "deploy" } }, ); fireEvent.click( screen.getByRole("button", { name: /newUi.sidebar.userProfile.createKey/, }), ); await waitFor(() => { expect(screen.getByText("tmx_test_secret")).toBeTruthy(); }); expect(onOpenChange).not.toHaveBeenCalledWith(false); fireEvent.click( screen.getByRole("button", { name: "newUi.sidebar.userProfile.done", }), ); expect(onOpenChange).toHaveBeenCalledWith(false); }); });