mirror of
https://github.com/gethomepage/homepage.git
synced 2026-04-04 09:11:21 -07:00
Chore: homepage tests (#6278)
This commit is contained in:
32
src/components/widgets/datetime/datetime.test.jsx
Normal file
32
src/components/widgets/datetime/datetime.test.jsx
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
166
src/components/widgets/glances/glances.test.jsx
Normal file
166
src/components/widgets/glances/glances.test.jsx
Normal file
@@ -0,0 +1,166 @@
|
||||
// @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 an error state when SWR errors", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
|
||||
renderWithProviders(<Glances options={{ cpu: true, mem: true }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("widget.api_error")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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 placeholder disk resources when loading and disk is an array", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<Glances options={{ disk: ["/", "/data"] }} />, { settings: { target: "_self" } });
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it("handles cpu sensor retrieval failures gracefully", () => {
|
||||
const sensor = {
|
||||
label: "cpu_thermal-0",
|
||||
type: "temperature_core",
|
||||
get value() {
|
||||
throw new Error("boom");
|
||||
},
|
||||
warning: 90,
|
||||
};
|
||||
|
||||
useSWR.mockReturnValue({
|
||||
data: {
|
||||
cpu: { total: 1 },
|
||||
load: { min15: 1 },
|
||||
mem: { available: 1, total: 1, percent: 1 },
|
||||
fs: [],
|
||||
sensors: [sensor],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Glances options={{ cputemp: true }} />, { settings: { target: "_self" } });
|
||||
|
||||
// When sensor processing fails, it should not render the temp block.
|
||||
expect(screen.queryByText("glances.temp")).toBeNull();
|
||||
expect(screen.getByText("glances.cpu")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders temperature in fahrenheit for matching cpu sensors and marks the widget expanded", () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: {
|
||||
cpu: { total: 1 },
|
||||
load: { min15: 1 },
|
||||
mem: { available: 1, total: 1, percent: 1 },
|
||||
fs: [],
|
||||
sensors: [
|
||||
{ label: "cpu_thermal-0", type: "temperature_core", value: 40, warning: 90 },
|
||||
{ label: "Core 1", type: "temperature_core", value: 50, warning: 100 },
|
||||
],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<Glances options={{ cputemp: true, units: "imperial", expanded: true, url: "http://glances" }} />,
|
||||
{
|
||||
settings: { target: "_self" },
|
||||
},
|
||||
);
|
||||
|
||||
// avg(40,50)=45C => 113F
|
||||
expect(screen.getByText("113")).toBeInTheDocument();
|
||||
expect(screen.getByRole("link")).toHaveClass("expanded");
|
||||
});
|
||||
|
||||
it("renders disk resources for an array of mount points and filters missing mounts", () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: {
|
||||
cpu: { total: 1 },
|
||||
load: { min15: 1 },
|
||||
mem: { available: 1, total: 1, percent: 1 },
|
||||
fs: [{ mnt_point: "/", free: 10, size: 20, percent: 50 }],
|
||||
sensors: [],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<Glances options={{ disk: ["/", "/missing"], diskUnits: "bbytes", expanded: true, url: "http://glances" }} />,
|
||||
{
|
||||
settings: { target: "_self" },
|
||||
},
|
||||
);
|
||||
|
||||
// only one mount exists, but both free + total values should render for it
|
||||
expect(screen.getByText("10")).toBeInTheDocument();
|
||||
expect(screen.getByText("20")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("formats uptime into translated day/hour labels", () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: {
|
||||
cpu: { total: 1 },
|
||||
load: { min15: 1 },
|
||||
mem: { available: 1, total: 1, percent: 1 },
|
||||
fs: [],
|
||||
sensors: [],
|
||||
uptime: "1 days, 00:00:00",
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Glances options={{ uptime: true, url: "http://glances" }} />, {
|
||||
settings: { target: "_self" },
|
||||
});
|
||||
|
||||
expect(screen.getByText("1glances.days 00glances.hours")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
20
src/components/widgets/greeting/greeting.test.jsx
Normal file
20
src/components/widgets/greeting/greeting.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
55
src/components/widgets/kubernetes/kubernetes.test.jsx
Normal file
55
src/components/widgets/kubernetes/kubernetes.test.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
// @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 an error state when SWR errors", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
|
||||
renderWithProviders(<Kubernetes options={{ cluster: { show: true }, nodes: { show: true } }} />, {
|
||||
settings: { target: "_self" },
|
||||
});
|
||||
|
||||
expect(screen.getByText("widget.api_error")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
35
src/components/widgets/kubernetes/node.test.jsx
Normal file
35
src/components/widgets/kubernetes/node.test.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import Node from "./node";
|
||||
|
||||
describe("components/widgets/kubernetes/node", () => {
|
||||
it("renders cluster label when showLabel is enabled", () => {
|
||||
const data = { cpu: { percent: 50 }, memory: { free: 123, percent: 10 } };
|
||||
|
||||
const { container } = render(<Node type="cluster" options={{ showLabel: true, label: "Cluster A" }} data={data} />);
|
||||
|
||||
expect(screen.getByText("50")).toBeInTheDocument();
|
||||
expect(screen.getByText("123")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cluster A")).toBeInTheDocument();
|
||||
expect(container.querySelectorAll('div[style*="width:"]').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders node name when showLabel is enabled for node type", () => {
|
||||
const data = { name: "node-1", ready: true, cpu: { percent: 1 }, memory: { free: 2, percent: 3 } };
|
||||
|
||||
render(<Node type="node" options={{ showLabel: true }} data={data} />);
|
||||
|
||||
expect(screen.getByText("node-1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a warning icon when the node is not ready", () => {
|
||||
const data = { name: "node-2", ready: false, cpu: { percent: 1 }, memory: { free: 2, percent: 3 } };
|
||||
|
||||
render(<Node type="node" options={{ showLabel: true }} data={data} />);
|
||||
|
||||
expect(screen.getByText("node-2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
26
src/components/widgets/logo/logo.test.jsx
Normal file
26
src/components/widgets/logo/logo.test.jsx
Normal 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");
|
||||
});
|
||||
});
|
||||
72
src/components/widgets/longhorn/longhorn.test.jsx
Normal file
72
src/components/widgets/longhorn/longhorn.test.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
// @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 error state when SWR errors", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
|
||||
renderWithProviders(<Longhorn options={{ nodes: true, total: true }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("widget.api_error")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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"]);
|
||||
});
|
||||
|
||||
it("omits non-total nodes when options.nodes is false", () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: {
|
||||
nodes: [{ id: "total" }, { id: "node1" }],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Longhorn options={{ nodes: false, total: true }} />, { settings: { target: "_self" } });
|
||||
|
||||
const nodes = screen.getAllByTestId("longhorn-node");
|
||||
expect(nodes.map((n) => n.getAttribute("data-id"))).toEqual(["total"]);
|
||||
});
|
||||
});
|
||||
32
src/components/widgets/longhorn/node.test.jsx
Normal file
32
src/components/widgets/longhorn/node.test.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { Resource } = vi.hoisted(() => ({
|
||||
Resource: vi.fn(({ children }) => <div data-testid="lh-resource">{children}</div>),
|
||||
}));
|
||||
|
||||
vi.mock("../widget/resource", () => ({
|
||||
default: Resource,
|
||||
}));
|
||||
|
||||
vi.mock("../widget/widget_label", () => ({
|
||||
default: ({ label }) => <div data-testid="lh-label">{label}</div>,
|
||||
}));
|
||||
|
||||
import Node from "./node";
|
||||
|
||||
describe("components/widgets/longhorn/node", () => {
|
||||
it("passes calculated percentage and renders label when enabled", () => {
|
||||
const data = { node: { id: "n1", available: 25, maximum: 100 } };
|
||||
|
||||
render(<Node data={{ node: data.node }} expanded labels />);
|
||||
|
||||
expect(Resource).toHaveBeenCalledTimes(1);
|
||||
const callProps = Resource.mock.calls[0][0];
|
||||
expect(callProps.percentage).toBe(75);
|
||||
expect(callProps.expanded).toBe(true);
|
||||
expect(screen.getByTestId("lh-label")).toHaveTextContent("n1");
|
||||
});
|
||||
});
|
||||
135
src/components/widgets/openmeteo/openmeteo.test.jsx
Normal file
135
src/components/widgets/openmeteo/openmeteo.test.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, 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("react-icons/md", () => ({
|
||||
MdLocationDisabled: (props) => <svg data-testid="location-disabled" {...props} />,
|
||||
MdLocationSearching: (props) => <svg data-testid="location-searching" {...props} />,
|
||||
}));
|
||||
|
||||
import OpenMeteo from "./openmeteo";
|
||||
|
||||
describe("components/widgets/openmeteo", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("renders an error state when the widget api returns an error", async () => {
|
||||
useSWR.mockReturnValue({ data: { error: "nope" }, error: undefined });
|
||||
|
||||
renderWithProviders(<OpenMeteo options={{ latitude: 1, longitude: 2 }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("widget.api_error")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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("requests browser geolocation on click and then renders the updating state", async () => {
|
||||
const getCurrentPosition = vi.fn((success) => success({ coords: { latitude: 10, longitude: 20 } }));
|
||||
vi.stubGlobal("navigator", {
|
||||
permissions: { query: vi.fn().mockResolvedValue({ state: "prompt" }) },
|
||||
geolocation: { getCurrentPosition },
|
||||
});
|
||||
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<OpenMeteo options={{}} />, { settings: { target: "_self" } });
|
||||
|
||||
screen.getByRole("button").click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getCurrentPosition).toHaveBeenCalled();
|
||||
});
|
||||
expect(screen.getByText("weather.updating")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clears the requesting state when the browser denies geolocation", async () => {
|
||||
const getCurrentPosition = vi.fn((_success, failure) => setTimeout(() => failure(), 10));
|
||||
vi.stubGlobal("navigator", {
|
||||
permissions: { query: vi.fn().mockResolvedValue({ state: "prompt" }) },
|
||||
geolocation: { getCurrentPosition },
|
||||
});
|
||||
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<OpenMeteo options={{}} />, { settings: { target: "_self" } });
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
expect(screen.getByTestId("location-searching")).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("location-disabled")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("auto-requests geolocation when permissions are granted", async () => {
|
||||
const getCurrentPosition = vi.fn((success) => success({ coords: { latitude: 30, longitude: 40 } }));
|
||||
const query = vi.fn().mockResolvedValue({ state: "granted" });
|
||||
vi.stubGlobal("navigator", {
|
||||
permissions: { query },
|
||||
geolocation: { getCurrentPosition },
|
||||
});
|
||||
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<OpenMeteo options={{}} />, { settings: { target: "_self" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(query).toHaveBeenCalled();
|
||||
expect(getCurrentPosition).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it("uses night conditions and fahrenheit units when configured", async () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: {
|
||||
current_weather: { temperature: 72, weathercode: 1, time: "2020-01-01T23:00" },
|
||||
daily: { sunrise: ["2020-01-01T06:00"], sunset: ["2020-01-01T18:00"] },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<OpenMeteo options={{ latitude: 1, longitude: 2, units: "imperial", format: {} }} />, {
|
||||
settings: { target: "_self" },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("72")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("wmo.1-night")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
141
src/components/widgets/openweathermap/weather.test.jsx
Normal file
141
src/components/widgets/openweathermap/weather.test.jsx
Normal file
@@ -0,0 +1,141 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, 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("react-icons/md", () => ({
|
||||
MdLocationDisabled: (props) => <svg data-testid="location-disabled" {...props} />,
|
||||
MdLocationSearching: (props) => <svg data-testid="location-searching" {...props} />,
|
||||
}));
|
||||
|
||||
import OpenWeatherMap from "./weather";
|
||||
|
||||
describe("components/widgets/openweathermap", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("renders an error state when SWR errors or the API reports an auth error", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
renderWithProviders(<OpenWeatherMap options={{ latitude: 1, longitude: 2 }} />, { settings: { target: "_self" } });
|
||||
expect(screen.getByText("widget.api_error")).toBeInTheDocument();
|
||||
|
||||
useSWR.mockReturnValue({ data: { cod: 401 }, error: undefined });
|
||||
renderWithProviders(<OpenWeatherMap options={{ latitude: 1, longitude: 2 }} />, { settings: { target: "_self" } });
|
||||
expect(screen.getAllByText("widget.api_error").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
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("auto-requests geolocation when permissions are granted", async () => {
|
||||
const getCurrentPosition = vi.fn((success) => success({ coords: { latitude: 30, longitude: 40 } }));
|
||||
const query = vi.fn().mockResolvedValue({ state: "granted" });
|
||||
vi.stubGlobal("navigator", {
|
||||
permissions: { query },
|
||||
geolocation: { getCurrentPosition },
|
||||
});
|
||||
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<OpenWeatherMap options={{}} />, { settings: { target: "_self" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(query).toHaveBeenCalled();
|
||||
expect(getCurrentPosition).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("requests browser geolocation on click and then renders the updating state", async () => {
|
||||
const getCurrentPosition = vi.fn((success) => success({ coords: { latitude: 10, longitude: 20 } }));
|
||||
vi.stubGlobal("navigator", {
|
||||
permissions: { query: vi.fn().mockResolvedValue({ state: "prompt" }) },
|
||||
geolocation: { getCurrentPosition },
|
||||
});
|
||||
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<OpenWeatherMap options={{}} />, { settings: { target: "_self" } });
|
||||
|
||||
screen.getByRole("button").click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getCurrentPosition).toHaveBeenCalled();
|
||||
});
|
||||
expect(screen.getByText("weather.updating")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clears the requesting state when the browser denies geolocation", async () => {
|
||||
const getCurrentPosition = vi.fn((_success, failure) => setTimeout(() => failure(), 10));
|
||||
vi.stubGlobal("navigator", {
|
||||
permissions: { query: vi.fn().mockResolvedValue({ state: "prompt" }) },
|
||||
geolocation: { getCurrentPosition },
|
||||
});
|
||||
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<OpenWeatherMap options={{}} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByTestId("location-disabled")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
expect(screen.getByTestId("location-searching")).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("location-disabled")).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();
|
||||
});
|
||||
|
||||
it("uses night conditions and celsius units when configured", async () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: {
|
||||
main: { temp: 10 },
|
||||
weather: [{ id: 800, description: "clear sky" }],
|
||||
dt: 200,
|
||||
sys: { sunrise: 0, sunset: 100 },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<OpenWeatherMap options={{ latitude: 1, longitude: 2, units: "metric", format: {} }} />, {
|
||||
settings: { target: "_self" },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("10")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
20
src/components/widgets/queue/queueEntry.test.jsx
Normal file
20
src/components/widgets/queue/queueEntry.test.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import QueueEntry from "./queueEntry";
|
||||
|
||||
describe("components/widgets/queue/queueEntry", () => {
|
||||
it("renders title and progress width", () => {
|
||||
const { container } = render(
|
||||
<QueueEntry title="Download" activity="Downloading" timeLeft="1m" progress={42} size="1GB" />,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Download")).toBeInTheDocument();
|
||||
expect(screen.getByText("1GB - Downloading - 1m")).toBeInTheDocument();
|
||||
|
||||
const bar = container.querySelector("div[style]");
|
||||
expect(bar.style.width).toBe("42%");
|
||||
});
|
||||
});
|
||||
55
src/components/widgets/resources/cpu.test.jsx
Normal file
55
src/components/widgets/resources/cpu.test.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { useSWR, Resource, Error } = vi.hoisted(() => ({
|
||||
useSWR: vi.fn(),
|
||||
Resource: vi.fn(() => <div data-testid="resource" />),
|
||||
Error: vi.fn(() => <div data-testid="error" />),
|
||||
}));
|
||||
|
||||
vi.mock("swr", () => ({ default: useSWR }));
|
||||
vi.mock("../widget/resource", () => ({ default: Resource }));
|
||||
vi.mock("../widget/error", () => ({ default: Error }));
|
||||
|
||||
import Cpu from "./cpu";
|
||||
|
||||
describe("components/widgets/resources/cpu", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders a placeholder Resource while loading", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
render(<Cpu expanded refresh={1000} />);
|
||||
|
||||
expect(Resource).toHaveBeenCalled();
|
||||
const props = Resource.mock.calls[0][0];
|
||||
expect(props.value).toBe("-");
|
||||
expect(props.expanded).toBe(true);
|
||||
});
|
||||
|
||||
it("renders usage/load values when data is present", () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: { cpu: { usage: 12.3, load: 1.23 } },
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
render(<Cpu expanded={false} />);
|
||||
|
||||
const props = Resource.mock.calls[0][0];
|
||||
expect(props.value).toBe("12.3");
|
||||
expect(props.expandedValue).toBe("1.23");
|
||||
expect(props.percentage).toBe(12.3);
|
||||
});
|
||||
|
||||
it("renders Error when SWR errors", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
|
||||
render(<Cpu expanded />);
|
||||
|
||||
expect(Error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
53
src/components/widgets/resources/cputemp.test.jsx
Normal file
53
src/components/widgets/resources/cputemp.test.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { useSWR, Resource, Error } = vi.hoisted(() => ({
|
||||
useSWR: vi.fn(),
|
||||
Resource: vi.fn(() => <div data-testid="resource" />),
|
||||
Error: vi.fn(() => <div data-testid="error" />),
|
||||
}));
|
||||
|
||||
vi.mock("swr", () => ({ default: useSWR }));
|
||||
vi.mock("../widget/resource", () => ({ default: Resource }));
|
||||
vi.mock("../widget/error", () => ({ default: Error }));
|
||||
|
||||
import CpuTemp from "./cputemp";
|
||||
|
||||
describe("components/widgets/resources/cputemp", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders placeholder when temperature data is missing", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
render(<CpuTemp expanded units="metric" />);
|
||||
|
||||
const props = Resource.mock.calls[0][0];
|
||||
expect(props.value).toBe("-");
|
||||
});
|
||||
|
||||
it("averages core temps, converts to fahrenheit and computes percentage", () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: { cputemp: { main: 10, cores: [10, 10], max: 20 } },
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
render(<CpuTemp expanded={false} units="imperial" tempmin={0} tempmax={-1} />);
|
||||
|
||||
const props = Resource.mock.calls[0][0];
|
||||
// common.number mock returns string of value
|
||||
expect(props.value).toBe("50");
|
||||
expect(props.expandedValue).toBe("68");
|
||||
expect(props.percentage).toBe(74);
|
||||
});
|
||||
|
||||
it("renders Error when SWR errors", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
|
||||
render(<CpuTemp expanded units="metric" />);
|
||||
|
||||
expect(Error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
53
src/components/widgets/resources/disk.test.jsx
Normal file
53
src/components/widgets/resources/disk.test.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { useSWR, Resource, Error } = vi.hoisted(() => ({
|
||||
useSWR: vi.fn(),
|
||||
Resource: vi.fn(() => <div data-testid="resource" />),
|
||||
Error: vi.fn(() => <div data-testid="error" />),
|
||||
}));
|
||||
|
||||
vi.mock("swr", () => ({ default: useSWR }));
|
||||
vi.mock("../widget/resource", () => ({ default: Resource }));
|
||||
vi.mock("../widget/error", () => ({ default: Error }));
|
||||
|
||||
import Disk from "./disk";
|
||||
|
||||
describe("components/widgets/resources/disk", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders a placeholder Resource while loading", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
render(<Disk options={{ disk: "/" }} expanded />);
|
||||
|
||||
const props = Resource.mock.calls[0][0];
|
||||
expect(props.value).toBe("-");
|
||||
});
|
||||
|
||||
it("computes percent used from size/available and renders bytes", () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: { drive: { size: 100, available: 40 } },
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
render(<Disk options={{ disk: "/data" }} diskUnits="bytes" expanded={false} />);
|
||||
|
||||
const props = Resource.mock.calls[0][0];
|
||||
expect(props.value).toBe("40");
|
||||
expect(props.expandedValue).toBe("100");
|
||||
expect(props.percentage).toBe(60);
|
||||
});
|
||||
|
||||
it("renders Error when SWR errors", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
|
||||
render(<Disk options={{ disk: "/" }} expanded />);
|
||||
|
||||
expect(Error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
53
src/components/widgets/resources/memory.test.jsx
Normal file
53
src/components/widgets/resources/memory.test.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { useSWR, Resource, Error } = vi.hoisted(() => ({
|
||||
useSWR: vi.fn(),
|
||||
Resource: vi.fn(() => <div data-testid="resource" />),
|
||||
Error: vi.fn(() => <div data-testid="error" />),
|
||||
}));
|
||||
|
||||
vi.mock("swr", () => ({ default: useSWR }));
|
||||
vi.mock("../widget/resource", () => ({ default: Resource }));
|
||||
vi.mock("../widget/error", () => ({ default: Error }));
|
||||
|
||||
import Memory from "./memory";
|
||||
|
||||
describe("components/widgets/resources/memory", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders a placeholder Resource while loading", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
render(<Memory expanded />);
|
||||
|
||||
const props = Resource.mock.calls[0][0];
|
||||
expect(props.value).toBe("-");
|
||||
});
|
||||
|
||||
it("calculates percentage from active/total and renders available/total", () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: { memory: { available: 10, total: 20, active: 5 } },
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
render(<Memory expanded={false} />);
|
||||
|
||||
const props = Resource.mock.calls[0][0];
|
||||
expect(props.value).toBe("10");
|
||||
expect(props.expandedValue).toBe("20");
|
||||
expect(props.percentage).toBe(25);
|
||||
});
|
||||
|
||||
it("renders Error when SWR errors", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
|
||||
render(<Memory expanded />);
|
||||
|
||||
expect(Error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
57
src/components/widgets/resources/network.test.jsx
Normal file
57
src/components/widgets/resources/network.test.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { useSWR, Resource, Error } = vi.hoisted(() => ({
|
||||
useSWR: vi.fn(),
|
||||
Resource: vi.fn(() => <div data-testid="resource" />),
|
||||
Error: vi.fn(() => <div data-testid="error" />),
|
||||
}));
|
||||
|
||||
vi.mock("swr", () => ({ default: useSWR }));
|
||||
vi.mock("../widget/resource", () => ({ default: Resource }));
|
||||
vi.mock("../widget/error", () => ({ default: Error }));
|
||||
|
||||
import Network from "./network";
|
||||
|
||||
describe("components/widgets/resources/network", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("normalizes options.network=true to default interfaceName in the request", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
render(<Network options={{ network: true }} />);
|
||||
|
||||
expect(useSWR).toHaveBeenCalledWith(expect.stringContaining("interfaceName=default"), expect.any(Object));
|
||||
});
|
||||
|
||||
it("renders rates and usage percentage when data is present", () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: {
|
||||
network: { rx_sec: 3, tx_sec: 1, rx_bytes: 30, tx_bytes: 10 },
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
render(<Network options={{ network: "en0", expanded: true }} />);
|
||||
|
||||
const props = Resource.mock.calls[0][0];
|
||||
expect(props.value).toContain("1");
|
||||
expect(props.value).toContain("↑");
|
||||
expect(props.label).toContain("3");
|
||||
expect(props.label).toContain("↓");
|
||||
expect(props.percentage).toBe(75);
|
||||
expect(props.wide).toBe(true);
|
||||
});
|
||||
|
||||
it("renders Error when SWR errors", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
|
||||
render(<Network options={{ network: "en0" }} />);
|
||||
|
||||
expect(Error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
49
src/components/widgets/resources/resources.test.jsx
Normal file
49
src/components/widgets/resources/resources.test.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
// @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();
|
||||
});
|
||||
|
||||
it("renders a single disk block when disk is not an array", () => {
|
||||
renderWithProviders(<Resources options={{ disk: true }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getAllByTestId("resources-disk")).toHaveLength(1);
|
||||
expect(screen.getByTestId("resources-disk").getAttribute("data-disk")).toBe("true");
|
||||
});
|
||||
});
|
||||
54
src/components/widgets/resources/uptime.test.jsx
Normal file
54
src/components/widgets/resources/uptime.test.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { useSWR, Resource, Error } = vi.hoisted(() => ({
|
||||
useSWR: vi.fn(),
|
||||
Resource: vi.fn(() => <div data-testid="resource" />),
|
||||
Error: vi.fn(() => <div data-testid="error" />),
|
||||
}));
|
||||
|
||||
vi.mock("swr", () => ({ default: useSWR }));
|
||||
vi.mock("../widget/resource", () => ({ default: Resource }));
|
||||
vi.mock("../widget/error", () => ({ default: Error }));
|
||||
|
||||
import Uptime from "./uptime";
|
||||
|
||||
describe("components/widgets/resources/uptime", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders a placeholder while loading", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
render(<Uptime />);
|
||||
expect(Resource).toHaveBeenCalled();
|
||||
expect(Resource.mock.calls[0][0].value).toBe("-");
|
||||
});
|
||||
|
||||
it("renders formatted duration and sets percentage based on current seconds", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.setSystemTime(new Date("2020-01-01T00:00:30.000Z"));
|
||||
|
||||
useSWR.mockReturnValue({ data: { uptime: 1234 }, error: undefined });
|
||||
render(<Uptime />);
|
||||
|
||||
const props = Resource.mock.calls[0][0];
|
||||
expect(props.value).toBe("1234");
|
||||
expect(props.percentage).toBe("50");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("renders Error when SWR errors", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
|
||||
render(<Uptime />);
|
||||
|
||||
expect(Error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
18
src/components/widgets/resources/usage-bar.test.jsx
Normal file
18
src/components/widgets/resources/usage-bar.test.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
describe("components/widgets/resources/usage-bar", () => {
|
||||
it("normalizes percent to [0, 100] and applies width style", () => {
|
||||
const { container: c0 } = render(<UsageBar percent={-5} />);
|
||||
const inner0 = c0.querySelector("div > div > div");
|
||||
expect(inner0.style.width).toBe("0%");
|
||||
|
||||
const { container: c1 } = render(<UsageBar percent={150} />);
|
||||
const inner1 = c1.querySelector("div > div > div");
|
||||
expect(inner1.style.width).toBe("100%");
|
||||
});
|
||||
});
|
||||
@@ -82,12 +82,10 @@ export function getStoredProvider() {
|
||||
export default function Search({ options }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const availableProviderIds = getAvailableProviderIds(options);
|
||||
const availableProviderIds = getAvailableProviderIds(options) ?? [];
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
const [selectedProvider, setSelectedProvider] = useState(
|
||||
searchProviders[availableProviderIds[0] ?? searchProviders.google],
|
||||
);
|
||||
const [selectedProvider, setSelectedProvider] = useState(searchProviders[availableProviderIds[0] ?? "google"]);
|
||||
const [searchSuggestions, setSearchSuggestions] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -153,7 +151,7 @@ export default function Search({ options }) {
|
||||
}
|
||||
};
|
||||
|
||||
if (!availableProviderIds) {
|
||||
if (!availableProviderIds.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
198
src/components/widgets/search/search.test.jsx
Normal file
198
src/components/widgets/search/search.test.jsx
Normal file
@@ -0,0 +1,198 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, 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, createContext, useContext } = React;
|
||||
const ListboxContext = createContext(null);
|
||||
|
||||
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: ({ value, onChange, children, ...props }) => (
|
||||
<ListboxContext.Provider value={{ value, onChange }}>
|
||||
<div {...props}>{typeof children === "function" ? children({}) : children}</div>
|
||||
</ListboxContext.Provider>
|
||||
),
|
||||
ListboxButton: (props) => <button type="button" {...props} />,
|
||||
ListboxOption: ({ as: _as, value, children, ...props }) => {
|
||||
const ctx = useContext(ListboxContext);
|
||||
const content = typeof children === "function" ? children({ active: false }) : children;
|
||||
return (
|
||||
<div role="option" data-provider={value?.name} onClick={() => ctx?.onChange?.(value)} {...props}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
ListboxOptions: passthrough,
|
||||
Transition: ({ children }) => <>{children}</>,
|
||||
};
|
||||
});
|
||||
|
||||
import Search from "./search";
|
||||
|
||||
describe("components/widgets/search", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it("accepts provider configured as a string", () => {
|
||||
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
|
||||
renderWithProviders(
|
||||
<Search options={{ provider: "duckduckgo", showSearchSuggestions: false, target: "_self" }} />,
|
||||
{
|
||||
settings: {},
|
||||
},
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText("search.placeholder");
|
||||
fireEvent.change(input, { target: { value: "hello" } });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith("https://duckduckgo.com/?q=hello", "_self");
|
||||
openSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("returns null when the configured provider list contains no supported providers", () => {
|
||||
const { container } = renderWithProviders(<Search options={{ provider: "nope", showSearchSuggestions: false }} />, {
|
||||
settings: {},
|
||||
});
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("stores the selected provider in localStorage when it is changed", async () => {
|
||||
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
|
||||
renderWithProviders(
|
||||
<Search options={{ provider: ["google", "duckduckgo"], showSearchSuggestions: false, target: "_self" }} />,
|
||||
{
|
||||
settings: {},
|
||||
},
|
||||
);
|
||||
|
||||
const option = document.querySelector('[data-provider="DuckDuckGo"]');
|
||||
expect(option).not.toBeNull();
|
||||
fireEvent.click(option);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem("search-name")).toBe("DuckDuckGo");
|
||||
});
|
||||
|
||||
const input = screen.getByPlaceholderText("search.placeholder");
|
||||
fireEvent.change(input, { target: { value: "hello" } });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith("https://duckduckgo.com/?q=hello", "_self");
|
||||
openSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("uses a stored provider from localStorage when it is available and allowed", () => {
|
||||
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
|
||||
localStorage.setItem("search-name", "DuckDuckGo");
|
||||
|
||||
renderWithProviders(
|
||||
<Search options={{ provider: ["google", "duckduckgo"], showSearchSuggestions: false, target: "_self" }} />,
|
||||
{
|
||||
settings: {},
|
||||
},
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText("search.placeholder");
|
||||
fireEvent.change(input, { target: { value: "hello" } });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith("https://duckduckgo.com/?q=hello", "_self");
|
||||
openSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("uses a custom provider URL when the selected provider is custom", () => {
|
||||
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
|
||||
renderWithProviders(
|
||||
<Search
|
||||
options={{
|
||||
provider: ["custom"],
|
||||
url: "https://example.com/search?q=",
|
||||
showSearchSuggestions: false,
|
||||
target: "_self",
|
||||
}}
|
||||
/>,
|
||||
{ settings: {} },
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText("search.placeholder");
|
||||
fireEvent.change(input, { target: { value: "hello world" } });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith("https://example.com/search?q=hello%20world", "_self");
|
||||
openSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("fetches search suggestions and triggers a search when a suggestion is selected", async () => {
|
||||
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const fetchSpy = vi.fn(async () => ({
|
||||
json: async () => ["hel", ["hello", "help", "helm", "helium", "held"]],
|
||||
}));
|
||||
// eslint-disable-next-line no-global-assign
|
||||
fetch = fetchSpy;
|
||||
|
||||
renderWithProviders(<Search options={{ provider: ["google"], showSearchSuggestions: true, target: "_self" }} />, {
|
||||
settings: {},
|
||||
});
|
||||
|
||||
const input = screen.getByPlaceholderText("search.placeholder");
|
||||
fireEvent.change(input, { target: { value: "hel" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/api/search/searchSuggestion?query=hel&providerName=Google"),
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[value="hello"]')).toBeTruthy();
|
||||
});
|
||||
expect(document.querySelector('[value="held"]')).toBeNull();
|
||||
fireEvent.mouseDown(document.querySelector('[value="hello"]'));
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith("https://www.google.com/search?q=hello", "_self");
|
||||
|
||||
openSpy.mockRestore();
|
||||
// eslint-disable-next-line no-global-assign
|
||||
fetch = originalFetch;
|
||||
});
|
||||
});
|
||||
72
src/components/widgets/stocks/stocks.test.jsx
Normal file
72
src/components/widgets/stocks/stocks.test.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
// @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 an error widget when the api call fails", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
|
||||
renderWithProviders(<Stocks options={{}} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("widget.api_error")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it("shows api_error for null prices and uses colored classes when enabled", () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: {
|
||||
stocks: [{ ticker: "NASDAQ:AAPL", currentPrice: null, percentChange: -1 }],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Stocks options={{}} />, { settings: { target: "_self" } });
|
||||
|
||||
const apiError = screen.getByText("widget.api_error");
|
||||
expect(apiError.className).toContain("text-rose");
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
const percent = screen.getByText("-1%");
|
||||
expect(percent.className).toContain("text-rose");
|
||||
});
|
||||
});
|
||||
261
src/components/widgets/unifi_console/unifi_console.test.jsx
Normal file
261
src/components/widgets/unifi_console/unifi_console.test.jsx
Normal file
@@ -0,0 +1,261 @@
|
||||
// @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,
|
||||
}));
|
||||
|
||||
vi.mock("react-icons/bi", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
BiWifi: (props) => <svg data-testid="bi-wifi" {...props} />,
|
||||
BiNetworkChart: (props) => <svg data-testid="bi-network-chart" {...props} />,
|
||||
BiError: (props) => <svg data-testid="bi-error" {...props} />,
|
||||
BiCheckCircle: (props) => <svg data-testid="bi-check-circle" {...props} />,
|
||||
BiXCircle: (props) => <svg data-testid="bi-x-circle" {...props} />,
|
||||
};
|
||||
});
|
||||
|
||||
import UnifiConsole from "./unifi_console";
|
||||
|
||||
describe("components/widgets/unifi_console", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders an api error state when the widget api call fails", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
|
||||
renderWithProviders(<UnifiConsole options={{ index: 0 }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("widget.api_error")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it("selects a site by description when options.site is set", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
name: "default",
|
||||
desc: "Other",
|
||||
health: [
|
||||
{ subsystem: "wan", status: "unknown" },
|
||||
{ subsystem: "lan", status: "unknown" },
|
||||
{ subsystem: "wlan", status: "unknown" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "site-2",
|
||||
desc: "My Site",
|
||||
health: [
|
||||
{ subsystem: "wan", status: "ok", gw_name: "My GW", "gw_system-stats": { uptime: 86400 } },
|
||||
{ subsystem: "lan", status: "unknown" },
|
||||
{ subsystem: "wlan", status: "unknown" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<UnifiConsole options={{ index: 0, site: "My Site" }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("My GW")).toBeInTheDocument();
|
||||
expect(screen.getByText("1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows wlan user/device counts when wlan is available and lan is unknown", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
name: "default",
|
||||
desc: "Home",
|
||||
health: [
|
||||
{ subsystem: "wan", status: "unknown" },
|
||||
{ subsystem: "lan", status: "unknown" },
|
||||
{ subsystem: "wlan", status: "ok", num_user: 3, num_adopted: 10 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<UnifiConsole options={{ index: 0 }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("Home")).toBeInTheDocument();
|
||||
expect(screen.getByText("unifi.wlan")).toBeInTheDocument();
|
||||
expect(screen.getByText("3")).toBeInTheDocument();
|
||||
expect(screen.getByTitle("unifi.devices")).toBeInTheDocument();
|
||||
expect(screen.getByText("10")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders an empty data hint when all subsystems are unknown and uptime is missing", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
name: "default",
|
||||
desc: "Home",
|
||||
health: [
|
||||
{ subsystem: "wan", status: "unknown" },
|
||||
{ subsystem: "lan", status: "unknown" },
|
||||
{ subsystem: "wlan", status: "unknown" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<UnifiConsole options={{ index: 0 }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("unifi.empty_data")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows wan state when wan is available but reports a non-ok status", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
name: "default",
|
||||
desc: "Home",
|
||||
health: [
|
||||
{ subsystem: "wan", status: "error", gw_name: "Router" },
|
||||
{ subsystem: "lan", status: "unknown" },
|
||||
{ subsystem: "wlan", status: "unknown" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<UnifiConsole options={{ index: 0 }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("Router")).toBeInTheDocument();
|
||||
expect(screen.getByText("unifi.wan")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows wlan down state when only wlan is available", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
name: "default",
|
||||
desc: "Home",
|
||||
health: [
|
||||
{ subsystem: "wan", status: "unknown" },
|
||||
{ subsystem: "lan", status: "unknown" },
|
||||
{ subsystem: "wlan", status: "error", num_user: 1, num_adopted: 2 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<UnifiConsole options={{ index: 0 }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("unifi.wlan")).toBeInTheDocument();
|
||||
expect(screen.getByText("1")).toBeInTheDocument();
|
||||
expect(screen.getByText("2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows lan user/device counts when only lan is available", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
name: "default",
|
||||
desc: "Home",
|
||||
health: [
|
||||
{ subsystem: "wan", status: "unknown" },
|
||||
{ subsystem: "lan", status: "ok", num_user: 2, num_adopted: 5 },
|
||||
{ subsystem: "wlan", status: "unknown" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<UnifiConsole options={{ index: 0 }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("unifi.lan")).toBeInTheDocument();
|
||||
expect(screen.getByText("2")).toBeInTheDocument();
|
||||
expect(screen.getByText("5")).toBeInTheDocument();
|
||||
expect(screen.getByTitle("unifi.devices")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a lan down state when only lan is available and reports a non-ok status", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
name: "default",
|
||||
desc: "Home",
|
||||
health: [
|
||||
{ subsystem: "wan", status: "unknown" },
|
||||
{ subsystem: "lan", status: "error", num_user: 1, num_adopted: 2 },
|
||||
{ subsystem: "wlan", status: "unknown" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<UnifiConsole options={{ index: 0 }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("unifi.lan")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("bi-x-circle")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
146
src/components/widgets/weather/weather.test.jsx
Normal file
146
src/components/widgets/weather/weather.test.jsx
Normal file
@@ -0,0 +1,146 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, 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("react-icons/md", () => ({
|
||||
MdLocationDisabled: (props) => <svg data-testid="location-disabled" {...props} />,
|
||||
MdLocationSearching: (props) => <svg data-testid="location-searching" {...props} />,
|
||||
}));
|
||||
|
||||
import WeatherApi from "./weather";
|
||||
|
||||
describe("components/widgets/weather", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("renders an error state when SWR errors or the API payload indicates an error", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: new Error("nope") });
|
||||
renderWithProviders(<WeatherApi options={{ latitude: 1, longitude: 2 }} />, { settings: { target: "_self" } });
|
||||
expect(screen.getByText("widget.api_error")).toBeInTheDocument();
|
||||
|
||||
useSWR.mockReturnValue({ data: { error: "nope" }, error: undefined });
|
||||
renderWithProviders(<WeatherApi options={{ latitude: 1, longitude: 2 }} />, { settings: { target: "_self" } });
|
||||
expect(screen.getAllByText("widget.api_error").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders a location prompt when no coordinates are available", () => {
|
||||
renderWithProviders(<WeatherApi options={{}} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("weather.current")).toBeInTheDocument();
|
||||
expect(screen.getByText("weather.allow")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("auto-requests geolocation when permissions are granted", async () => {
|
||||
const getCurrentPosition = vi.fn((success) => success({ coords: { latitude: 30, longitude: 40 } }));
|
||||
const query = vi.fn().mockResolvedValue({ state: "granted" });
|
||||
vi.stubGlobal("navigator", {
|
||||
permissions: { query },
|
||||
geolocation: { getCurrentPosition },
|
||||
});
|
||||
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<WeatherApi options={{}} />, { settings: { target: "_self" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(query).toHaveBeenCalled();
|
||||
expect(getCurrentPosition).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("requests browser geolocation on click and then renders the updating state", async () => {
|
||||
const getCurrentPosition = vi.fn((success) => success({ coords: { latitude: 10, longitude: 20 } }));
|
||||
vi.stubGlobal("navigator", {
|
||||
permissions: { query: vi.fn().mockResolvedValue({ state: "prompt" }) },
|
||||
geolocation: { getCurrentPosition },
|
||||
});
|
||||
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<WeatherApi options={{}} />, { settings: { target: "_self" } });
|
||||
|
||||
screen.getByRole("button").click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getCurrentPosition).toHaveBeenCalled();
|
||||
});
|
||||
expect(screen.getByText("weather.updating")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clears the requesting state when the browser denies geolocation", async () => {
|
||||
const getCurrentPosition = vi.fn((_success, failure) => setTimeout(() => failure(), 10));
|
||||
vi.stubGlobal("navigator", {
|
||||
permissions: { query: vi.fn().mockResolvedValue({ state: "prompt" }) },
|
||||
geolocation: { getCurrentPosition },
|
||||
});
|
||||
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<WeatherApi options={{}} />, { settings: { target: "_self" } });
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
expect(screen.getByTestId("location-searching")).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("location-disabled")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders temperature and condition when coordinates are provided", async () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: {
|
||||
current: {
|
||||
temp_c: 21.5,
|
||||
temp_f: 70.7,
|
||||
is_day: 1,
|
||||
condition: { code: 1000, text: "Sunny" },
|
||||
},
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<WeatherApi options={{ latitude: 1, longitude: 2, units: "metric", label: "Home", format: {} }} />,
|
||||
{ settings: { target: "_self" } },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Home, 21.5")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("Sunny")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uses fahrenheit and night conditions when configured", async () => {
|
||||
useSWR.mockReturnValue({
|
||||
data: {
|
||||
current: {
|
||||
temp_c: 21.5,
|
||||
temp_f: 70.7,
|
||||
is_day: 0,
|
||||
condition: { code: 1000, text: "Clear" },
|
||||
},
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<WeatherApi options={{ latitude: 1, longitude: 2, units: "imperial", format: {} }} />, {
|
||||
settings: { target: "_self" },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("70.7")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("Clear")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
58
src/components/widgets/widget.test.jsx
Normal file
58
src/components/widgets/widget.test.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { dynamic } = vi.hoisted(() => {
|
||||
const dynamic = vi.fn((loader, opts) => {
|
||||
const loaderStr = loader.toString();
|
||||
const ssr = opts?.ssr === false ? "false" : "true";
|
||||
|
||||
return function DynamicWidget({ options }) {
|
||||
return (
|
||||
<div
|
||||
data-testid="dynamic-widget"
|
||||
data-loader={loaderStr}
|
||||
data-ssr={ssr}
|
||||
data-options={JSON.stringify(options)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
return { dynamic };
|
||||
});
|
||||
|
||||
vi.mock("next/dynamic", () => ({
|
||||
default: dynamic,
|
||||
}));
|
||||
|
||||
vi.mock("components/errorboundry", () => ({
|
||||
default: ({ children }) => <div data-testid="error-boundary">{children}</div>,
|
||||
}));
|
||||
|
||||
import Widget from "./widget";
|
||||
|
||||
describe("components/widgets/widget", () => {
|
||||
it("renders the mapped widget component and forwards style into options", () => {
|
||||
render(
|
||||
<Widget widget={{ type: "search", options: { provider: ["google"] } }} style={{ header: "boxedWidgets" }} />,
|
||||
);
|
||||
|
||||
const boundary = screen.getByTestId("error-boundary");
|
||||
expect(boundary).toBeInTheDocument();
|
||||
|
||||
const el = screen.getByTestId("dynamic-widget");
|
||||
expect(el.getAttribute("data-loader")).toContain("search/search");
|
||||
|
||||
const forwarded = JSON.parse(el.getAttribute("data-options"));
|
||||
expect(forwarded.provider).toEqual(["google"]);
|
||||
expect(forwarded.style).toEqual({ header: "boxedWidgets" });
|
||||
});
|
||||
|
||||
it("renders a missing message when widget type is unknown", () => {
|
||||
render(<Widget widget={{ type: "nope", options: {} }} style={{}} />);
|
||||
expect(screen.getByText("Missing")).toBeInTheDocument();
|
||||
expect(screen.getByText("nope")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
76
src/components/widgets/widget/container.test.jsx
Normal file
76
src/components/widgets/widget/container.test.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
|
||||
import Container, { getAllClasses } from "./container";
|
||||
import PrimaryText from "./primary_text";
|
||||
import Raw from "./raw";
|
||||
import SecondaryText from "./secondary_text";
|
||||
import WidgetIcon from "./widget_icon";
|
||||
|
||||
function FakeIcon(props) {
|
||||
return <svg data-testid="fake-icon" {...props} />;
|
||||
}
|
||||
|
||||
describe("components/widgets/widget/container", () => {
|
||||
it("getAllClasses supports boxedWidgets + cardBlur and right alignment", () => {
|
||||
const boxed = getAllClasses({ style: { header: "boxedWidgets", cardBlur: "md" } }, "x");
|
||||
expect(boxed).toContain("backdrop-blur-md");
|
||||
expect(boxed).toContain("x");
|
||||
|
||||
const right = getAllClasses({ style: { isRightAligned: true } }, "y");
|
||||
expect(right).toContain("justify-center");
|
||||
expect(right).toContain("y");
|
||||
expect(right).not.toContain("max-w:full");
|
||||
});
|
||||
|
||||
it("renders an anchor when href is provided and prefers options.target over settings.target", () => {
|
||||
renderWithProviders(
|
||||
<Container options={{ href: "http://example", target: "_self" }}>
|
||||
<WidgetIcon icon={FakeIcon} />
|
||||
<PrimaryText>P</PrimaryText>
|
||||
<SecondaryText>S</SecondaryText>
|
||||
<Raw>
|
||||
<div data-testid="bottom">B</div>
|
||||
</Raw>
|
||||
</Container>,
|
||||
{ settings: { target: "_blank" } },
|
||||
);
|
||||
|
||||
const link = screen.getByRole("link");
|
||||
expect(link.getAttribute("href")).toBe("http://example");
|
||||
expect(link.getAttribute("target")).toBe("_self");
|
||||
expect(screen.getByTestId("fake-icon")).toBeInTheDocument();
|
||||
expect(screen.getByText("P")).toBeInTheDocument();
|
||||
expect(screen.getByText("S")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("bottom")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders only bottom content when children are a single Raw element", () => {
|
||||
const { container } = renderWithProviders(
|
||||
<Container options={{}}>
|
||||
<Raw>
|
||||
<div data-testid="only-bottom">B</div>
|
||||
</Raw>
|
||||
</Container>,
|
||||
{ settings: { target: "_self" } },
|
||||
);
|
||||
|
||||
expect(container.querySelector(".widget-inner")).toBeNull();
|
||||
expect(screen.getByTestId("only-bottom")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not crash when clicked (href case is normal link)", () => {
|
||||
renderWithProviders(
|
||||
<Container options={{ href: "http://example" }}>
|
||||
<Raw>
|
||||
<div>Bottom</div>
|
||||
</Raw>
|
||||
</Container>,
|
||||
{ settings: { target: "_self" } },
|
||||
);
|
||||
});
|
||||
});
|
||||
23
src/components/widgets/widget/container_button.test.jsx
Normal file
23
src/components/widgets/widget/container_button.test.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import ContainerButton from "./container_button";
|
||||
import Raw from "./raw";
|
||||
|
||||
describe("components/widgets/widget/container_button", () => {
|
||||
it("invokes callback on click", () => {
|
||||
const cb = vi.fn();
|
||||
render(
|
||||
<ContainerButton options={{}} callback={cb}>
|
||||
<Raw>
|
||||
<div>child</div>
|
||||
</Raw>
|
||||
</ContainerButton>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
23
src/components/widgets/widget/container_form.test.jsx
Normal file
23
src/components/widgets/widget/container_form.test.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import ContainerForm from "./container_form";
|
||||
|
||||
describe("components/widgets/widget/container_form", () => {
|
||||
it("calls callback on submit", () => {
|
||||
const cb = vi.fn((e) => e.preventDefault());
|
||||
|
||||
const { container } = render(
|
||||
<ContainerForm options={{}} callback={cb}>
|
||||
{[<div key="c">child</div>]}
|
||||
</ContainerForm>,
|
||||
);
|
||||
|
||||
const form = container.querySelector("form");
|
||||
fireEvent.submit(form);
|
||||
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
24
src/components/widgets/widget/container_link.test.jsx
Normal file
24
src/components/widgets/widget/container_link.test.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import ContainerLink from "./container_link";
|
||||
import Raw from "./raw";
|
||||
|
||||
describe("components/widgets/widget/container_link", () => {
|
||||
it("renders an anchor using href or url", () => {
|
||||
const { rerender } = render(<ContainerLink options={{ href: "http://a" }} target="_self" />);
|
||||
expect(screen.getByRole("link").getAttribute("href")).toBe("http://a");
|
||||
expect(screen.getByRole("link").getAttribute("target")).toBe("_self");
|
||||
|
||||
rerender(
|
||||
<ContainerLink options={{ url: "http://b" }} target="_blank">
|
||||
<Raw>
|
||||
<div>child</div>
|
||||
</Raw>
|
||||
</ContainerLink>,
|
||||
);
|
||||
expect(screen.getByRole("link").getAttribute("href")).toBe("http://b");
|
||||
});
|
||||
});
|
||||
15
src/components/widgets/widget/error.test.jsx
Normal file
15
src/components/widgets/widget/error.test.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
|
||||
import Error from "./error";
|
||||
|
||||
describe("components/widgets/widget/error", () => {
|
||||
it("renders the api_error message", () => {
|
||||
renderWithProviders(<Error options={{}} />, { settings: { target: "_self" } });
|
||||
expect(screen.getByText("widget.api_error")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
13
src/components/widgets/widget/primary_text.test.jsx
Normal file
13
src/components/widgets/widget/primary_text.test.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import PrimaryText from "./primary_text";
|
||||
|
||||
describe("components/widgets/widget/primary_text", () => {
|
||||
it("renders children", () => {
|
||||
render(<PrimaryText>hello</PrimaryText>);
|
||||
expect(screen.getByText("hello")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
20
src/components/widgets/widget/raw.test.jsx
Normal file
20
src/components/widgets/widget/raw.test.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import Raw from "./raw";
|
||||
|
||||
describe("components/widgets/widget/raw", () => {
|
||||
it("renders nested Raw content", () => {
|
||||
render(
|
||||
<Raw>
|
||||
<Raw>
|
||||
<div>inner</div>
|
||||
</Raw>
|
||||
</Raw>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("inner")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
38
src/components/widgets/widget/resource.test.jsx
Normal file
38
src/components/widgets/widget/resource.test.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { UsageBar } = vi.hoisted(() => ({
|
||||
UsageBar: vi.fn(({ percent }) => <div data-testid="usagebar" data-percent={String(percent)} />),
|
||||
}));
|
||||
|
||||
vi.mock("../resources/usage-bar", () => ({
|
||||
default: UsageBar,
|
||||
}));
|
||||
|
||||
import Resource from "./resource";
|
||||
|
||||
function FakeIcon(props) {
|
||||
return <svg data-testid="resource-icon" {...props} />;
|
||||
}
|
||||
|
||||
describe("components/widgets/widget/resource", () => {
|
||||
it("renders icon/value/label and shows usage bar when percentage is set", () => {
|
||||
render(<Resource icon={FakeIcon} value="v" label="l" percentage={0} />);
|
||||
|
||||
expect(screen.getByTestId("resource-icon")).toBeInTheDocument();
|
||||
expect(screen.getByText("v")).toBeInTheDocument();
|
||||
expect(screen.getByText("l")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("usagebar").getAttribute("data-percent")).toBe("0");
|
||||
});
|
||||
|
||||
it("renders expanded values when expanded", () => {
|
||||
render(
|
||||
<Resource icon={FakeIcon} value="v" label="l" expanded expandedValue="ev" expandedLabel="el" percentage={10} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText("ev")).toBeInTheDocument();
|
||||
expect(screen.getByText("el")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
31
src/components/widgets/widget/resources.test.jsx
Normal file
31
src/components/widgets/widget/resources.test.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import Resource from "./resource";
|
||||
import Resources from "./resources";
|
||||
import WidgetLabel from "./widget_label";
|
||||
|
||||
function FakeIcon() {
|
||||
return <svg />;
|
||||
}
|
||||
|
||||
describe("components/widgets/widget/resources", () => {
|
||||
it("filters children to Resource + WidgetLabel and wraps them in a link", () => {
|
||||
render(
|
||||
<Resources options={{ href: "http://example" }} target="_self" additionalClassNames="x">
|
||||
{[
|
||||
<Resource key="r" icon={FakeIcon} value="v" label="l" />,
|
||||
<WidgetLabel key="w" label="Label" />,
|
||||
<div key="o">Other</div>,
|
||||
]}
|
||||
</Resources>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("link").getAttribute("href")).toBe("http://example");
|
||||
expect(screen.getByText("v")).toBeInTheDocument();
|
||||
expect(screen.getByText("Label")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Other")).toBeNull();
|
||||
});
|
||||
});
|
||||
13
src/components/widgets/widget/secondary_text.test.jsx
Normal file
13
src/components/widgets/widget/secondary_text.test.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import SecondaryText from "./secondary_text";
|
||||
|
||||
describe("components/widgets/widget/secondary_text", () => {
|
||||
it("renders children", () => {
|
||||
render(<SecondaryText>world</SecondaryText>);
|
||||
expect(screen.getByText("world")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
30
src/components/widgets/widget/widget_icon.test.jsx
Normal file
30
src/components/widgets/widget/widget_icon.test.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import WidgetIcon from "./widget_icon";
|
||||
|
||||
function FakeIcon(props) {
|
||||
return <svg data-testid="icon" {...props} />;
|
||||
}
|
||||
|
||||
describe("components/widgets/widget/widget_icon", () => {
|
||||
it("applies size classes and pulse animation", () => {
|
||||
render(
|
||||
<>
|
||||
<WidgetIcon icon={FakeIcon} size="s" />
|
||||
<WidgetIcon icon={FakeIcon} size="m" />
|
||||
<WidgetIcon icon={FakeIcon} size="l" pulse />
|
||||
<WidgetIcon icon={FakeIcon} size="xl" />
|
||||
</>,
|
||||
);
|
||||
|
||||
const icons = screen.getAllByTestId("icon");
|
||||
expect(icons[0].getAttribute("class")).toContain("w-5 h-5");
|
||||
expect(icons[1].getAttribute("class")).toContain("w-6 h-6");
|
||||
expect(icons[2].getAttribute("class")).toContain("w-8 h-8");
|
||||
expect(icons[2].getAttribute("class")).toContain("animate-pulse");
|
||||
expect(icons[3].getAttribute("class")).toContain("w-10 h-10");
|
||||
});
|
||||
});
|
||||
13
src/components/widgets/widget/widget_label.test.jsx
Normal file
13
src/components/widgets/widget/widget_label.test.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import WidgetLabel from "./widget_label";
|
||||
|
||||
describe("components/widgets/widget/widget_label", () => {
|
||||
it("renders label text", () => {
|
||||
render(<WidgetLabel label="Label A" />);
|
||||
expect(screen.getByText("Label A")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user