test: cover services + bookmarks group components

This commit is contained in:
shamoon
2026-02-04 08:24:34 -08:00
parent f9a93d77a2
commit 0cb50794fa
10 changed files with 592 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
// @vitest-environment jsdom
import { render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
vi.mock("@headlessui/react", async () => {
const React = await import("react");
const { Fragment } = React;
function Transition({ as: As = Fragment, children }) {
if (As === Fragment) return <>{children}</>;
return <As>{children}</As>;
}
function Disclosure({ defaultOpen = true, children }) {
const content = typeof children === "function" ? children({ open: defaultOpen }) : children;
return <div>{content}</div>;
}
function DisclosureButton(props) {
return <button type="button" {...props} />;
}
const DisclosurePanel = React.forwardRef(function DisclosurePanel(props, ref) {
// HeadlessUI uses a boolean `static` prop; avoid forwarding it to the DOM.
const { static: isStatic, ...rest } = props; // eslint-disable-line no-unused-vars
return <div ref={ref} data-testid="disclosure-panel" {...rest} />;
});
Disclosure.Button = DisclosureButton;
Disclosure.Panel = DisclosurePanel;
return { Disclosure, Transition };
});
vi.mock("components/bookmarks/list", () => ({
default: function BookmarksListMock({ bookmarks }) {
return <div data-testid="bookmarks-list">count:{bookmarks?.length ?? 0}</div>;
},
}));
vi.mock("components/errorboundry", () => ({
default: function ErrorBoundaryMock({ children }) {
return <>{children}</>;
},
}));
vi.mock("components/resolvedicon", () => ({
default: function ResolvedIconMock() {
return <div data-testid="resolved-icon" />;
},
}));
import BookmarksGroup from "./group";
describe("components/bookmarks/group", () => {
it("renders the group header and list", () => {
render(
<BookmarksGroup
bookmarks={{ name: "Bookmarks", bookmarks: [{ name: "A" }] }}
layout={{ icon: "mdi:test" }}
disableCollapse={false}
groupsInitiallyCollapsed={false}
/>,
);
expect(screen.getByText("Bookmarks")).toBeInTheDocument();
expect(screen.getByTestId("resolved-icon")).toBeInTheDocument();
expect(screen.getByTestId("bookmarks-list")).toHaveTextContent("count:1");
});
it("sets the panel height to 0 when initially collapsed", async () => {
render(
<BookmarksGroup
bookmarks={{ name: "Bookmarks", bookmarks: [] }}
layout={{ initiallyCollapsed: true }}
groupsInitiallyCollapsed={false}
/>,
);
const panel = screen.getByTestId("disclosure-panel");
await waitFor(() => {
expect(panel.style.height).toBe("0px");
});
});
});

View File

@@ -0,0 +1,56 @@
// @vitest-environment jsdom
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
// Stub Menu/Transition to always render children (keeps tests deterministic).
vi.mock("@headlessui/react", async () => {
const React = await import("react");
const { Fragment } = React;
function Transition({ as: As = Fragment, children }) {
if (As === Fragment) return <>{children}</>;
return <As>{children}</As>;
}
function Menu({ as: As = "div", children, ...props }) {
const content = typeof children === "function" ? children({ open: true }) : children;
return <As {...props}>{content}</As>;
}
function MenuButton(props) {
return <button type="button" {...props} />;
}
function MenuItems(props) {
return <div {...props} />;
}
function MenuItem({ children }) {
return <>{children}</>;
}
Menu.Button = MenuButton;
Menu.Items = MenuItems;
Menu.Item = MenuItem;
return { Menu, Transition };
});
import Dropdown from "./dropdown";
describe("components/services/dropdown", () => {
it("renders the selected label and updates value when an option is clicked", () => {
const setValue = vi.fn();
const options = [
{ value: "a", label: "Alpha" },
{ value: "b", label: "Beta" },
];
render(<Dropdown options={options} value="a" setValue={setValue} />);
// "Alpha" appears both in the menu button and in the list of options.
expect(screen.getAllByRole("button", { name: "Alpha" })[0]).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Beta" }));
expect(setValue).toHaveBeenCalledWith("b");
});
});

View File

@@ -0,0 +1,89 @@
// @vitest-environment jsdom
import { render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
vi.mock("@headlessui/react", async () => {
const React = await import("react");
const { Fragment } = React;
function Transition({ as: As = Fragment, children }) {
if (As === Fragment) return <>{children}</>;
return <As>{children}</As>;
}
function Disclosure({ defaultOpen = true, children }) {
const content = typeof children === "function" ? children({ open: defaultOpen }) : children;
return <div>{content}</div>;
}
function DisclosureButton(props) {
return <button type="button" {...props} />;
}
const DisclosurePanel = React.forwardRef(function DisclosurePanel(props, ref) {
// HeadlessUI uses a boolean `static` prop; avoid forwarding it to the DOM.
const { static: isStatic, ...rest } = props; // eslint-disable-line no-unused-vars
return <div ref={ref} data-testid="disclosure-panel" {...rest} />;
});
Disclosure.Button = DisclosureButton;
Disclosure.Panel = DisclosurePanel;
return { Disclosure, Transition };
});
vi.mock("components/resolvedicon", () => ({
default: function ResolvedIconMock() {
return <div data-testid="resolved-icon" />;
},
}));
vi.mock("components/services/list", () => ({
default: function ServicesListMock({ groupName, services }) {
return (
<div data-testid="services-list-mock">
{groupName}:{services?.length ?? 0}
</div>
);
},
}));
import ServicesGroup from "./group";
describe("components/services/group", () => {
it("renders group and subgroup headers", () => {
render(
<ServicesGroup
group={{
name: "Main",
services: [{ name: "svc" }],
groups: [{ name: "Sub", services: [], groups: [] }],
}}
layout={{ icon: "mdi:test" }}
groupsInitiallyCollapsed={false}
/>,
);
expect(screen.getByText("Main")).toBeInTheDocument();
expect(screen.getByTestId("resolved-icon")).toBeInTheDocument();
const lists = screen.getAllByTestId("services-list-mock");
expect(lists[0]).toHaveTextContent("Main:1");
expect(screen.getByText("Sub")).toBeInTheDocument();
});
it("sets the panel height to 0 when initially collapsed", async () => {
render(
<ServicesGroup
group={{ name: "Main", services: [], groups: [] }}
layout={{ initiallyCollapsed: true }}
groupsInitiallyCollapsed={false}
/>,
);
const panel = screen.getAllByTestId("disclosure-panel")[0];
await waitFor(() => {
expect(panel.style.height).toBe("0px");
});
});
});

View File

@@ -0,0 +1,117 @@
// @vitest-environment jsdom
import { fireEvent, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
vi.mock("components/resolvedicon", () => ({
default: function ResolvedIconMock() {
return <div data-testid="resolved-icon" />;
},
}));
vi.mock("widgets/docker/component", () => ({
default: function DockerWidgetMock() {
return <div data-testid="docker-widget" />;
},
}));
vi.mock("widgets/kubernetes/component", () => ({
default: function KubernetesWidgetMock() {
return <div data-testid="kubernetes-widget" />;
},
}));
vi.mock("widgets/proxmoxvm/component", () => ({
default: function ProxmoxVMWidgetMock() {
return <div data-testid="proxmoxvm-widget" />;
},
}));
vi.mock("./ping", () => ({
default: function PingMock() {
return <div data-testid="ping" />;
},
}));
vi.mock("./site-monitor", () => ({
default: function SiteMonitorMock() {
return <div data-testid="site-monitor" />;
},
}));
vi.mock("./status", () => ({
default: function StatusMock() {
return <div data-testid="status" />;
},
}));
vi.mock("./kubernetes-status", () => ({
default: function KubernetesStatusMock() {
return <div data-testid="kubernetes-status" />;
},
}));
vi.mock("./proxmox-status", () => ({
default: function ProxmoxStatusMock() {
return <div data-testid="proxmox-status" />;
},
}));
vi.mock("./widget", () => ({
default: function ServiceWidgetMock({ widget }) {
return <div data-testid="service-widget">idx:{widget.index}</div>;
},
}));
import Item from "./item";
describe("components/services/item", () => {
it("renders the service title as a link when href is provided", () => {
renderWithProviders(
<Item
groupName="G"
useEqualHeights={false}
service={{
id: "svc1",
name: "My Service",
description: "Desc",
href: "https://example.com",
icon: "mdi:test",
widgets: [],
}}
/>,
{ settings: { target: "_self", showStats: false, statusStyle: "basic" } },
);
const links = screen.getAllByRole("link");
expect(links.some((l) => l.getAttribute("href") === "https://example.com")).toBe(true);
expect(screen.getByText("My Service")).toBeInTheDocument();
});
it("toggles container stats on click when stats are hidden by default", () => {
renderWithProviders(
<Item
groupName="G"
useEqualHeights={false}
service={{
id: "svc1",
name: "My Service",
description: "Desc",
href: "https://example.com",
container: "c",
server: "s",
ping: true,
siteMonitor: true,
widgets: [{ index: 1 }, { index: 2 }],
}}
/>,
{ settings: { showStats: false, statusStyle: "basic" } },
);
expect(screen.queryByTestId("docker-widget")).not.toBeInTheDocument();
expect(screen.getByTestId("ping")).toBeInTheDocument();
expect(screen.getByTestId("site-monitor")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "View container stats" }));
expect(screen.getByTestId("docker-widget")).toBeInTheDocument();
expect(screen.getAllByTestId("service-widget")).toHaveLength(2);
});
});

View File

@@ -0,0 +1,46 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() }));
vi.mock("swr", () => ({
default: useSWR,
}));
vi.mock("i18next", () => ({
t: (key) => key,
}));
import KubernetesStatus from "./kubernetes-status";
describe("components/services/kubernetes-status", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("includes podSelector in the request when provided", () => {
useSWR.mockReturnValue({ data: undefined, error: undefined });
render(<KubernetesStatus service={{ namespace: "ns", app: "app", podSelector: "x=y" }} />);
expect(useSWR).toHaveBeenCalledWith("/api/kubernetes/status/ns/app?podSelector=x=y");
});
it("renders the health/status label when running", () => {
useSWR.mockReturnValue({ data: { status: "running", health: "healthy" }, error: undefined });
render(<KubernetesStatus service={{ namespace: "ns", app: "app" }} />);
expect(screen.getByText("healthy")).toBeInTheDocument();
});
it("renders a dot when style is dot", () => {
useSWR.mockReturnValue({ data: { status: "running" }, error: undefined });
const { container } = render(<KubernetesStatus service={{ namespace: "ns", app: "app" }} style="dot" />);
expect(container.querySelector(".rounded-full")).toBeTruthy();
});
});

