mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
* fix(ci): build plugin SDK before dependent checks * fix(mysql): keep plugin settings scope indexes within limits * test(mysql): refresh reviewed plugin upgrade SQL * fix(file-manager): preserve selection feedback on hover * test(ui): await dialog focus cleanup before teardown
172 lines
4.7 KiB
TypeScript
172 lines
4.7 KiB
TypeScript
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<typeof import("@/main-axios")>()),
|
|
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<void>((resolve) => setTimeout(resolve, 0));
|
|
});
|
|
|
|
function renderSection(hidden: boolean, open = false, onToggle = vi.fn()) {
|
|
render(
|
|
<AccordionSection
|
|
id="security"
|
|
label="Security"
|
|
icon={<span data-testid="icon" />}
|
|
open={open}
|
|
onToggle={onToggle}
|
|
hidden={hidden}
|
|
>
|
|
<button>Change password</button>
|
|
</AccordionSection>,
|
|
);
|
|
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(
|
|
<AccordionSection
|
|
id="account"
|
|
label="Account"
|
|
icon={<span />}
|
|
open={false}
|
|
onToggle={vi.fn()}
|
|
>
|
|
<span>body</span>
|
|
</AccordionSection>,
|
|
);
|
|
|
|
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(
|
|
<NewApiKeyDialog
|
|
open
|
|
onOpenChange={onOpenChange}
|
|
onAdd={vi.fn()}
|
|
userId="user-1"
|
|
/>,
|
|
);
|
|
|
|
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);
|
|
});
|
|
});
|