test: add info widget component tests

This commit is contained in:
shamoon
2026-02-03 15:29:04 -08:00
parent 7df5aa9017
commit 3a6faa3f41
12 changed files with 508 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
import DateTime from "./datetime";
describe("components/widgets/datetime", () => {
it("renders formatted date/time and updates on an interval", async () => {
vi.useFakeTimers();
try {
vi.setSystemTime(new Date("2020-01-01T00:00:00.000Z"));
const format = { timeZone: "UTC", hour: "2-digit", minute: "2-digit", second: "2-digit" };
const expected0 = new Intl.DateTimeFormat("en-US", format).format(new Date());
renderWithProviders(<DateTime options={{ locale: "en-US", format }} />, { settings: { target: "_self" } });
// `render` wraps in `act`, so effects should flush synchronously.
expect(screen.getByText(expected0)).toBeInTheDocument();
await vi.advanceTimersByTimeAsync(1000);
const expected1 = new Intl.DateTimeFormat("en-US", format).format(new Date());
expect(screen.getByText(expected1)).toBeInTheDocument();
} finally {
vi.useRealTimers();
}
});
});

View File

@@ -0,0 +1,51 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() }));
vi.mock("swr", () => ({ default: useSWR }));
import Glances from "./glances";
describe("components/widgets/glances", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholder resources while loading", () => {
useSWR.mockReturnValue({ data: undefined, error: undefined });
renderWithProviders(<Glances options={{ cpu: true, mem: true, cputemp: true, disk: "/", uptime: true }} />, {
settings: { target: "_self" },
});
// All placeholders use glances.wait.
expect(screen.getAllByText("glances.wait").length).toBeGreaterThan(0);
});
it("renders cpu percent and memory available when data is present", () => {
useSWR.mockReturnValue({
data: {
cpu: { total: 12.34 },
load: { min15: 5 },
mem: { available: 1024, total: 2048, percent: 50 },
fs: [{ mnt_point: "/", free: 100, size: 200, percent: 50 }],
sensors: [],
uptime: "1 days, 00:00:00",
},
error: undefined,
});
renderWithProviders(<Glances options={{ cpu: true, mem: true, disk: "/", uptime: true }} />, {
settings: { target: "_self" },
});
// common.number is mocked to return the numeric value as a string.
expect(screen.getByText("12.34")).toBeInTheDocument();
// common.bytes is mocked similarly; we just assert the numeric value is present.
expect(screen.getByText("1024")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,20 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
import Greeting from "./greeting";
describe("components/widgets/greeting", () => {
it("renders nothing when text is not configured", () => {
const { container } = renderWithProviders(<Greeting options={{}} />, { settings: { target: "_self" } });
expect(container).toBeEmptyDOMElement();
});
it("renders configured greeting text", () => {
renderWithProviders(<Greeting options={{ text: "Hello there" }} />, { settings: { target: "_self" } });
expect(screen.getByText("Hello there")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,45 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() }));
vi.mock("swr", () => ({ default: useSWR }));
vi.mock("./node", () => ({
default: ({ type }) => <div data-testid="kube-node" data-type={type} />,
}));
import Kubernetes from "./kubernetes";
describe("components/widgets/kubernetes", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholder nodes while loading", () => {
useSWR.mockReturnValue({ data: undefined, error: undefined });
renderWithProviders(<Kubernetes options={{ cluster: { show: true }, nodes: { show: true } }} />, {
settings: { target: "_self" },
});
expect(screen.getAllByTestId("kube-node").map((n) => n.getAttribute("data-type"))).toEqual(["cluster", "node"]);
});
it("renders a node per returned entry when data is available", () => {
useSWR.mockReturnValue({
data: { cluster: {}, nodes: [{ name: "n1" }, { name: "n2" }] },
error: undefined,
});
renderWithProviders(<Kubernetes options={{ cluster: { show: true }, nodes: { show: true } }} />, {
settings: { target: "_self" },
});
// cluster + 2 nodes
expect(screen.getAllByTestId("kube-node")).toHaveLength(3);
});
});

View File

@@ -0,0 +1,26 @@
// @vitest-environment jsdom
import { 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: ({ icon }) => <div data-testid="resolved-icon" data-icon={icon} />,
}));
import Logo from "./logo";
describe("components/widgets/logo", () => {
it("renders a fallback SVG when no icon is configured", () => {
const { container } = renderWithProviders(<Logo options={{}} />, { settings: { target: "_self" } });
expect(screen.queryByTestId("resolved-icon")).toBeNull();
expect(container.querySelector("svg")).not.toBeNull();
});
it("renders the configured icon via ResolvedIcon", () => {
renderWithProviders(<Logo options={{ icon: "mdi:home" }} />, { settings: { target: "_self" } });
const icon = screen.getByTestId("resolved-icon");
expect(icon.getAttribute("data-icon")).toBe("mdi:home");
});
});

