From 3a6faa3f412f7b17922d45f2a79e4d807fd38ff0 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:29:04 -0800 Subject: [PATCH] test: add info widget component tests --- .../widgets/datetime/datetime.test.jsx | 32 ++++++++++ .../widgets/glances/glances.test.jsx | 51 ++++++++++++++++ .../widgets/greeting/greeting.test.jsx | 20 +++++++ .../widgets/kubernetes/kubernetes.test.jsx | 45 ++++++++++++++ src/components/widgets/logo/logo.test.jsx | 26 ++++++++ .../widgets/longhorn/longhorn.test.jsx | 50 ++++++++++++++++ .../widgets/openmeteo/openmeteo.test.jsx | 43 ++++++++++++++ .../widgets/openweathermap/weather.test.jsx | 45 ++++++++++++++ .../widgets/resources/resources.test.jsx | 42 +++++++++++++ src/components/widgets/search/search.test.jsx | 49 +++++++++++++++ src/components/widgets/stocks/stocks.test.jsx | 46 +++++++++++++++ .../unifi_console/unifi_console.test.jsx | 59 +++++++++++++++++++ 12 files changed, 508 insertions(+) create mode 100644 src/components/widgets/datetime/datetime.test.jsx create mode 100644 src/components/widgets/glances/glances.test.jsx create mode 100644 src/components/widgets/greeting/greeting.test.jsx create mode 100644 src/components/widgets/kubernetes/kubernetes.test.jsx create mode 100644 src/components/widgets/logo/logo.test.jsx create mode 100644 src/components/widgets/longhorn/longhorn.test.jsx create mode 100644 src/components/widgets/openmeteo/openmeteo.test.jsx create mode 100644 src/components/widgets/openweathermap/weather.test.jsx create mode 100644 src/components/widgets/resources/resources.test.jsx create mode 100644 src/components/widgets/search/search.test.jsx create mode 100644 src/components/widgets/stocks/stocks.test.jsx create mode 100644 src/components/widgets/unifi_console/unifi_console.test.jsx diff --git a/src/components/widgets/datetime/datetime.test.jsx b/src/components/widgets/datetime/datetime.test.jsx new file mode 100644 index 000000000..da16f9dc6 --- /dev/null +++ b/src/components/widgets/datetime/datetime.test.jsx @@ -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(, { 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(); + } + }); +}); diff --git a/src/components/widgets/glances/glances.test.jsx b/src/components/widgets/glances/glances.test.jsx new file mode 100644 index 000000000..1450120e4 --- /dev/null +++ b/src/components/widgets/glances/glances.test.jsx @@ -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(, { + 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(, { + 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(); + }); +}); diff --git a/src/components/widgets/greeting/greeting.test.jsx b/src/components/widgets/greeting/greeting.test.jsx new file mode 100644 index 000000000..6fd6d8509 --- /dev/null +++ b/src/components/widgets/greeting/greeting.test.jsx @@ -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(, { settings: { target: "_self" } }); + expect(container).toBeEmptyDOMElement(); + }); + + it("renders configured greeting text", () => { + renderWithProviders(, { settings: { target: "_self" } }); + expect(screen.getByText("Hello there")).toBeInTheDocument(); + }); +}); diff --git a/src/components/widgets/kubernetes/kubernetes.test.jsx b/src/components/widgets/kubernetes/kubernetes.test.jsx new file mode 100644 index 000000000..b8a898ac8 --- /dev/null +++ b/src/components/widgets/kubernetes/kubernetes.test.jsx @@ -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 }) =>
, +})); + +import Kubernetes from "./kubernetes"; + +describe("components/widgets/kubernetes", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders placeholder nodes while loading", () => { + useSWR.mockReturnValue({ data: undefined, error: undefined }); + + renderWithProviders(, { + 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(, { + settings: { target: "_self" }, + }); + + // cluster + 2 nodes + expect(screen.getAllByTestId("kube-node")).toHaveLength(3); + }); +}); diff --git a/src/components/widgets/logo/logo.test.jsx b/src/components/widgets/logo/logo.test.jsx new file mode 100644 index 000000000..931b4e832 --- /dev/null +++ b/src/components/widgets/logo/logo.test.jsx @@ -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 }) =>
, +})); + +import Logo from "./logo"; + +describe("components/widgets/logo", () => { + it("renders a fallback SVG when no icon is configured", () => { + const { container } = renderWithProviders(, { settings: { target: "_self" } }); + expect(screen.queryByTestId("resolved-icon")).toBeNull(); + expect(container.querySelector("svg")).not.toBeNull(); + }); + + it("renders the configured icon via ResolvedIcon", () => { + renderWithProviders(, { settings: { target: "_self" } }); + const icon = screen.getByTestId("resolved-icon"); + expect(icon.getAttribute("data-icon")).toBe("mdi:home"); + }); +}); diff --git a/src/components/widgets/longhorn/longhorn.test.jsx b/src/components/widgets/longhorn/longhorn.test.jsx new file mode 100644 index 000000000..8f6c8e757 --- /dev/null +++ b/src/components/widgets/longhorn/longhorn.test.jsx @@ -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 }) =>
, +})); + +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(, { + 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( + , + { settings: { target: "_self" } }, + ); + + const nodes = screen.getAllByTestId("longhorn-node"); + expect(nodes.map((n) => n.getAttribute("data-id"))).toEqual(["total", "node1"]); + }); +}); diff --git a/src/components/widgets/openmeteo/openmeteo.test.jsx b/src/components/widgets/openmeteo/openmeteo.test.jsx new file mode 100644 index 000000000..36931ca54 --- /dev/null +++ b/src/components/widgets/openmeteo/openmeteo.test.jsx @@ -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(, { 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(, { + settings: { target: "_self" }, + }); + + await waitFor(() => { + expect(screen.getByText("Home, 22.2")).toBeInTheDocument(); + }); + expect(screen.getByText("wmo.0-day")).toBeInTheDocument(); + }); +}); diff --git a/src/components/widgets/openweathermap/weather.test.jsx b/src/components/widgets/openweathermap/weather.test.jsx new file mode 100644 index 000000000..3b7deaede --- /dev/null +++ b/src/components/widgets/openweathermap/weather.test.jsx @@ -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(, { 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(, { + settings: { target: "_self" }, + }); + + await waitFor(() => { + expect(screen.getByText("Home, 71")).toBeInTheDocument(); + }); + expect(screen.getByText("clear sky")).toBeInTheDocument(); + }); +}); diff --git a/src/components/widgets/resources/resources.test.jsx b/src/components/widgets/resources/resources.test.jsx new file mode 100644 index 000000000..05b1b4e88 --- /dev/null +++ b/src/components/widgets/resources/resources.test.jsx @@ -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: () =>
})); +vi.mock("./memory", () => ({ default: () =>
})); +vi.mock("./disk", () => ({ default: ({ options }) =>
})); +vi.mock("./network", () => ({ default: () =>
})); +vi.mock("./cputemp", () => ({ default: () =>
})); +vi.mock("./uptime", () => ({ default: () =>
})); + +import Resources from "./resources"; + +describe("components/widgets/resources", () => { + it("renders selected resource blocks and an optional label", () => { + renderWithProviders( + , + { 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(); + }); +}); diff --git a/src/components/widgets/search/search.test.jsx b/src/components/widgets/search/search.test.jsx new file mode 100644 index 000000000..4dbaf0691 --- /dev/null +++ b/src/components/widgets/search/search.test.jsx @@ -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 {content}; + } + + return { + Combobox: passthrough, + ComboboxInput: (props) => , + ComboboxOption: passthrough, + ComboboxOptions: passthrough, + Listbox: passthrough, + ListboxButton: (props) =>