Chore: homepage tests (#6278)

This commit is contained in:
shamoon
2026-02-04 19:58:39 -08:00
committed by GitHub
parent 7d019185a3
commit 872a3600aa
558 changed files with 32606 additions and 84 deletions
+56
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");
});
});
+1 -1
View File
@@ -36,7 +36,7 @@ export default function ServicesGroup({
isSubgroup ? "subgroup" : "",
)}
>
<Disclosure defaultOpen={!(layout?.initiallyCollapsed ?? groupsInitiallyCollapsed) ?? true}>
<Disclosure defaultOpen={!(layout?.initiallyCollapsed ?? groupsInitiallyCollapsed)}>
{({ open }) => (
<>
{layout?.header !== false && (
+87
View File
@@ -0,0 +1,87 @@
// @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) {
return <div ref={ref} data-testid="disclosure-panel" {...props} static="true" />;
});
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");
});
});
});
@@ -0,0 +1,92 @@
// @vitest-environment jsdom
import { act, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
vi.mock("@headlessui/react", async () => {
const React = await import("react");
const { Fragment, useEffect } = React;
function Transition({ as: As = Fragment, beforeEnter, beforeLeave, children }) {
useEffect(() => {
// Simulate a mount -> enter animation, then a leave animation shortly after.
beforeEnter?.();
setTimeout(() => beforeLeave?.(), 200);
}, [beforeEnter, beforeLeave]);
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) {
const { static: _static, ...rest } = props;
return (
<div
ref={(node) => {
if (node) {
// JSDOM doesn't calculate layout; give the panel a deterministic height.
Object.defineProperty(node, "scrollHeight", { value: 123, configurable: true });
}
if (typeof ref === "function") ref(node);
else if (ref) ref.current = node;
}}
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() {
return <div data-testid="services-list" />;
},
}));
import ServicesGroup from "./group";
describe("components/services/group transition hooks", () => {
it("runs the Transition beforeEnter/beforeLeave height calculations", async () => {
vi.useFakeTimers();
render(
<ServicesGroup
group={{ name: "Main", services: [], groups: [] }}
layout={{ initiallyCollapsed: false }}
groupsInitiallyCollapsed={false}
/>,
);
const panel = screen.getByTestId("disclosure-panel");
expect(panel).toBeTruthy();
await act(async () => {
vi.runAllTimers();
});
// The leave animation sets height back to 0.
expect(panel.style.height).toBe("0px");
vi.useRealTimers();
});
});
+247
View File
@@ -0,0 +1,247 @@
// @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("renders the icon without a link when href is missing or '#'", () => {
renderWithProviders(
<Item
groupName="G"
useEqualHeights={false}
service={{
id: "svc1",
name: "My Service",
description: "Desc",
href: "#",
icon: "mdi:test",
widgets: [],
}}
/>,
{ settings: { target: "_self", showStats: false, statusStyle: "basic" } },
);
// The title area should not create a clickable href="#" link.
expect(screen.queryByRole("link")).not.toBeInTheDocument();
expect(screen.getByTestId("resolved-icon")).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);
});
it("shows stats by default when settings.showStats is enabled, unless overridden by the service", () => {
const baseService = {
id: "svc1",
name: "My Service",
description: "Desc",
container: "c",
server: "s",
widgets: [],
};
renderWithProviders(<Item groupName="G" useEqualHeights={false} service={baseService} />, {
settings: { showStats: true, statusStyle: "basic" },
});
expect(screen.getByTestId("docker-widget")).toBeInTheDocument();
renderWithProviders(
<Item groupName="G" useEqualHeights={false} service={{ ...baseService, id: "svc2", showStats: false }} />,
{
settings: { showStats: true, statusStyle: "basic" },
},
);
expect(screen.getAllByTestId("docker-widget")).toHaveLength(1);
});
it("closes stats after a short delay when toggled closed", async () => {
vi.useFakeTimers();
renderWithProviders(
<Item
groupName="G"
useEqualHeights={false}
service={{
id: "svc1",
name: "My Service",
description: "Desc",
container: "c",
server: "s",
widgets: [],
}}
/>,
{ settings: { showStats: false, statusStyle: "basic" } },
);
const btn = screen.getByRole("button", { name: "View container stats" });
fireEvent.click(btn);
expect(screen.getByTestId("docker-widget")).toBeInTheDocument();
fireEvent.click(btn);
// Still rendered while the close animation runs.
expect(screen.getByTestId("docker-widget")).toBeInTheDocument();
await vi.advanceTimersByTimeAsync(300);
expect(screen.queryByTestId("docker-widget")).not.toBeInTheDocument();
vi.useRealTimers();
});
it("toggles app and proxmox stats using their respective status tags", () => {
renderWithProviders(
<Item
groupName="G"
useEqualHeights={false}
service={{
id: "svc1",
name: "My Service",
description: "Desc",
app: "app",
namespace: "default",
proxmoxNode: "pve",
proxmoxVMID: "100",
proxmoxType: "qemu",
widgets: [],
}}
/>,
{ settings: { showStats: false, statusStyle: "basic" } },
);
const appBtn = screen.getByTestId("kubernetes-status").closest("button");
expect(appBtn).toBeTruthy();
fireEvent.click(appBtn);
expect(screen.getByTestId("kubernetes-widget")).toBeInTheDocument();
const proxmoxBtn = screen.getByTestId("proxmox-status").closest("button");
expect(proxmoxBtn).toBeTruthy();
fireEvent.click(proxmoxBtn);
expect(screen.getByTestId("proxmoxvm-widget")).toBeInTheDocument();
});
it("does not render the app status tag when the service is marked external", () => {
renderWithProviders(
<Item
groupName="G"
useEqualHeights={false}
service={{
id: "svc1",
name: "My Service",
description: "Desc",
app: "app",
external: true,
widgets: [],
}}
/>,
{ settings: { showStats: false, statusStyle: "basic" } },
);
expect(screen.queryByTestId("kubernetes-status")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,65 @@
// @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();
});
it("renders an error label when SWR returns an error", () => {
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
const { container } = render(<KubernetesStatus service={{ namespace: "ns", app: "app" }} />);
expect(screen.getByText("docker.error")).toBeInTheDocument();
expect(container.querySelector(".k8s-status")?.getAttribute("title")).toBe("docker.error");
});
it("renders orange status labels when the workload is down/partial/not found", () => {
useSWR.mockReturnValue({ data: { status: "down" }, error: undefined });
const { container } = render(<KubernetesStatus service={{ namespace: "ns", app: "app" }} />);
expect(screen.getByText("down")).toBeInTheDocument();
// Ensure the status is used as a tooltip/title too.
expect(container.querySelector(".k8s-status")?.getAttribute("title")).toBe("down");
});
});
+35
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");
});
});
+76
View File
@@ -0,0 +1,76 @@
// @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 a loading state when data is not available yet", () => {
useSWR.mockReturnValue({ data: undefined, error: undefined });
render(<Ping groupName="g" serviceName="s" />);
expect(screen.getByText("ping.ping")).toBeInTheDocument();
expect(screen.getByText("ping.ping").closest(".ping-status")).toHaveAttribute(
"title",
expect.stringContaining("ping.not_available"),
);
});
it("renders an error label when SWR returns error", () => {
useSWR.mockReturnValue({ data: undefined, error: new Error("boom") });
render(<Ping groupName="g" serviceName="s" />);
expect(screen.getByText("ping.error")).toBeInTheDocument();
});
it("renders down when the host is not alive", () => {
useSWR.mockReturnValue({ data: { alive: false, time: 0 }, error: undefined });
render(<Ping groupName="g" serviceName="s" />);
expect(screen.getByText("ping.down")).toBeInTheDocument();
});
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 an up label for basic style", () => {
useSWR.mockReturnValue({ data: { alive: true, time: 1 }, error: undefined });
render(<Ping groupName="g" serviceName="s" style="basic" />);
expect(screen.getByText("ping.up")).toBeInTheDocument();
});
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();
});
});
@@ -0,0 +1,75 @@
// @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("renders unknown when data is not available yet", () => {
useSWR.mockReturnValue({ data: undefined, error: undefined });
render(<ProxmoxStatus service={{ proxmoxNode: "n1", proxmoxVMID: "100" }} />);
expect(screen.getByText("docker.unknown")).toBeInTheDocument();
});
it("renders error when SWR returns an error", () => {
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
render(<ProxmoxStatus service={{ proxmoxNode: "n1", proxmoxVMID: "100" }} />);
expect(screen.getByText("docker.error")).toBeInTheDocument();
});
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();
});
it("renders other terminal statuses (stopped/offline/not found)", () => {
useSWR.mockReturnValue({ data: { status: "stopped" }, error: undefined });
render(<ProxmoxStatus service={{ proxmoxNode: "n1", proxmoxVMID: "100" }} />);
expect(screen.getByText("docker.exited")).toBeInTheDocument();
useSWR.mockReturnValue({ data: { status: "offline" }, error: undefined });
render(<ProxmoxStatus service={{ proxmoxNode: "n1", proxmoxVMID: "100" }} />);
expect(screen.getByText("offline")).toBeInTheDocument();
useSWR.mockReturnValue({ data: { status: "not found" }, error: undefined });
render(<ProxmoxStatus service={{ proxmoxNode: "n1", proxmoxVMID: "100" }} />);
expect(screen.getByText("docker.not_found")).toBeInTheDocument();
});
it("renders a dot status when style=dot", () => {
useSWR.mockReturnValue({ data: { status: "running" }, error: undefined });
const { container } = render(<ProxmoxStatus service={{ proxmoxNode: "n1", proxmoxVMID: "100" }} style="dot" />);
expect(container.querySelector(".rounded-full")).toBeTruthy();
expect(screen.queryByText("docker.running")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,88 @@
// @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 a loading state when data is not available yet", () => {
useSWR.mockReturnValue({ data: undefined, error: undefined });
render(<SiteMonitor groupName="g" serviceName="s" />);
expect(screen.getByText("siteMonitor.response")).toBeInTheDocument();
expect(screen.getByText("siteMonitor.response").closest(".site-monitor-status")).toHaveAttribute(
"title",
expect.stringContaining("siteMonitor.not_available"),
);
});
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 up label for basic style when status is ok", () => {
useSWR.mockReturnValue({ data: { status: 200, latency: 1 }, error: undefined });
render(<SiteMonitor groupName="g" serviceName="s" style="basic" />);
expect(screen.getByText("siteMonitor.up")).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 the http status code for failing status in non-basic style", () => {
useSWR.mockReturnValue({ data: { status: 500, latency: 0 }, error: undefined });
render(<SiteMonitor groupName="g" serviceName="s" />);
expect(screen.getByText("500")).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();
});
it("treats an embedded data.error as an error state", () => {
useSWR.mockReturnValue({ data: { error: "bad" }, error: undefined });
render(<SiteMonitor groupName="g" serviceName="s" />);
expect(screen.getByText("siteMonitor.error")).toBeInTheDocument();
});
it("renders a dot when style is dot", () => {
useSWR.mockReturnValue({ data: { status: 500, latency: 0 }, error: undefined });
const { container } = render(<SiteMonitor groupName="g" serviceName="s" style="dot" />);
expect(container.querySelector(".rounded-full")).toBeTruthy();
expect(screen.queryByText("500")).toBeNull();
});
});
+74
View File
@@ -0,0 +1,74 @@
// @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 error when SWR fails", () => {
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.error")).toBeInTheDocument();
});
it("renders healthy/unhealthy and partial/exited/not found statuses", () => {
useSWR.mockReturnValue({ data: { status: "running", health: "healthy" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.healthy")).toBeInTheDocument();
useSWR.mockReturnValue({ data: { status: "running", health: "unhealthy" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.unhealthy")).toBeInTheDocument();
useSWR.mockReturnValue({ data: { status: "partial 1/2" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.partial 1/2")).toBeInTheDocument();
useSWR.mockReturnValue({ data: { status: "exited" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.exited")).toBeInTheDocument();
useSWR.mockReturnValue({ data: { status: "not found" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.not_found")).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();
});
});
+1 -2
View File
@@ -8,8 +8,7 @@ export default function Widget({ widget, service }) {
const ServiceWidget = components[widget.type];
const fullService = Object.apply({}, service);
fullService.widget = widget;
const fullService = { ...service, widget };
if (ServiceWidget) {
return (
<ErrorBoundary>
+38
View File
@@ -0,0 +1,38 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
vi.mock("components/errorboundry", () => ({
default: function ErrorBoundaryMock({ children }) {
return <>{children}</>;
},
}));
vi.mock("widgets/components", () => ({
default: {
mock: function MockWidget({ service }) {
return (
<div data-testid="mock-service-widget">
{service.name}:{service.widget?.type}
</div>
);
},
},
}));
import Widget from "./widget";
describe("components/services/widget", () => {
it("renders the mapped widget component and passes merged service.widget", () => {
render(<Widget widget={{ type: "mock" }} service={{ name: "Svc" }} />);
expect(screen.getByTestId("mock-service-widget")).toHaveTextContent("Svc:mock");
});
it("renders a missing widget message when the type is unknown", () => {
render(<Widget widget={{ type: "nope" }} service={{ name: "Svc" }} />);
expect(screen.getByText("widget.missing_type")).toBeInTheDocument();
});
});
@@ -0,0 +1,41 @@
// @vitest-environment jsdom
import { describe, expect, it } from "vitest";
import Block from "./block";
import { BlockHighlightContext } from "./highlight-context";
import { renderWithProviders } from "test-utils/render-with-providers";
describe("components/services/widget/block", () => {
it("renders a placeholder when value is undefined", () => {
const { container } = renderWithProviders(<Block label="some.label" />, { settings: {} });
// Value slot is rendered as "-" while loading.
expect(container.textContent).toContain("-");
expect(container.textContent).toContain("some.label");
});
it("sets highlight metadata when a rule matches", () => {
const highlightConfig = {
levels: { danger: "danger-class" },
fields: {
foo: {
numeric: { when: "gt", value: 10, level: "danger" },
},
},
};
const { container } = renderWithProviders(
<BlockHighlightContext.Provider value={highlightConfig}>
<Block label="foo.label" field="foo" value="11" />
</BlockHighlightContext.Provider>,
{ settings: {} },
);
const el = container.querySelector(".service-block");
expect(el).not.toBeNull();
expect(el.getAttribute("data-highlight-level")).toBe("danger");
expect(el.className).toContain("danger-class");
});
});
@@ -0,0 +1,86 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { useContext } from "react";
import { describe, expect, it } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
import Container from "./container";
import { BlockHighlightContext } from "./highlight-context";
function Dummy({ label }) {
return <div data-testid={label} />;
}
function HighlightProbe() {
const value = useContext(BlockHighlightContext);
return <div data-testid="highlight-probe" data-highlight={value ? "yes" : "no"} />;
}
describe("components/services/widget/container", () => {
it("filters children based on widget.fields (auto-namespaced by widget type)", () => {
renderWithProviders(
<Container service={{ widget: { type: "omada", fields: ["connectedAp", "alerts"] } }}>
<Dummy label="omada.connectedAp" />
<Dummy label="omada.alerts" />
<Dummy label="omada.activeUser" />
</Container>,
{ settings: {} },
);
expect(screen.getByTestId("omada.connectedAp")).toBeInTheDocument();
expect(screen.getByTestId("omada.alerts")).toBeInTheDocument();
expect(screen.queryByTestId("omada.activeUser")).toBeNull();
});
it("accepts widget.fields as a JSON string", () => {
renderWithProviders(
<Container service={{ widget: { type: "omada", fields: JSON.stringify(["alerts"]) } }}>
<Dummy label="omada.connectedAp" />
<Dummy label="omada.alerts" />
</Container>,
{ settings: {} },
);
expect(screen.getByTestId("omada.alerts")).toBeInTheDocument();
expect(screen.queryByTestId("omada.connectedAp")).toBeNull();
});
it("supports aliased widget types when filtering (hoarder -> karakeep)", () => {
renderWithProviders(
<Container service={{ widget: { type: "hoarder", fields: ["hoarder.count"] } }}>
<Dummy label="karakeep.count" />
</Container>,
{ settings: {} },
);
expect(screen.getByTestId("karakeep.count")).toBeInTheDocument();
});
it("returns null when errors are hidden via settings.hideErrors", () => {
const { container } = renderWithProviders(
<Container error="nope" service={{ widget: { type: "omada", hide_errors: false } }}>
<Dummy label="omada.alerts" />
</Container>,
{ settings: { hideErrors: true } },
);
expect(container).toBeEmptyDOMElement();
});
it("skips the highlight provider when highlight levels are fully disabled", () => {
renderWithProviders(
<Container service={{ widget: { type: "omada" } }}>
<HighlightProbe />
</Container>,
{
settings: {
blockHighlights: { levels: { good: null, warn: null, danger: null } },
},
},
);
expect(screen.getByTestId("highlight-probe").getAttribute("data-highlight")).toBe("no");
});
});
@@ -0,0 +1,45 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import Error from "./error";
describe("components/services/widget/error", () => {
it("normalizes string errors to an object with a message", () => {
render(<Error error="boom" />);
expect(screen.getByText((_, el) => el?.textContent === "widget.api_error:")).toBeInTheDocument();
expect(screen.getByText(/boom/)).toBeInTheDocument();
});
it("normalizes numeric errors to an object with a message", () => {
render(<Error error={500} />);
expect(screen.getByText(/Error 500/)).toBeInTheDocument();
});
it("unwraps nested response errors and renders raw/data sections", () => {
render(
<Error
error={{
message: "outer",
data: {
error: {
message: "inner",
url: "https://example.com",
rawError: ["oops", { code: 1 }],
data: { type: "Buffer", data: [97, 98] },
},
},
}}
/>,
);
expect(screen.getByText(/inner/)).toBeInTheDocument();
expect(screen.getByText("https://example.com")).toBeInTheDocument();
expect(screen.getByText(/\"code\": 1/)).toBeInTheDocument();
// Buffer.from({type:"Buffer",data:[97,98]}).toString() === "ab"
expect(screen.getByText(/ab/)).toBeInTheDocument();
});
});
@@ -0,0 +1,29 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { useContext } from "react";
import { BlockHighlightContext } from "./highlight-context";
function Reader() {
const value = useContext(BlockHighlightContext);
return <div data-testid="value">{value === null ? "null" : value}</div>;
}
describe("components/services/widget/highlight-context", () => {
it("defaults to null", () => {
render(<Reader />);
expect(screen.getByTestId("value")).toHaveTextContent("null");
});
it("provides a value to consumers", () => {
render(
<BlockHighlightContext.Provider value="on">
<Reader />
</BlockHighlightContext.Provider>,
);
expect(screen.getByTestId("value")).toHaveTextContent("on");
});
});