2026-10-01 16:20:16 -05:00
|
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
2026-10-10 14:58:27 -05:00
|
|
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
2026-10-01 16:20:16 -05:00
|
|
|
import userEvent from "@testing-library/user-event";
|
|
|
|
|
import { Bell, Boxes } from "lucide-react";
|
|
|
|
|
import { AppRail } from "../../sidebar/AppRail";
|
|
|
|
|
import { RailBadge } from "../../sidebar/RailBadge";
|
|
|
|
|
import {
|
|
|
|
|
registerRailItem,
|
|
|
|
|
resetRegisteredRailItems,
|
|
|
|
|
} from "../../sidebar/rail-items";
|
|
|
|
|
|
|
|
|
|
vi.mock("react-i18next", () => ({
|
|
|
|
|
useTranslation: () => ({ t: (key: string) => key }),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-10-10 14:58:27 -05:00
|
|
|
vi.mock("@/main-axios", () => ({
|
|
|
|
|
getUserPreferences: vi.fn(async () => ({ storageMode: "local" })),
|
|
|
|
|
saveUserPreferences: vi.fn(async () => undefined),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-10-01 16:20:16 -05:00
|
|
|
vi.mock("@/hooks/use-permissions", () => ({
|
|
|
|
|
usePermissions: () => ({ has: () => true, loaded: true }),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
resetRegisteredRailItems();
|
|
|
|
|
localStorage.clear();
|
2026-10-10 14:58:27 -05:00
|
|
|
delete (window as { IS_ELECTRON?: boolean }).IS_ELECTRON;
|
2026-10-01 16:20:16 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function renderRail(onRailClick = vi.fn()) {
|
|
|
|
|
render(
|
|
|
|
|
<AppRail
|
|
|
|
|
railView="hosts"
|
|
|
|
|
sidebarOpen={false}
|
|
|
|
|
username="alice"
|
|
|
|
|
isAdmin={false}
|
|
|
|
|
onRailClick={onRailClick}
|
2026-10-10 14:58:27 -05:00
|
|
|
onOpenSettings={vi.fn()}
|
|
|
|
|
onOpenNavigationSettings={vi.fn()}
|
2026-10-01 16:20:16 -05:00
|
|
|
onLogout={vi.fn()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
return onRailClick;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("AppRail", () => {
|
2026-10-10 14:58:27 -05:00
|
|
|
it("shows logout in the browser but not on the desktop", () => {
|
|
|
|
|
renderRail();
|
|
|
|
|
expect(screen.getByLabelText("common.logout")).toBeTruthy();
|
|
|
|
|
cleanup();
|
|
|
|
|
(window as { IS_ELECTRON?: boolean }).IS_ELECTRON = true;
|
|
|
|
|
renderRail();
|
|
|
|
|
expect(screen.queryByLabelText("common.logout")).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-18 00:34:50 -05:00
|
|
|
it("expands over the content on hover without taking more room", () => {
|
|
|
|
|
renderRail();
|
|
|
|
|
const rail = screen
|
|
|
|
|
.getByLabelText("common.logout")
|
|
|
|
|
.closest(".absolute") as HTMLElement;
|
|
|
|
|
const slot = rail.parentElement as HTMLElement;
|
|
|
|
|
const collapsed = slot.style.width;
|
|
|
|
|
fireEvent.mouseEnter(rail);
|
|
|
|
|
expect(rail.style.width).not.toBe(collapsed);
|
|
|
|
|
expect(slot.style.width).toBe(collapsed);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("pushes the content on hover when the user asks for it", () => {
|
|
|
|
|
localStorage.setItem("appRailHoverPushesContent", "true");
|
|
|
|
|
renderRail();
|
|
|
|
|
const rail = screen
|
|
|
|
|
.getByLabelText("common.logout")
|
|
|
|
|
.closest(".absolute") as HTMLElement;
|
|
|
|
|
const slot = rail.parentElement as HTMLElement;
|
|
|
|
|
fireEvent.mouseEnter(rail);
|
|
|
|
|
expect(slot.style.width).toBe(rail.style.width);
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-01 16:20:16 -05:00
|
|
|
it("puts a footer item above the profile with its badge", async () => {
|
|
|
|
|
registerRailItem({
|
|
|
|
|
id: "inbox",
|
|
|
|
|
icon: Bell,
|
|
|
|
|
labelKey: "test.inbox",
|
|
|
|
|
placement: "footer",
|
|
|
|
|
useBadge: () => 3,
|
|
|
|
|
pluginId: "demo",
|
|
|
|
|
});
|
|
|
|
|
registerRailItem({
|
|
|
|
|
id: "boxes",
|
|
|
|
|
icon: Boxes,
|
|
|
|
|
labelKey: "test.boxes",
|
|
|
|
|
pluginId: "demo",
|
|
|
|
|
});
|
|
|
|
|
const onRailClick = renderRail();
|
|
|
|
|
|
|
|
|
|
const inbox = screen.getByTitle("test.inbox");
|
2026-10-10 14:58:27 -05:00
|
|
|
const profile = screen.getAllByTitle("nav.openSettings")[0];
|
2026-10-01 16:20:16 -05:00
|
|
|
const boxes = screen.getByTitle("test.boxes");
|
|
|
|
|
// Footer items render after the main list and before the profile.
|
|
|
|
|
expect(
|
|
|
|
|
boxes.compareDocumentPosition(inbox) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
|
|
|
).toBeTruthy();
|
|
|
|
|
expect(
|
|
|
|
|
inbox.compareDocumentPosition(profile) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
|
|
|
).toBeTruthy();
|
|
|
|
|
expect(inbox.textContent).toContain("3");
|
|
|
|
|
|
|
|
|
|
await userEvent.click(inbox);
|
|
|
|
|
expect(onRailClick).toHaveBeenCalledWith("inbox");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("hides a footer item the user hid from Navigation", () => {
|
|
|
|
|
localStorage.setItem("hiddenRailTabs", JSON.stringify(["inbox"]));
|
|
|
|
|
registerRailItem({
|
|
|
|
|
id: "inbox",
|
|
|
|
|
icon: Bell,
|
|
|
|
|
labelKey: "test.inbox",
|
|
|
|
|
placement: "footer",
|
|
|
|
|
pluginId: "demo",
|
|
|
|
|
});
|
|
|
|
|
renderRail();
|
|
|
|
|
expect(screen.queryByTitle("test.inbox")).toBeNull();
|
|
|
|
|
});
|
2026-10-10 14:58:27 -05:00
|
|
|
|
|
|
|
|
it("groups items into bands with a labelled divider", () => {
|
|
|
|
|
registerRailItem({
|
|
|
|
|
id: "fleets",
|
|
|
|
|
icon: Boxes,
|
|
|
|
|
labelKey: "test.fleets",
|
|
|
|
|
group: "objects",
|
|
|
|
|
pluginId: "demo",
|
|
|
|
|
});
|
|
|
|
|
renderRail();
|
|
|
|
|
expect(screen.getByText("nav.group.tools")).toBeTruthy();
|
|
|
|
|
const fleets = screen.getByTitle("test.fleets");
|
|
|
|
|
const connections = screen.getByTitle("nav.connections");
|
|
|
|
|
expect(
|
|
|
|
|
fleets.compareDocumentPosition(connections) &
|
|
|
|
|
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
|
|
|
).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("lists hidden items and brings one back", async () => {
|
|
|
|
|
localStorage.setItem("hiddenRailTabs", JSON.stringify(["connections"]));
|
|
|
|
|
renderRail();
|
|
|
|
|
expect(screen.queryByTitle("nav.connections")).toBeNull();
|
|
|
|
|
await userEvent.click(screen.getByTitle("nav.hiddenCount"));
|
|
|
|
|
await userEvent.click(screen.getByTitle("nav.showInRail"));
|
|
|
|
|
expect(JSON.parse(localStorage.getItem("hiddenRailTabs")!)).toEqual([]);
|
|
|
|
|
expect(await screen.findByTitle("nav.connections")).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("opens plain settings from the account button", async () => {
|
|
|
|
|
const onOpenSettings = vi.fn();
|
|
|
|
|
const onOpenNavigationSettings = vi.fn();
|
|
|
|
|
render(
|
|
|
|
|
<AppRail
|
|
|
|
|
railView="hosts"
|
|
|
|
|
sidebarOpen={false}
|
|
|
|
|
username="alice"
|
|
|
|
|
isAdmin
|
|
|
|
|
onRailClick={vi.fn()}
|
|
|
|
|
onOpenSettings={onOpenSettings}
|
|
|
|
|
onOpenNavigationSettings={onOpenNavigationSettings}
|
|
|
|
|
onLogout={vi.fn()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
await userEvent.click(screen.getByLabelText("nav.openSettings"));
|
|
|
|
|
expect(onOpenSettings).toHaveBeenCalled();
|
|
|
|
|
expect(onOpenNavigationSettings).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("opens the navigation settings from the rail menu", async () => {
|
|
|
|
|
const onOpenSettings = vi.fn();
|
|
|
|
|
const onOpenNavigationSettings = vi.fn();
|
|
|
|
|
render(
|
|
|
|
|
<AppRail
|
|
|
|
|
railView="hosts"
|
|
|
|
|
sidebarOpen={false}
|
|
|
|
|
username="alice"
|
|
|
|
|
isAdmin
|
|
|
|
|
onRailClick={vi.fn()}
|
|
|
|
|
onOpenSettings={onOpenSettings}
|
|
|
|
|
onOpenNavigationSettings={onOpenNavigationSettings}
|
|
|
|
|
onLogout={vi.fn()}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
fireEvent.contextMenu(screen.getByTitle("nav.connections"));
|
|
|
|
|
await userEvent.click(screen.getByText("nav.openSettings"));
|
|
|
|
|
expect(onOpenNavigationSettings).toHaveBeenCalled();
|
|
|
|
|
expect(onOpenSettings).not.toHaveBeenCalled();
|
|
|
|
|
});
|
2026-10-01 16:20:16 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("RailBadge", () => {
|
|
|
|
|
it("shows nothing for zero", () => {
|
|
|
|
|
const { container } = render(<RailBadge useBadge={() => 0} />);
|
|
|
|
|
expect(container.textContent).toBe("");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("caps a large count", () => {
|
|
|
|
|
render(<RailBadge useBadge={() => 150} />);
|
|
|
|
|
expect(screen.getByText("99+")).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
});
|