diff --git a/src/widgets/calendar/agenda.test.jsx b/src/widgets/calendar/agenda.test.jsx
new file mode 100644
index 000000000..c69bb6f1c
--- /dev/null
+++ b/src/widgets/calendar/agenda.test.jsx
@@ -0,0 +1,64 @@
+// @vitest-environment jsdom
+
+import { render, screen } from "@testing-library/react";
+import { DateTime } from "luxon";
+import { describe, expect, it, vi } from "vitest";
+
+const { EventStub, compareDateTimezoneStub } = vi.hoisted(() => ({
+ EventStub: vi.fn(({ event, showDate, showTime }) => (
+
+ {event.title}
+
+ )),
+ compareDateTimezoneStub: vi.fn(
+ (date, event) => date.startOf("day").toISODate() === event.date.startOf("day").toISODate(),
+ ),
+}));
+
+vi.mock("./event", () => ({
+ default: EventStub,
+ compareDateTimezone: compareDateTimezoneStub,
+}));
+
+import Agenda from "./agenda";
+
+describe("widgets/calendar/agenda", () => {
+ it("renders an empty placeholder when showDate is not set", () => {
+ const { container } = render();
+ expect(container.textContent).toBe("");
+ });
+
+ it("renders a no-events placeholder when there are no events in range", () => {
+ render();
+ expect(screen.getByText("calendar.noEventsToday")).toBeInTheDocument();
+ expect(EventStub).toHaveBeenCalled();
+ });
+
+ it("filters by previousDays, sorts, and enforces maxEvents", () => {
+ const showDate = DateTime.local(2099, 1, 2).startOf("day");
+ const service = { widget: { previousDays: 0, maxEvents: 2, showTime: true } };
+
+ const events = {
+ old: { title: "Old", date: DateTime.local(2099, 1, 1, 0, 0), color: "gray" },
+ a: { title: "A", date: DateTime.local(2099, 1, 2, 10, 0), color: "gray" },
+ b: { title: "B", date: DateTime.local(2099, 1, 3, 10, 0), color: "gray" },
+ c: { title: "C", date: DateTime.local(2099, 1, 4, 10, 0), color: "gray" },
+ };
+
+ render();
+
+ // Old is filtered out, C is sliced out by maxEvents.
+ expect(screen.queryByText("Old")).toBeNull();
+ expect(screen.getByText("A")).toBeInTheDocument();
+ expect(screen.getByText("B")).toBeInTheDocument();
+ expect(screen.queryByText("C")).toBeNull();
+
+ const renderedEvents = screen.getAllByTestId("event");
+ expect(renderedEvents).toHaveLength(2);
+
+ // showTime is only true for the selected day.
+ const [first, second] = renderedEvents;
+ expect(first).toHaveAttribute("data-showtime", "1");
+ expect(second).toHaveAttribute("data-showtime", "0");
+ });
+});
diff --git a/src/widgets/calendar/event.test.jsx b/src/widgets/calendar/event.test.jsx
new file mode 100644
index 000000000..56b37b159
--- /dev/null
+++ b/src/widgets/calendar/event.test.jsx
@@ -0,0 +1,56 @@
+// @vitest-environment jsdom
+
+import { fireEvent, render, screen } from "@testing-library/react";
+import { DateTime } from "luxon";
+import { describe, expect, it } from "vitest";
+
+import Event, { compareDateTimezone } from "./event";
+
+describe("widgets/calendar/event", () => {
+ it("renders an anchor when a url is provided and toggles additional text on hover", () => {
+ const date = DateTime.fromISO("2099-01-01T13:00:00.000Z").setZone("utc");
+
+ render(
+ ,
+ );
+
+ const link = screen.getByRole("link", { name: /primary/i });
+ expect(link).toHaveAttribute("href", "https://example.com");
+ expect(link).toHaveAttribute("target", "_blank");
+ expect(link).toHaveAttribute("rel", "noopener noreferrer");
+
+ // time is rendered when showTime=true
+ expect(link.textContent).toContain("13:00");
+
+ // default shows title, hover shows `additional`
+ expect(screen.getByText("Primary")).toBeInTheDocument();
+ expect(screen.queryByText("More info")).toBeNull();
+
+ fireEvent.mouseEnter(link);
+ expect(screen.getByText("More info")).toBeInTheDocument();
+
+ fireEvent.mouseLeave(link);
+ expect(screen.getByText("Primary")).toBeInTheDocument();
+
+ // completed icon from react-icons renders an SVG
+ expect(link.querySelector("svg")).toBeTruthy();
+ });
+
+ it("compareDateTimezone matches dates by day", () => {
+ const day = DateTime.fromISO("2099-01-01T00:00:00.000Z").setZone("utc");
+ expect(compareDateTimezone(day, { date: DateTime.fromISO("2099-01-01T23:59:00.000Z").setZone("utc") })).toBe(true);
+ expect(compareDateTimezone(day, { date: DateTime.fromISO("2099-01-02T00:00:00.000Z").setZone("utc") })).toBe(false);
+ });
+});
diff --git a/src/widgets/calendar/integrations/ical.test.jsx b/src/widgets/calendar/integrations/ical.test.jsx
new file mode 100644
index 000000000..cd1764c37
--- /dev/null
+++ b/src/widgets/calendar/integrations/ical.test.jsx
@@ -0,0 +1,64 @@
+// @vitest-environment jsdom
+
+import { render, waitFor } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Integration from "./ical";
+
+describe("widgets/calendar/integrations/ical", () => {
+ it("adds parsed events within the date range", async () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ data: [
+ "BEGIN:VCALENDAR",
+ "VERSION:2.0",
+ "PRODID:-//Test//EN",
+ "BEGIN:VEVENT",
+ "UID:uid1",
+ "DTSTAMP:20990101T000000Z",
+ "DTSTART:20990101T130000Z",
+ "DTEND:20990101T140000Z",
+ "SUMMARY:Test Event",
+ "LOCATION:Office",
+ "URL:https://example.com",
+ "END:VEVENT",
+ "END:VCALENDAR",
+ "",
+ ].join("\n"),
+ },
+ error: undefined,
+ });
+
+ const setEvents = vi.fn();
+ render(
+ ,
+ );
+
+ await waitFor(() => expect(setEvents).toHaveBeenCalled());
+
+ const updater = setEvents.mock.calls[0][0];
+ const next = updater({});
+ const entries = Object.values(next);
+ expect(entries).toHaveLength(1);
+
+ const [event] = entries;
+ expect(event.title).toBe("Work: Test Event");
+ expect(event.color).toBe("blue");
+ expect(event.type).toBe("ical");
+ expect(event.additional).toBe("Office");
+ expect(event.url).toBe("https://example.com");
+ expect(event.isCompleted).toBe(false);
+ });
+});
diff --git a/src/widgets/calendar/integrations/lidarr.test.jsx b/src/widgets/calendar/integrations/lidarr.test.jsx
new file mode 100644
index 000000000..88ce51c51
--- /dev/null
+++ b/src/widgets/calendar/integrations/lidarr.test.jsx
@@ -0,0 +1,39 @@
+// @vitest-environment jsdom
+
+import { render, waitFor } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Integration from "./lidarr";
+
+describe("widgets/calendar/integrations/lidarr", () => {
+ it("adds release events", async () => {
+ useWidgetAPI.mockReturnValue({
+ data: [
+ { artist: { artistName: "Artist" }, title: "Album", releaseDate: "2099-01-01T00:00:00.000Z", grabbed: true },
+ ],
+ error: undefined,
+ });
+
+ const setEvents = vi.fn();
+ render(
+ ,
+ );
+
+ await waitFor(() => expect(setEvents).toHaveBeenCalled());
+
+ const next = setEvents.mock.calls[0][0]({});
+ expect(Object.keys(next)).toEqual(["Artist - Album"]);
+ expect(next["Artist - Album"].isCompleted).toBe(true);
+ });
+});
diff --git a/src/widgets/calendar/integrations/radarr.test.jsx b/src/widgets/calendar/integrations/radarr.test.jsx
new file mode 100644
index 000000000..b5c4568b9
--- /dev/null
+++ b/src/widgets/calendar/integrations/radarr.test.jsx
@@ -0,0 +1,49 @@
+// @vitest-environment jsdom
+
+import { render, waitFor } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Integration from "./radarr";
+
+describe("widgets/calendar/integrations/radarr", () => {
+ it("adds cinema/physical/digital events", async () => {
+ useWidgetAPI.mockReturnValue({
+ data: [
+ {
+ title: "Movie",
+ titleSlug: "movie",
+ hasFile: false,
+ inCinemas: "2099-01-01T00:00:00.000Z",
+ physicalRelease: "2099-01-02T00:00:00.000Z",
+ digitalRelease: "2099-01-03T00:00:00.000Z",
+ },
+ ],
+ error: undefined,
+ });
+
+ const setEvents = vi.fn();
+ render(
+ ,
+ );
+
+ await waitFor(() => expect(setEvents).toHaveBeenCalled());
+
+ const next = setEvents.mock.calls[0][0]({});
+ const keys = Object.keys(next);
+ expect(keys.some((k) => k.includes("calendar.inCinemas"))).toBe(true);
+ expect(keys.some((k) => k.includes("calendar.physicalRelease"))).toBe(true);
+ expect(keys.some((k) => k.includes("calendar.digitalRelease"))).toBe(true);
+ expect(Object.values(next)[0].url).toBe("https://radarr.example/movie/movie");
+ });
+});
diff --git a/src/widgets/calendar/integrations/readarr.test.jsx b/src/widgets/calendar/integrations/readarr.test.jsx
new file mode 100644
index 000000000..f3955a833
--- /dev/null
+++ b/src/widgets/calendar/integrations/readarr.test.jsx
@@ -0,0 +1,48 @@
+// @vitest-environment jsdom
+
+import { render, waitFor } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Integration from "./readarr";
+
+describe("widgets/calendar/integrations/readarr", () => {
+ it("adds release events with author name", async () => {
+ useWidgetAPI.mockReturnValue({
+ data: [
+ {
+ title: "Book",
+ seriesTitle: "Series",
+ releaseDate: "2099-01-01T00:00:00.000Z",
+ grabbed: false,
+ author: { authorName: "Author" },
+ authorTitle: "Author Book",
+ },
+ ],
+ error: undefined,
+ });
+
+ const setEvents = vi.fn();
+ render(
+ ,
+ );
+
+ await waitFor(() => expect(setEvents).toHaveBeenCalled());
+
+ const next = setEvents.mock.calls[0][0]({});
+ const [key] = Object.keys(next);
+ expect(key).toContain("Author");
+ expect(key).toContain("Book");
+ expect(key).toContain("(Series)");
+ });
+});
diff --git a/src/widgets/calendar/integrations/sonarr.test.jsx b/src/widgets/calendar/integrations/sonarr.test.jsx
new file mode 100644
index 000000000..748e35e18
--- /dev/null
+++ b/src/widgets/calendar/integrations/sonarr.test.jsx
@@ -0,0 +1,48 @@
+// @vitest-environment jsdom
+
+import { render, waitFor } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Integration from "./sonarr";
+
+describe("widgets/calendar/integrations/sonarr", () => {
+ it("adds episode events", async () => {
+ useWidgetAPI.mockReturnValue({
+ data: [
+ {
+ series: { title: "Show", titleSlug: "show" },
+ seasonNumber: 1,
+ episodeNumber: 2,
+ airDateUtc: "2099-01-01T00:00:00.000Z",
+ hasFile: true,
+ },
+ ],
+ error: undefined,
+ });
+
+ const setEvents = vi.fn();
+ render(
+ ,
+ );
+
+ await waitFor(() => expect(setEvents).toHaveBeenCalled());
+
+ const next = setEvents.mock.calls[0][0]({});
+ const [entry] = Object.values(next);
+ expect(entry.title).toBe("Show");
+ expect(entry.additional).toBe("S1 E2");
+ expect(entry.url).toBe("https://sonarr.example/series/show");
+ expect(entry.isCompleted).toBe(true);
+ });
+});
diff --git a/src/widgets/calendar/monthly.test.jsx b/src/widgets/calendar/monthly.test.jsx
new file mode 100644
index 000000000..f9dbc5eee
--- /dev/null
+++ b/src/widgets/calendar/monthly.test.jsx
@@ -0,0 +1,71 @@
+// @vitest-environment jsdom
+
+import { fireEvent, render, screen } from "@testing-library/react";
+import { DateTime } from "luxon";
+import { describe, expect, it, vi } from "vitest";
+
+const { EventStub, compareDateTimezoneStub } = vi.hoisted(() => ({
+ EventStub: vi.fn(({ event }) => {event.title}
),
+ compareDateTimezoneStub: vi.fn(
+ (date, event) => date.startOf("day").toISODate() === event.date.startOf("day").toISODate(),
+ ),
+}));
+
+vi.mock("./event", () => ({
+ default: EventStub,
+ compareDateTimezone: compareDateTimezoneStub,
+}));
+
+import Monthly from "./monthly";
+
+describe("widgets/calendar/monthly", () => {
+ it("renders an empty placeholder when showDate is not set", () => {
+ const { container } = render(
+ {}}
+ currentDate={DateTime.now()}
+ />,
+ );
+ expect(container.textContent).toBe("");
+ });
+
+ it("navigates months and renders day events", () => {
+ const setShowDate = vi.fn();
+ const showDate = DateTime.local(2099, 2, 15).startOf("day");
+ const currentDate = DateTime.local(2099, 2, 4).startOf("day");
+ const service = { widget: { maxEvents: 10, showTime: false } };
+
+ const events = {
+ e1: { title: "Today Event", date: DateTime.local(2099, 2, 15, 10, 0), color: "zinc" },
+ e2: { title: "Other Event", date: DateTime.local(2099, 2, 16, 10, 0), color: "zinc" },
+ };
+
+ render(
+ ,
+ );
+
+ expect(screen.getByText("Today Event")).toBeInTheDocument();
+ expect(screen.queryByText("Other Event")).toBeNull();
+
+ fireEvent.click(screen.getByRole("button", { name: ">" }));
+ expect(setShowDate).toHaveBeenCalled();
+ expect(setShowDate.mock.calls[0][0].toISODate()).toBe(showDate.plus({ months: 1 }).startOf("day").toISODate());
+
+ fireEvent.click(screen.getByRole("button", { name: "<" }));
+ expect(setShowDate.mock.calls[1][0].toISODate()).toBe(showDate.minus({ months: 1 }).startOf("day").toISODate());
+
+ fireEvent.click(screen.getByRole("button", { name: showDate.toFormat("MMMM y") }));
+ expect(setShowDate.mock.calls[2][0].toISODate()).toBe(currentDate.startOf("day").toISODate());
+ });
+});
diff --git a/src/widgets/jdownloader/tools.test.js b/src/widgets/jdownloader/tools.test.js
new file mode 100644
index 000000000..0b44f871d
--- /dev/null
+++ b/src/widgets/jdownloader/tools.test.js
@@ -0,0 +1,39 @@
+import crypto from "crypto";
+import { describe, expect, it, vi } from "vitest";
+
+import { createEncryptionToken, decrypt, encrypt, sha256, uniqueRid, validateRid } from "./tools";
+
+describe("widgets/jdownloader/tools", () => {
+ it("sha256 returns a 32-byte buffer", () => {
+ expect(sha256("hello")).toBeInstanceOf(Buffer);
+ expect(sha256("hello")).toHaveLength(32);
+ });
+
+ it("uniqueRid returns an integer", () => {
+ vi.spyOn(Math, "random").mockReturnValueOnce(0.123);
+ expect(uniqueRid()).toBeTypeOf("number");
+ expect(Number.isInteger(uniqueRid())).toBe(true);
+ Math.random.mockRestore();
+ });
+
+ it("validateRid throws when mismatched", () => {
+ expect(() => validateRid({ rid: 1 }, 2)).toThrow(/RequestID mismatch/i);
+ expect(validateRid({ rid: 5 }, 5)).toEqual({ rid: 5 });
+ });
+
+ it("encrypt/decrypt roundtrip with a 32-byte ivKey", () => {
+ const ivKey = crypto.randomBytes(32);
+ const plaintext = "secret";
+ const encrypted = encrypt(plaintext, ivKey);
+ const decrypted = decrypt(encrypted, ivKey);
+ expect(decrypted).toBe(plaintext);
+ });
+
+ it("createEncryptionToken merges buffers and hashes", () => {
+ const oldToken = Buffer.from("aa", "hex");
+ const updateToken = "bb";
+ const token = createEncryptionToken(oldToken, updateToken);
+ expect(token).toBeInstanceOf(Buffer);
+ expect(token).toHaveLength(32);
+ });
+});
diff --git a/src/widgets/openmediavault/methods/downloader_get_downloadlist.test.jsx b/src/widgets/openmediavault/methods/downloader_get_downloadlist.test.jsx
new file mode 100644
index 000000000..7875caa20
--- /dev/null
+++ b/src/widgets/openmediavault/methods/downloader_get_downloadlist.test.jsx
@@ -0,0 +1,42 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./downloader_get_downloadlist";
+
+describe("widgets/openmediavault/methods/downloader_get_downloadlist", () => {
+ it("renders '-' values when data is missing", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(screen.getByText("openmediavault.downloading")).toBeInTheDocument();
+ expect(screen.getByText("openmediavault.total")).toBeInTheDocument();
+ expect(screen.getAllByText("-").length).toBeGreaterThan(0);
+ });
+
+ it("counts downloading and total items", () => {
+ useWidgetAPI.mockReturnValue({
+ data: { response: { data: [{ downloading: true }, { downloading: false }, { downloading: true }] } },
+ error: undefined,
+ });
+
+ renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(screen.getByText("2")).toBeInTheDocument();
+ expect(screen.getByText("3")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/openmediavault/methods/services_get_status.test.jsx b/src/widgets/openmediavault/methods/services_get_status.test.jsx
new file mode 100644
index 000000000..d77d02cbb
--- /dev/null
+++ b/src/widgets/openmediavault/methods/services_get_status.test.jsx
@@ -0,0 +1,35 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./services_get_status";
+
+describe("widgets/openmediavault/methods/services_get_status", () => {
+ it("counts running/stopped/total services", () => {
+ useWidgetAPI.mockReturnValue({
+ data: { response: { data: [{ running: true }, { running: false }, { running: true }] } },
+ error: undefined,
+ });
+
+ renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(screen.getByText("openmediavault.running")).toBeInTheDocument();
+ expect(screen.getByText("openmediavault.stopped")).toBeInTheDocument();
+ expect(screen.getByText("openmediavault.total")).toBeInTheDocument();
+
+ expect(screen.getByText("2")).toBeInTheDocument();
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.getByText("3")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/openmediavault/methods/smart_get_list.test.jsx b/src/widgets/openmediavault/methods/smart_get_list.test.jsx
new file mode 100644
index 000000000..5ba7546c6
--- /dev/null
+++ b/src/widgets/openmediavault/methods/smart_get_list.test.jsx
@@ -0,0 +1,41 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./smart_get_list";
+
+describe("widgets/openmediavault/methods/smart_get_list", () => {
+ it("counts passed/failed monitored disks", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ response: {
+ output: JSON.stringify({
+ data: [
+ { monitor: true, overallstatus: "GOOD" },
+ { monitor: true, overallstatus: "BAD" },
+ { monitor: false, overallstatus: "BAD" },
+ ],
+ }),
+ },
+ },
+ error: undefined,
+ });
+
+ renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(screen.getByText("openmediavault.passed")).toBeInTheDocument();
+ expect(screen.getByText("openmediavault.failed")).toBeInTheDocument();
+ expect(screen.getAllByText("1")).toHaveLength(2);
+ });
+});
diff --git a/src/widgets/openwrt/methods/interface.test.jsx b/src/widgets/openwrt/methods/interface.test.jsx
new file mode 100644
index 000000000..80d87ec9c
--- /dev/null
+++ b/src/widgets/openwrt/methods/interface.test.jsx
@@ -0,0 +1,37 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./interface";
+
+describe("widgets/openwrt/methods/interface", () => {
+ it("returns null while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+ const { container } = renderWithProviders();
+ expect(container.firstChild).toBeNull();
+ });
+
+ it("renders status and byte counters when loaded", () => {
+ useWidgetAPI.mockReturnValue({ data: { up: true, bytesTx: 100, bytesRx: 200 }, error: undefined });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getByText("widget.status")).toBeInTheDocument();
+ expect(screen.getByText("openwrt.bytesTx")).toBeInTheDocument();
+ expect(screen.getByText("openwrt.bytesRx")).toBeInTheDocument();
+
+ // t("common.bytes") mock returns the numeric value as a string.
+ expect(screen.getByText("100")).toBeInTheDocument();
+ expect(screen.getByText("200")).toBeInTheDocument();
+ expect(screen.getByText("openwrt.up")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/openwrt/methods/system.test.jsx b/src/widgets/openwrt/methods/system.test.jsx
new file mode 100644
index 000000000..122665243
--- /dev/null
+++ b/src/widgets/openwrt/methods/system.test.jsx
@@ -0,0 +1,33 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./system";
+
+describe("widgets/openwrt/methods/system", () => {
+ it("returns null while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+ const { container } = renderWithProviders();
+ expect(container.firstChild).toBeNull();
+ });
+
+ it("renders uptime and cpu load when loaded", () => {
+ useWidgetAPI.mockReturnValue({ data: { uptime: 123, cpuLoad: "0.5" }, error: undefined });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getByText("openwrt.uptime")).toBeInTheDocument();
+ expect(screen.getByText("openwrt.cpuLoad")).toBeInTheDocument();
+ expect(screen.getByText("123")).toBeInTheDocument();
+ expect(screen.getByText("0.5")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/truenas/pool.test.jsx b/src/widgets/truenas/pool.test.jsx
new file mode 100644
index 000000000..2ae31b323
--- /dev/null
+++ b/src/widgets/truenas/pool.test.jsx
@@ -0,0 +1,19 @@
+// @vitest-environment jsdom
+
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import Pool from "./pool";
+
+describe("widgets/truenas/pool", () => {
+ it("renders pool name, usage percent, and status color", () => {
+ const { container } = render();
+ expect(screen.getByText("tank")).toBeInTheDocument();
+
+ // 50 / 100 => 50%
+ expect(container.textContent).toContain("(50%)");
+
+ // status color reflects healthy=false
+ expect(container.querySelector(".bg-yellow-500")).toBeTruthy();
+ });
+});