From 0cb50794fa2657da5ae37a23e44294c73f065f86 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Wed, 4 Feb 2026 08:24:34 -0800
Subject: [PATCH] test: cover services + bookmarks group components
---
src/components/bookmarks/group.test.jsx | 86 +++++++++++++
src/components/services/dropdown.test.jsx | 56 +++++++++
src/components/services/group.test.jsx | 89 +++++++++++++
src/components/services/item.test.jsx | 117 ++++++++++++++++++
.../services/kubernetes-status.test.jsx | 46 +++++++
src/components/services/list.test.jsx | 35 ++++++
src/components/services/ping.test.jsx | 40 ++++++
.../services/proxmox-status.test.jsx | 36 ++++++
src/components/services/site-monitor.test.jsx | 43 +++++++
src/components/services/status.test.jsx | 44 +++++++
10 files changed, 592 insertions(+)
create mode 100644 src/components/bookmarks/group.test.jsx
create mode 100644 src/components/services/dropdown.test.jsx
create mode 100644 src/components/services/group.test.jsx
create mode 100644 src/components/services/item.test.jsx
create mode 100644 src/components/services/kubernetes-status.test.jsx
create mode 100644 src/components/services/list.test.jsx
create mode 100644 src/components/services/ping.test.jsx
create mode 100644 src/components/services/proxmox-status.test.jsx
create mode 100644 src/components/services/site-monitor.test.jsx
create mode 100644 src/components/services/status.test.jsx
diff --git a/src/components/bookmarks/group.test.jsx b/src/components/bookmarks/group.test.jsx
new file mode 100644
index 000000000..6548a5127
--- /dev/null
+++ b/src/components/bookmarks/group.test.jsx
@@ -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 {children};
+ }
+
+ function Disclosure({ defaultOpen = true, children }) {
+ const content = typeof children === "function" ? children({ open: defaultOpen }) : children;
+ return
{content}
;
+ }
+
+ function DisclosureButton(props) {
+ return ;
+ }
+
+ 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 ;
+ });
+
+ Disclosure.Button = DisclosureButton;
+ Disclosure.Panel = DisclosurePanel;
+
+ return { Disclosure, Transition };
+});
+
+vi.mock("components/bookmarks/list", () => ({
+ default: function BookmarksListMock({ bookmarks }) {
+ return count:{bookmarks?.length ?? 0}
;
+ },
+}));
+
+vi.mock("components/errorboundry", () => ({
+ default: function ErrorBoundaryMock({ children }) {
+ return <>{children}>;
+ },
+}));
+
+vi.mock("components/resolvedicon", () => ({
+ default: function ResolvedIconMock() {
+ return ;
+ },
+}));
+
+import BookmarksGroup from "./group";
+
+describe("components/bookmarks/group", () => {
+ it("renders the group header and list", () => {
+ render(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ const panel = screen.getByTestId("disclosure-panel");
+ await waitFor(() => {
+ expect(panel.style.height).toBe("0px");
+ });
+ });
+});
diff --git a/src/components/services/dropdown.test.jsx b/src/components/services/dropdown.test.jsx
new file mode 100644
index 000000000..dfd0e3a38
--- /dev/null
+++ b/src/components/services/dropdown.test.jsx
@@ -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 {children};
+ }
+
+ function Menu({ as: As = "div", children, ...props }) {
+ const content = typeof children === "function" ? children({ open: true }) : children;
+ return {content};
+ }
+
+ function MenuButton(props) {
+ return ;
+ }
+ function MenuItems(props) {
+ return ;
+ }
+ 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();
+
+ // "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");
+ });
+});
diff --git a/src/components/services/group.test.jsx b/src/components/services/group.test.jsx
new file mode 100644
index 000000000..1c4a51f04
--- /dev/null
+++ b/src/components/services/group.test.jsx
@@ -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 {children};
+ }
+
+ function Disclosure({ defaultOpen = true, children }) {
+ const content = typeof children === "function" ? children({ open: defaultOpen }) : children;
+ return {content}
;
+ }
+
+ function DisclosureButton(props) {
+ return ;
+ }
+
+ 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 ;
+ });
+
+ Disclosure.Button = DisclosureButton;
+ Disclosure.Panel = DisclosurePanel;
+
+ return { Disclosure, Transition };
+});
+
+vi.mock("components/resolvedicon", () => ({
+ default: function ResolvedIconMock() {
+ return ;
+ },
+}));
+
+vi.mock("components/services/list", () => ({
+ default: function ServicesListMock({ groupName, services }) {
+ return (
+
+ {groupName}:{services?.length ?? 0}
+
+ );
+ },
+}));
+
+import ServicesGroup from "./group";
+
+describe("components/services/group", () => {
+ it("renders group and subgroup headers", () => {
+ render(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ const panel = screen.getAllByTestId("disclosure-panel")[0];
+ await waitFor(() => {
+ expect(panel.style.height).toBe("0px");
+ });
+ });
+});
diff --git a/src/components/services/item.test.jsx b/src/components/services/item.test.jsx
new file mode 100644
index 000000000..7689381ef
--- /dev/null
+++ b/src/components/services/item.test.jsx
@@ -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 ;
+ },
+}));
+
+vi.mock("widgets/docker/component", () => ({
+ default: function DockerWidgetMock() {
+ return ;
+ },
+}));
+
+vi.mock("widgets/kubernetes/component", () => ({
+ default: function KubernetesWidgetMock() {
+ return ;
+ },
+}));
+
+vi.mock("widgets/proxmoxvm/component", () => ({
+ default: function ProxmoxVMWidgetMock() {
+ return ;
+ },
+}));
+
+vi.mock("./ping", () => ({
+ default: function PingMock() {
+ return ;
+ },
+}));
+vi.mock("./site-monitor", () => ({
+ default: function SiteMonitorMock() {
+ return ;
+ },
+}));
+vi.mock("./status", () => ({
+ default: function StatusMock() {
+ return ;
+ },
+}));
+vi.mock("./kubernetes-status", () => ({
+ default: function KubernetesStatusMock() {
+ return ;
+ },
+}));
+vi.mock("./proxmox-status", () => ({
+ default: function ProxmoxStatusMock() {
+ return ;
+ },
+}));
+vi.mock("./widget", () => ({
+ default: function ServiceWidgetMock({ widget }) {
+ return idx:{widget.index}
;
+ },
+}));
+
+import Item from "./item";
+
+describe("components/services/item", () => {
+ it("renders the service title as a link when href is provided", () => {
+ renderWithProviders(
+ ,
+ { 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(
+ ,
+ { 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);
+ });
+});
diff --git a/src/components/services/kubernetes-status.test.jsx b/src/components/services/kubernetes-status.test.jsx
new file mode 100644
index 000000000..41640f1e5
--- /dev/null
+++ b/src/components/services/kubernetes-status.test.jsx
@@ -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();
+
+ 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();
+
+ expect(screen.getByText("healthy")).toBeInTheDocument();
+ });
+
+ it("renders a dot when style is dot", () => {
+ useSWR.mockReturnValue({ data: { status: "running" }, error: undefined });
+
+ const { container } = render();
+
+ expect(container.querySelector(".rounded-full")).toBeTruthy();
+ });
+});
diff --git a/src/components/services/list.test.jsx b/src/components/services/list.test.jsx
new file mode 100644
index 000000000..803e821ea
--- /dev/null
+++ b/src/components/services/list.test.jsx
@@ -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 (
+
+ {groupName}:{service.name}:{String(useEqualHeights)}
+
+ );
+ },
+}));
+
+import List from "./list";
+
+describe("components/services/list", () => {
+ it("renders items and passes the computed useEqualHeights value", () => {
+ render(
+
,
+ );
+
+ const items = screen.getAllByTestId("service-item");
+ expect(items).toHaveLength(2);
+ expect(items[0]).toHaveTextContent("G:A:true");
+ expect(items[1]).toHaveTextContent("G:B:true");
+ });
+});
diff --git a/src/components/services/ping.test.jsx b/src/components/services/ping.test.jsx
new file mode 100644
index 000000000..a45324a9f
--- /dev/null
+++ b/src/components/services/ping.test.jsx
@@ -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();
+
+ 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();
+
+ expect(screen.queryByText("5")).not.toBeInTheDocument();
+ expect(container.querySelector(".rounded-full")).toBeTruthy();
+ });
+});
diff --git a/src/components/services/proxmox-status.test.jsx b/src/components/services/proxmox-status.test.jsx
new file mode 100644
index 000000000..2573f6fd6
--- /dev/null
+++ b/src/components/services/proxmox-status.test.jsx
@@ -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();
+
+ 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();
+
+ expect(useSWR).toHaveBeenCalledWith("/api/proxmox/stats/n1/100?type=lxc");
+ expect(screen.getByText("paused")).toBeInTheDocument();
+ });
+});
diff --git a/src/components/services/site-monitor.test.jsx b/src/components/services/site-monitor.test.jsx
new file mode 100644
index 000000000..7949f4c48
--- /dev/null
+++ b/src/components/services/site-monitor.test.jsx
@@ -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();
+
+ 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();
+
+ expect(screen.getByText("siteMonitor.down")).toBeInTheDocument();
+ });
+
+ it("renders an error label when SWR returns error", () => {
+ useSWR.mockReturnValue({ data: undefined, error: new Error("boom") });
+
+ render();
+
+ expect(screen.getByText("siteMonitor.error")).toBeInTheDocument();
+ });
+});
diff --git a/src/components/services/status.test.jsx b/src/components/services/status.test.jsx
new file mode 100644
index 000000000..e16cbbfc0
--- /dev/null
+++ b/src/components/services/status.test.jsx
@@ -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();
+
+ 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();
+
+ expect(screen.getByText("docker.starting")).toBeInTheDocument();
+ });
+
+ it("renders a dot when style is dot", () => {
+ useSWR.mockReturnValue({ data: { status: "running" }, error: undefined });
+
+ const { container } = render();
+
+ expect(screen.queryByText("docker.running")).not.toBeInTheDocument();
+ expect(container.querySelector(".rounded-full")).toBeTruthy();
+ });
+});