diff --git a/src/components/widgets/kubernetes/node.test.jsx b/src/components/widgets/kubernetes/node.test.jsx
new file mode 100644
index 000000000..63c1c418e
--- /dev/null
+++ b/src/components/widgets/kubernetes/node.test.jsx
@@ -0,0 +1,27 @@
+// @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();
+
+ 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();
+
+ expect(screen.getByText("node-1")).toBeInTheDocument();
+ });
+});
diff --git a/src/components/widgets/longhorn/node.test.jsx b/src/components/widgets/longhorn/node.test.jsx
new file mode 100644
index 000000000..06033d27c
--- /dev/null
+++ b/src/components/widgets/longhorn/node.test.jsx
@@ -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 }) =>
{children}
),
+}));
+
+vi.mock("../widget/resource", () => ({
+ default: Resource,
+}));
+
+vi.mock("../widget/widget_label", () => ({
+ default: ({ label }) => {label}
,
+}));
+
+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();
+
+ 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");
+ });
+});
diff --git a/src/components/widgets/queue/queueEntry.test.jsx b/src/components/widgets/queue/queueEntry.test.jsx
new file mode 100644
index 000000000..0cd2592a6
--- /dev/null
+++ b/src/components/widgets/queue/queueEntry.test.jsx
@@ -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(
+ ,
+ );
+
+ expect(screen.getByText("Download")).toBeInTheDocument();
+ expect(screen.getByText("1GB - Downloading - 1m")).toBeInTheDocument();
+
+ const bar = container.querySelector("div[style]");
+ expect(bar.style.width).toBe("42%");
+ });
+});
diff --git a/src/components/widgets/resources/usage-bar.test.jsx b/src/components/widgets/resources/usage-bar.test.jsx
new file mode 100644
index 000000000..09f28bf26
--- /dev/null
+++ b/src/components/widgets/resources/usage-bar.test.jsx
@@ -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();
+ const inner0 = c0.querySelector("div > div > div");
+ expect(inner0.style.width).toBe("0%");
+
+ const { container: c1 } = render();
+ const inner1 = c1.querySelector("div > div > div");
+ expect(inner1.style.width).toBe("100%");
+ });
+});
diff --git a/src/components/widgets/widget.test.jsx b/src/components/widgets/widget.test.jsx
new file mode 100644
index 000000000..9be466598
--- /dev/null
+++ b/src/components/widgets/widget.test.jsx
@@ -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 (
+
+ );
+ };
+ });
+
+ return { dynamic };
+});
+
+vi.mock("next/dynamic", () => ({
+ default: dynamic,
+}));
+
+vi.mock("components/errorboundry", () => ({
+ default: ({ children }) => {children}
,
+}));
+
+import Widget from "./widget";
+
+describe("components/widgets/widget", () => {
+ it("renders the mapped widget component and forwards style into options", () => {
+ render(
+ ,
+ );
+
+ 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();
+ expect(screen.getByText("Missing")).toBeInTheDocument();
+ expect(screen.getByText("nope")).toBeInTheDocument();
+ });
+});
diff --git a/src/components/widgets/widget/container.test.jsx b/src/components/widgets/widget/container.test.jsx
new file mode 100644
index 000000000..db74bdf5d
--- /dev/null
+++ b/src/components/widgets/widget/container.test.jsx
@@ -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 ;
+}
+
+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(
+
+
+ P
+ S
+
+ B
+
+ ,
+ { 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(
+
+
+ B
+
+ ,
+ { 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(
+
+
+ Bottom
+
+ ,
+ { settings: { target: "_self" } },
+ );
+ });
+});
diff --git a/src/components/widgets/widget/container_button.test.jsx b/src/components/widgets/widget/container_button.test.jsx
new file mode 100644
index 000000000..a24d07064
--- /dev/null
+++ b/src/components/widgets/widget/container_button.test.jsx
@@ -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(
+
+
+ child
+
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button"));
+ expect(cb).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/components/widgets/widget/container_form.test.jsx b/src/components/widgets/widget/container_form.test.jsx
new file mode 100644
index 000000000..56b9d9a9a
--- /dev/null
+++ b/src/components/widgets/widget/container_form.test.jsx
@@ -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(
+
+ {[child
]}
+ ,
+ );
+
+ const form = container.querySelector("form");
+ fireEvent.submit(form);
+
+ expect(cb).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/components/widgets/widget/container_link.test.jsx b/src/components/widgets/widget/container_link.test.jsx
new file mode 100644
index 000000000..5c4f15b34
--- /dev/null
+++ b/src/components/widgets/widget/container_link.test.jsx
@@ -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();
+ expect(screen.getByRole("link").getAttribute("href")).toBe("http://a");
+ expect(screen.getByRole("link").getAttribute("target")).toBe("_self");
+
+ rerender(
+
+
+ child
+
+ ,
+ );
+ expect(screen.getByRole("link").getAttribute("href")).toBe("http://b");
+ });
+});
diff --git a/src/components/widgets/widget/error.test.jsx b/src/components/widgets/widget/error.test.jsx
new file mode 100644
index 000000000..b82c587ae
--- /dev/null
+++ b/src/components/widgets/widget/error.test.jsx
@@ -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(, { settings: { target: "_self" } });
+ expect(screen.getByText("widget.api_error")).toBeInTheDocument();
+ });
+});
diff --git a/src/components/widgets/widget/primary_text.test.jsx b/src/components/widgets/widget/primary_text.test.jsx
new file mode 100644
index 000000000..962e2bf4b
--- /dev/null
+++ b/src/components/widgets/widget/primary_text.test.jsx
@@ -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(hello);
+ expect(screen.getByText("hello")).toBeInTheDocument();
+ });
+});
diff --git a/src/components/widgets/widget/raw.test.jsx b/src/components/widgets/widget/raw.test.jsx
new file mode 100644
index 000000000..f5f175ee1
--- /dev/null
+++ b/src/components/widgets/widget/raw.test.jsx
@@ -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(
+
+
+ inner
+
+ ,
+ );
+
+ expect(screen.getByText("inner")).toBeInTheDocument();
+ });
+});
diff --git a/src/components/widgets/widget/resource.test.jsx b/src/components/widgets/widget/resource.test.jsx
new file mode 100644
index 000000000..4afe066c5
--- /dev/null
+++ b/src/components/widgets/widget/resource.test.jsx
@@ -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 }) => ),
+}));
+
+vi.mock("../resources/usage-bar", () => ({
+ default: UsageBar,
+}));
+
+import Resource from "./resource";
+
+function FakeIcon(props) {
+ return ;
+}
+
+describe("components/widgets/widget/resource", () => {
+ it("renders icon/value/label and shows usage bar when percentage is set", () => {
+ render();
+
+ 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(
+ ,
+ );
+
+ expect(screen.getByText("ev")).toBeInTheDocument();
+ expect(screen.getByText("el")).toBeInTheDocument();
+ });
+});
diff --git a/src/components/widgets/widget/resources.test.jsx b/src/components/widgets/widget/resources.test.jsx
new file mode 100644
index 000000000..4b71f1598
--- /dev/null
+++ b/src/components/widgets/widget/resources.test.jsx
@@ -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 ;
+}
+
+describe("components/widgets/widget/resources", () => {
+ it("filters children to Resource + WidgetLabel and wraps them in a link", () => {
+ render(
+
+ {[
+ ,
+ ,
+ Other
,
+ ]}
+ ,
+ );
+
+ expect(screen.getByRole("link").getAttribute("href")).toBe("http://example");
+ expect(screen.getByText("v")).toBeInTheDocument();
+ expect(screen.getByText("Label")).toBeInTheDocument();
+ expect(screen.queryByText("Other")).toBeNull();
+ });
+});
diff --git a/src/components/widgets/widget/secondary_text.test.jsx b/src/components/widgets/widget/secondary_text.test.jsx
new file mode 100644
index 000000000..4e80962d8
--- /dev/null
+++ b/src/components/widgets/widget/secondary_text.test.jsx
@@ -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(world);
+ expect(screen.getByText("world")).toBeInTheDocument();
+ });
+});
diff --git a/src/components/widgets/widget/widget_icon.test.jsx b/src/components/widgets/widget/widget_icon.test.jsx
new file mode 100644
index 000000000..cd073ee47
--- /dev/null
+++ b/src/components/widgets/widget/widget_icon.test.jsx
@@ -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 ;
+}
+
+describe("components/widgets/widget/widget_icon", () => {
+ it("applies size classes and pulse animation", () => {
+ render(
+ <>
+
+
+
+
+ >,
+ );
+
+ 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");
+ });
+});
diff --git a/src/components/widgets/widget/widget_label.test.jsx b/src/components/widgets/widget/widget_label.test.jsx
new file mode 100644
index 000000000..920bd722b
--- /dev/null
+++ b/src/components/widgets/widget/widget_label.test.jsx
@@ -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();
+ expect(screen.getByText("Label A")).toBeInTheDocument();
+ });
+});