View File

@@ -0,0 +1,35 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
vi.mock("components/services/item", () => ({
default: function ServiceItemMock({ service, groupName, useEqualHeights }) {
return (
<li data-testid="service-item">
{groupName}:{service.name}:{String(useEqualHeights)}
</li>
);
},
}));
import List from "./list";
describe("components/services/list", () => {
it("renders items and passes the computed useEqualHeights value", () => {
render(
<List
groupName="G"
services={[{ name: "A" }, { name: "B" }]}
layout={{ useEqualHeights: true }}
useEqualHeights={false}
header
/>,
);
const items = screen.getAllByTestId("service-item");
expect(items).toHaveLength(2);
expect(items[0]).toHaveTextContent("G:A:true");
expect(items[1]).toHaveTextContent("G:B:true");
});
});

View File

@@ -0,0 +1,40 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() }));
vi.mock("swr", () => ({
default: useSWR,
}));
import Ping from "./ping";
describe("components/services/ping", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders the ping time when the host is alive", () => {
useSWR.mockReturnValue({ data: { alive: true, time: 123 }, error: undefined });
render(<Ping groupName="g" serviceName="s" />);
expect(useSWR).toHaveBeenCalledWith("/api/ping?groupName=g&serviceName=s", { refreshInterval: 30000 });
expect(screen.getByText("123")).toBeInTheDocument();
expect(screen.getByText("123").closest(".ping-status")).toHaveAttribute(
"title",
expect.stringContaining("ping.up"),
);
});
it("renders a dot when style is dot", () => {
useSWR.mockReturnValue({ data: { alive: true, time: 5 }, error: undefined });
const { container } = render(<Ping groupName="g" serviceName="s" style="dot" />);
expect(screen.queryByText("5")).not.toBeInTheDocument();
expect(container.querySelector(".rounded-full")).toBeTruthy();
});
});