View File

@@ -0,0 +1,50 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() }));
vi.mock("swr", () => ({ default: useSWR }));
vi.mock("./node", () => ({
default: ({ data }) => <div data-testid="longhorn-node" data-id={data.node.id} />,
}));
import Longhorn from "./longhorn";
describe("components/widgets/longhorn", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders an empty container while loading", () => {
useSWR.mockReturnValue({ data: undefined, error: undefined });
const { container } = renderWithProviders(<Longhorn options={{ nodes: true, total: true }} />, {
settings: { target: "_self" },
});
expect(container.querySelector(".infomation-widget-longhorn")).not.toBeNull();
expect(screen.queryAllByTestId("longhorn-node")).toHaveLength(0);
});
it("filters nodes based on options (total/include)", () => {
useSWR.mockReturnValue({
data: {
nodes: [{ id: "total" }, { id: "node1" }, { id: "node2" }],
},
error: undefined,
});
renderWithProviders(
<Longhorn options={{ nodes: true, total: true, include: ["node1"], expanded: false, labels: false }} />,
{ settings: { target: "_self" } },
);
const nodes = screen.getAllByTestId("longhorn-node");
expect(nodes.map((n) => n.getAttribute("data-id"))).toEqual(["total", "node1"]);
});
});

View File

@@ -0,0 +1,43 @@
// @vitest-environment jsdom
import { screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() }));
vi.mock("swr", () => ({ default: useSWR }));
import OpenMeteo from "./openmeteo";
describe("components/widgets/openmeteo", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders a location prompt when no coordinates are available", () => {
renderWithProviders(<OpenMeteo options={{}} />, { settings: { target: "_self" } });
expect(screen.getByText("weather.current")).toBeInTheDocument();
expect(screen.getByText("weather.allow")).toBeInTheDocument();
});
it("renders temperature and condition when coordinates are provided", async () => {
useSWR.mockReturnValue({
data: {
current_weather: { temperature: 22.2, weathercode: 0, time: "2020-01-01T12:00" },
daily: { sunrise: ["2020-01-01T06:00"], sunset: ["2020-01-01T18:00"] },
},
error: undefined,
});
renderWithProviders(<OpenMeteo options={{ latitude: 1, longitude: 2, label: "Home", format: {} }} />, {
settings: { target: "_self" },
});
await waitFor(() => {
expect(screen.getByText("Home, 22.2")).toBeInTheDocument();
});
expect(screen.getByText("wmo.0-day")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,45 @@
// @vitest-environment jsdom
import { screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() }));
vi.mock("swr", () => ({ default: useSWR }));
import OpenWeatherMap from "./weather";
describe("components/widgets/openweathermap", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders a location prompt when no coordinates are available", () => {
renderWithProviders(<OpenWeatherMap options={{}} />, { settings: { target: "_self" } });
expect(screen.getByText("weather.current")).toBeInTheDocument();
expect(screen.getByText("weather.allow")).toBeInTheDocument();
});
it("renders temperature and description when coordinates are provided", async () => {
useSWR.mockReturnValue({
data: {
main: { temp: 71 },
weather: [{ id: 800, description: "clear sky" }],
dt: 10,
sys: { sunrise: 0, sunset: 100 },
},
error: undefined,
});
renderWithProviders(<OpenWeatherMap options={{ latitude: 1, longitude: 2, label: "Home", format: {} }} />, {
settings: { target: "_self" },
});
await waitFor(() => {
expect(screen.getByText("Home, 71")).toBeInTheDocument();
});
expect(screen.getByText("clear sky")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,42 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
vi.mock("./cpu", () => ({ default: () => <div data-testid="resources-cpu" /> }));
vi.mock("./memory", () => ({ default: () => <div data-testid="resources-memory" /> }));
vi.mock("./disk", () => ({ default: ({ options }) => <div data-testid="resources-disk" data-disk={options.disk} /> }));
vi.mock("./network", () => ({ default: () => <div data-testid="resources-network" /> }));
vi.mock("./cputemp", () => ({ default: () => <div data-testid="resources-cputemp" /> }));
vi.mock("./uptime", () => ({ default: () => <div data-testid="resources-uptime" /> }));
import Resources from "./resources";
describe("components/widgets/resources", () => {
it("renders selected resource blocks and an optional label", () => {
renderWithProviders(
<Resources
options={{
cpu: true,
memory: true,
disk: ["/", "/data"],
network: true,
cputemp: true,
uptime: true,
label: "Host A",
}}
/>,
{ settings: { target: "_self" } },
);
expect(screen.getByTestId("resources-cpu")).toBeInTheDocument();
expect(screen.getByTestId("resources-memory")).toBeInTheDocument();
expect(screen.getAllByTestId("resources-disk")).toHaveLength(2);
expect(screen.getByTestId("resources-network")).toBeInTheDocument();
expect(screen.getByTestId("resources-cputemp")).toBeInTheDocument();
expect(screen.getByTestId("resources-uptime")).toBeInTheDocument();
expect(screen.getByText("Host A")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,49 @@
// @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";
// HeadlessUI is hard to test reliably; stub the primitives to simple pass-through components.
vi.mock("@headlessui/react", async () => {
const React = await import("react");
const { Fragment } = React;
function passthrough({ as: As = "div", children, ...props }) {
if (As === Fragment) return <>{typeof children === "function" ? children({ active: false }) : children}</>;
const content = typeof children === "function" ? children({ active: false }) : children;
return <As {...props}>{content}</As>;
}
return {
Combobox: passthrough,
ComboboxInput: (props) => <input {...props} />,
ComboboxOption: passthrough,
ComboboxOptions: passthrough,
Listbox: passthrough,
ListboxButton: (props) => <button type="button" {...props} />,
ListboxOption: passthrough,
ListboxOptions: passthrough,
Transition: ({ children }) => <>{children}</>,
};
});
import Search from "./search";
describe("components/widgets/search", () => {
it("opens a search URL when Enter is pressed", () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
renderWithProviders(<Search options={{ provider: ["google"], showSearchSuggestions: false, target: "_self" }} />, {
settings: { target: "_blank" },
});
const input = screen.getByPlaceholderText("search.placeholder");
fireEvent.change(input, { target: { value: "hello world" } });
fireEvent.keyDown(input, { key: "Enter" });
expect(openSpy).toHaveBeenCalledWith("https://www.google.com/search?q=hello%20world", "_self");
openSpy.mockRestore();
});
});

View File

@@ -0,0 +1,46 @@
// @vitest-environment jsdom
import { fireEvent, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() }));
vi.mock("swr", () => ({ default: useSWR }));
import Stocks from "./stocks";
describe("components/widgets/stocks", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders a loading state while waiting for data", () => {
useSWR.mockReturnValue({ data: undefined, error: undefined });
renderWithProviders(<Stocks options={{}} />, { settings: { target: "_self" } });
expect(screen.getByText(/stocks\.loading/)).toBeInTheDocument();
});
it("toggles between price and percent change on click", () => {
useSWR.mockReturnValue({
data: {
stocks: [
{ ticker: "NASDAQ:AAPL", currentPrice: 123.45, percentChange: 1.23 },
{ ticker: "MSFT", currentPrice: 99.99, percentChange: -0.5 },
],
},
error: undefined,
});
renderWithProviders(<Stocks options={{ color: false }} />, { settings: { target: "_self" } });
expect(screen.getByText("AAPL")).toBeInTheDocument();
expect(screen.getByText("123.45")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button"));
expect(screen.getByText("1.23%")).toBeInTheDocument();
expect(screen.getByText("-0.5%")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,59 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
vi.mock("utils/proxy/use-widget-api", () => ({
default: useWidgetAPI,
}));
import UnifiConsole from "./unifi_console";
describe("components/widgets/unifi_console", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders a wait state when no site is available yet", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
renderWithProviders(<UnifiConsole options={{ index: 0 }} />, { settings: { target: "_self" } });
expect(screen.getByText("unifi.wait")).toBeInTheDocument();
});
it("renders site name and uptime when data is available", () => {
useWidgetAPI.mockReturnValue({
data: {
data: [
{
name: "default",
desc: "Home",
health: [
{
subsystem: "wan",
status: "ok",
gw_name: "Router",
"gw_system-stats": { uptime: 172800 },
},
{ subsystem: "lan", status: "unknown" },
{ subsystem: "wlan", status: "unknown" },
],
},
],
},
error: undefined,
});
renderWithProviders(<UnifiConsole options={{ index: 0 }} />, { settings: { target: "_self" } });
expect(screen.getByText("Router")).toBeInTheDocument();
// common.number is mocked to return the numeric value as a string.
expect(screen.getByText("2")).toBeInTheDocument();
expect(screen.getByText("unifi.days")).toBeInTheDocument();
});
});