View File

@@ -0,0 +1,36 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() }));
vi.mock("swr", () => ({
default: useSWR,
}));
import ProxmoxStatus from "./proxmox-status";
describe("components/services/proxmox-status", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("requests vm stats and renders running when status is running", () => {
useSWR.mockReturnValue({ data: { status: "running" }, error: undefined });
render(<ProxmoxStatus service={{ proxmoxNode: "n1", proxmoxVMID: "100" }} />);
expect(useSWR).toHaveBeenCalledWith("/api/proxmox/stats/n1/100?type=qemu");
expect(screen.getByText("docker.running")).toBeInTheDocument();
});
it("renders paused for paused vms", () => {
useSWR.mockReturnValue({ data: { status: "paused" }, error: undefined });
render(<ProxmoxStatus service={{ proxmoxNode: "n1", proxmoxVMID: "100", proxmoxType: "lxc" }} />);
expect(useSWR).toHaveBeenCalledWith("/api/proxmox/stats/n1/100?type=lxc");
expect(screen.getByText("paused")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,43 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() }));
vi.mock("swr", () => ({
default: useSWR,
}));
import SiteMonitor from "./site-monitor";
describe("components/services/site-monitor", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders response time when status is up", () => {
useSWR.mockReturnValue({ data: { status: 200, latency: 10 }, error: undefined });
render(<SiteMonitor groupName="g" serviceName="s" />);
expect(useSWR).toHaveBeenCalledWith("/api/siteMonitor?groupName=g&serviceName=s", { refreshInterval: 30000 });
expect(screen.getByText("10")).toBeInTheDocument();
});
it("renders down label for failing status in basic style", () => {
useSWR.mockReturnValue({ data: { status: 500, latency: 0 }, error: undefined });
render(<SiteMonitor groupName="g" serviceName="s" style="basic" />);
expect(screen.getByText("siteMonitor.down")).toBeInTheDocument();
});
it("renders an error label when SWR returns error", () => {
useSWR.mockReturnValue({ data: undefined, error: new Error("boom") });
render(<SiteMonitor groupName="g" serviceName="s" />);
expect(screen.getByText("siteMonitor.error")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,44 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() }));
vi.mock("swr", () => ({
default: useSWR,
}));
import Status from "./status";
describe("components/services/status", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("requests docker status and renders unknown by default", () => {
useSWR.mockReturnValue({ data: undefined, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(useSWR).toHaveBeenCalledWith("/api/docker/status/c/s");
expect(screen.getByText("docker.unknown")).toBeInTheDocument();
});
it("renders starting health when container is running and starting", () => {
useSWR.mockReturnValue({ data: { status: "running", health: "starting" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.starting")).toBeInTheDocument();
});
it("renders a dot when style is dot", () => {
useSWR.mockReturnValue({ data: { status: "running" }, error: undefined });
const { container } = render(<Status service={{ container: "c", server: "s" }} style="dot" />);
expect(screen.queryByText("docker.running")).not.toBeInTheDocument();
expect(container.querySelector(".rounded-full")).toBeTruthy();
});
});