diff --git a/src/widgets/booklore/component.test.jsx b/src/widgets/booklore/component.test.jsx
new file mode 100644
index 000000000..efa001f9e
--- /dev/null
+++ b/src/widgets/booklore/component.test.jsx
@@ -0,0 +1,50 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({
+ default: useWidgetAPI,
+}));
+
+import Component from "./component";
+
+describe("widgets/booklore/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders placeholders while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("booklore.libraries")).toBeInTheDocument();
+ expect(screen.getByText("booklore.books")).toBeInTheDocument();
+ expect(screen.getByText("booklore.reading")).toBeInTheDocument();
+ expect(screen.getByText("booklore.finished")).toBeInTheDocument();
+ });
+
+ it("renders values with nullish fallback defaults", () => {
+ useWidgetAPI.mockReturnValue({
+ data: { libraries: 1, books: 2, finished: 4 }, // reading missing -> 0
+ error: undefined,
+ });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.getByText("2")).toBeInTheDocument();
+ expect(screen.getByText("0")).toBeInTheDocument();
+ expect(screen.getByText("4")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/firefly/component.test.jsx b/src/widgets/firefly/component.test.jsx
new file mode 100644
index 000000000..2d4dcdf44
--- /dev/null
+++ b/src/widgets/firefly/component.test.jsx
@@ -0,0 +1,74 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({
+ default: useWidgetAPI,
+}));
+
+import Component from "./component";
+
+describe("widgets/firefly/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders placeholders while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(2);
+ expect(screen.getByText("firefly.networth")).toBeInTheDocument();
+ expect(screen.getByText("firefly.budget")).toBeInTheDocument();
+ });
+
+ it("renders error UI when either request errors", () => {
+ useWidgetAPI
+ .mockReturnValueOnce({ data: undefined, error: { message: "nope" } }) // summary
+ .mockReturnValueOnce({ data: undefined, error: undefined }); // budgets
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ // The widget uses a string error, which Error normalizes to { message }.
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ expect(screen.getByText("Failed to load Firefly account summary and budgets")).toBeInTheDocument();
+ });
+
+ it("renders net worth and budget summary", () => {
+ useWidgetAPI
+ .mockReturnValueOnce({
+ data: { "net-worth-in-EUR": { value_parsed: "100" } },
+ error: undefined,
+ })
+ .mockReturnValueOnce({
+ data: {
+ data: [
+ {
+ type: "available_budgets",
+ attributes: {
+ amount: "100",
+ currency_symbol: "$",
+ spent_in_budgets: [{ sum: "-10" }],
+ },
+ },
+ ],
+ },
+ error: undefined,
+ });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getByText("100")).toBeInTheDocument();
+ expect(screen.getByText("$ 10 / $ 100")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/jellystat/component.test.jsx b/src/widgets/jellystat/component.test.jsx
new file mode 100644
index 000000000..ecb822496
--- /dev/null
+++ b/src/widgets/jellystat/component.test.jsx
@@ -0,0 +1,65 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({
+ default: useWidgetAPI,
+}));
+
+import Component from "./component";
+
+describe("widgets/jellystat/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("defaults invalid days to 30 and renders placeholders while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const service = { widget: { type: "jellystat", days: -1 } };
+ const { container } = renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(service.widget.days).toBe(30);
+ expect(useWidgetAPI).toHaveBeenCalledWith(service.widget, "getViewsByLibraryType", { days: 30 });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("jellystat.songs")).toBeInTheDocument();
+ expect(screen.getByText("jellystat.movies")).toBeInTheDocument();
+ expect(screen.getByText("jellystat.episodes")).toBeInTheDocument();
+ expect(screen.getByText("jellystat.other")).toBeInTheDocument();
+ });
+
+ it("renders error UI when widget API errors", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } });
+
+ renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ });
+
+ it("renders values when loaded", () => {
+ useWidgetAPI.mockReturnValue({
+ data: { Audio: 1, Movie: 2, Series: 3, Other: 4 },
+ error: undefined,
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.getByText("2")).toBeInTheDocument();
+ expect(screen.getByText("3")).toBeInTheDocument();
+ expect(screen.getByText("4")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/nextcloud/component.test.jsx b/src/widgets/nextcloud/component.test.jsx
new file mode 100644
index 000000000..e69d219c9
--- /dev/null
+++ b/src/widgets/nextcloud/component.test.jsx
@@ -0,0 +1,83 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({
+ default: useWidgetAPI,
+}));
+
+import Component from "./component";
+
+describe("widgets/nextcloud/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders default placeholders (no cpu/memory blocks when fields are unset)", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.queryByText("nextcloud.cpuload")).toBeNull();
+ expect(screen.queryByText("nextcloud.memoryusage")).toBeNull();
+ expect(screen.getByText("nextcloud.freespace")).toBeInTheDocument();
+ expect(screen.getByText("nextcloud.activeusers")).toBeInTheDocument();
+ expect(screen.getByText("nextcloud.numfiles")).toBeInTheDocument();
+ expect(screen.getByText("nextcloud.numshares")).toBeInTheDocument();
+ });
+
+ it("respects widget.fields and renders computed values", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ ocs: {
+ data: {
+ nextcloud: {
+ system: {
+ cpuload: [0.5],
+ mem_total: "100",
+ mem_free: "50",
+ freespace: 1024,
+ },
+ storage: { num_files: 1 },
+ shares: { num_shares: 2 },
+ },
+ activeUsers: { last24hours: 3 },
+ },
+ },
+ },
+ error: undefined,
+ });
+
+ // 4 fields triggers the legacy behavior where CPU + memory are shown;
+ // Container then filters to exactly these fields.
+ const service = {
+ widget: { type: "nextcloud", fields: ["cpuload", "memoryusage", "freespace", "activeusers"] },
+ };
+
+ const { container } = renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("nextcloud.cpuload")).toBeInTheDocument();
+ expect(screen.getByText("nextcloud.memoryusage")).toBeInTheDocument();
+ expect(screen.getByText("nextcloud.freespace")).toBeInTheDocument();
+ expect(screen.getByText("nextcloud.activeusers")).toBeInTheDocument();
+ expect(screen.queryByText("nextcloud.numfiles")).toBeNull();
+ expect(screen.queryByText("nextcloud.numshares")).toBeNull();
+
+ // Values: cpu load 0.5, memory usage 50, freespace 1024, active users 3.
+ expect(screen.getByText("0.5")).toBeInTheDocument();
+ expect(screen.getByText("50")).toBeInTheDocument();
+ expect(screen.getByText("1024")).toBeInTheDocument();
+ expect(screen.getByText("3")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/peanut/component.test.jsx b/src/widgets/peanut/component.test.jsx
new file mode 100644
index 000000000..b7a0b1423
--- /dev/null
+++ b/src/widgets/peanut/component.test.jsx
@@ -0,0 +1,52 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({
+ default: useWidgetAPI,
+}));
+
+import Component from "./component";
+
+describe("widgets/peanut/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders placeholders while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(3);
+ expect(screen.getByText("peanut.battery_charge")).toBeInTheDocument();
+ expect(screen.getByText("peanut.ups_load")).toBeInTheDocument();
+ expect(screen.getByText("peanut.ups_status")).toBeInTheDocument();
+ });
+
+ it("renders legacy field mapping and status translation", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ "battery.charge": 55,
+ "ups.load": 12,
+ "ups.status": "OL",
+ },
+ error: undefined,
+ });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getByText("55")).toBeInTheDocument();
+ expect(screen.getByText("12")).toBeInTheDocument();
+ expect(screen.getByText("peanut.online")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/proxmoxbackupserver/component.test.jsx b/src/widgets/proxmoxbackupserver/component.test.jsx
new file mode 100644
index 000000000..c4a2ddd0a
--- /dev/null
+++ b/src/widgets/proxmoxbackupserver/component.test.jsx
@@ -0,0 +1,77 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({
+ default: useWidgetAPI,
+}));
+
+import Component from "./component";
+
+describe("widgets/proxmoxbackupserver/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders placeholders while loading", () => {
+ useWidgetAPI
+ .mockReturnValueOnce({ data: undefined, error: undefined }) // datastore
+ .mockReturnValueOnce({ data: undefined, error: undefined }) // tasks
+ .mockReturnValueOnce({ data: undefined, error: undefined }); // host
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("proxmoxbackupserver.datastore_usage")).toBeInTheDocument();
+ expect(screen.getByText("proxmoxbackupserver.failed_tasks_24h")).toBeInTheDocument();
+ expect(screen.getByText("proxmoxbackupserver.cpu_usage")).toBeInTheDocument();
+ expect(screen.getByText("proxmoxbackupserver.memory_usage")).toBeInTheDocument();
+ });
+
+ it("renders error UI when any endpoint errors", () => {
+ useWidgetAPI
+ .mockReturnValueOnce({ data: undefined, error: undefined })
+ .mockReturnValueOnce({ data: undefined, error: { message: "nope" } })
+ .mockReturnValueOnce({ data: undefined, error: undefined });
+
+ renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ });
+
+ it("renders computed values and caps failed tasks at 99+", () => {
+ useWidgetAPI
+ .mockReturnValueOnce({
+ data: {
+ data: [
+ { store: "ds1", used: 50, total: 100 },
+ { store: "ds2", used: 25, total: 50 },
+ ],
+ },
+ error: undefined,
+ })
+ .mockReturnValueOnce({ data: { total: 1000 }, error: undefined })
+ .mockReturnValueOnce({ data: { data: { cpu: 0.2, memory: { used: 1, total: 4 } } }, error: undefined });
+
+ renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ // datastore usage for ds2: 25/50*100 = 50
+ expect(screen.getByText("50")).toBeInTheDocument();
+ expect(screen.getByText("20")).toBeInTheDocument(); // cpu usage
+ expect(screen.getByText("25")).toBeInTheDocument(); // memory usage
+ expect(screen.getByText("99+")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/rutorrent/component.test.jsx b/src/widgets/rutorrent/component.test.jsx
new file mode 100644
index 000000000..36ab86ae0
--- /dev/null
+++ b/src/widgets/rutorrent/component.test.jsx
@@ -0,0 +1,63 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({
+ default: useWidgetAPI,
+}));
+
+import Component from "./component";
+
+describe("widgets/rutorrent/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders placeholders while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(3);
+ expect(screen.getByText("rutorrent.active")).toBeInTheDocument();
+ expect(screen.getByText("rutorrent.upload")).toBeInTheDocument();
+ expect(screen.getByText("rutorrent.download")).toBeInTheDocument();
+ });
+
+ it("renders error UI when widget API errors", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ });
+
+ it("renders computed active/upload/download values", () => {
+ useWidgetAPI.mockReturnValue({
+ data: [
+ { "d.get_state": "1", "d.get_up_rate": "10", "d.get_down_rate": "5" },
+ { "d.get_state": "0", "d.get_up_rate": "20", "d.get_down_rate": "15" },
+ { "d.get_state": "1", "d.get_up_rate": "0", "d.get_down_rate": "0" },
+ ],
+ error: undefined,
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(3);
+ expect(screen.getByText("2")).toBeInTheDocument(); // active torrents
+ expect(screen.getByText("30")).toBeInTheDocument(); // upload sum (common.byterate mocked)
+ expect(screen.getByText("20")).toBeInTheDocument(); // download sum (common.byterate mocked)
+ });
+});
diff --git a/src/widgets/swagdashboard/component.test.jsx b/src/widgets/swagdashboard/component.test.jsx
new file mode 100644
index 000000000..6c6a83ac8
--- /dev/null
+++ b/src/widgets/swagdashboard/component.test.jsx
@@ -0,0 +1,53 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({
+ default: useWidgetAPI,
+}));
+
+import Component from "./component";
+
+describe("widgets/swagdashboard/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders placeholders while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("swagdashboard.proxied")).toBeInTheDocument();
+ expect(screen.getByText("swagdashboard.auth")).toBeInTheDocument();
+ expect(screen.getByText("swagdashboard.outdated")).toBeInTheDocument();
+ expect(screen.getByText("swagdashboard.banned")).toBeInTheDocument();
+ });
+
+ it("renders values when loaded", () => {
+ useWidgetAPI.mockReturnValue({
+ data: { proxied: 1, auth: 2, outdated: 3, banned: 4 },
+ error: undefined,
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.getByText("2")).toBeInTheDocument();
+ expect(screen.getByText("3")).toBeInTheDocument();
+ expect(screen.getByText("4")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/truenas/component.test.jsx b/src/widgets/truenas/component.test.jsx
new file mode 100644
index 000000000..bf9697259
--- /dev/null
+++ b/src/widgets/truenas/component.test.jsx
@@ -0,0 +1,81 @@
+// @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,
+}));
+
+// Pool is rendered outside of the main Container; stub it to a simple marker.
+vi.mock("widgets/truenas/pool", () => ({
+ default: ({ name, healthy, allocated, free }) => (
+
+ ),
+}));
+
+import Component from "./component";
+
+describe("widgets/truenas/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders placeholders while loading (no pools)", () => {
+ useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined }));
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(3);
+ expect(screen.getByText("truenas.load")).toBeInTheDocument();
+ expect(screen.getByText("truenas.uptime")).toBeInTheDocument();
+ expect(screen.getByText("truenas.alerts")).toBeInTheDocument();
+ expect(screen.queryByTestId("truenas-pool")).toBeNull();
+ });
+
+ it("renders values and pool list when enablePools is on and data is present", () => {
+ useWidgetAPI.mockImplementation((widget, endpoint) => {
+ if (endpoint === "alerts") return { data: { pending: 7 }, error: undefined };
+ if (endpoint === "status") return { data: { loadavg: [1.23], uptime_seconds: 3600 }, error: undefined };
+ if (endpoint === "pools") return { data: [{ id: "1", name: "tank", healthy: true }], error: undefined };
+ if (endpoint === "dataset")
+ return {
+ data: [{ pool: "tank", name: "tank", used: { parsed: 10 }, available: { parsed: 20 } }],
+ error: undefined,
+ };
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(
+ ,
+ {
+ settings: { hideErrors: false },
+ },
+ );
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(3);
+ expect(screen.getByText("1.23")).toBeInTheDocument();
+ expect(screen.getByText("3600")).toBeInTheDocument(); // common.duration mocked
+ expect(screen.getByText("7")).toBeInTheDocument();
+
+ const pool = screen.getByTestId("truenas-pool");
+ expect(pool.getAttribute("data-name")).toBe("tank");
+ expect(pool.getAttribute("data-healthy")).toBe("true");
+ expect(pool.getAttribute("data-allocated")).toBe("10");
+ expect(pool.getAttribute("data-free")).toBe("20");
+ });
+});
diff --git a/src/widgets/unraid/component.test.jsx b/src/widgets/unraid/component.test.jsx
new file mode 100644
index 000000000..e6b82743d
--- /dev/null
+++ b/src/widgets/unraid/component.test.jsx
@@ -0,0 +1,67 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("utils/proxy/use-widget-api", () => ({
+ default: useWidgetAPI,
+}));
+
+import Component from "./component";
+
+describe("widgets/unraid/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("defaults widget.fields and filters down to 4 visible blocks while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const service = { widget: { type: "unraid" } };
+ const { container } = renderWithProviders(, { settings: { hideErrors: false } });
+
+ // Component sets default fields
+ expect(service.widget.fields).toEqual(["status", "cpu", "memoryPercent", "notifications"]);
+
+ // Container filters the many placeholder Blocks down to the selected fields.
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("unraid.status")).toBeInTheDocument();
+ expect(screen.getByText("unraid.cpu")).toBeInTheDocument();
+ expect(screen.getByText("unraid.notifications")).toBeInTheDocument();
+ expect(screen.getByText("unraid.memoryUsed")).toBeInTheDocument();
+ expect(screen.queryByText("unraid.memoryAvailable")).toBeNull();
+ });
+
+ it("renders values for the default fields", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ arrayState: "started",
+ cpuPercent: 12,
+ memoryAvailable: 100,
+ memoryUsed: 50,
+ memoryUsedPercent: 33,
+ unreadNotifications: 7,
+ arrayUsed: 1,
+ arrayFree: 2,
+ arrayUsedPercent: 3,
+ caches: {},
+ },
+ error: undefined,
+ });
+
+ const service = { widget: { type: "unraid" } };
+ const { container } = renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("unraid.started")).toBeInTheDocument();
+ expect(screen.getByText("12")).toBeInTheDocument();
+ expect(screen.getByText("33")).toBeInTheDocument();
+ expect(screen.getByText("7")).toBeInTheDocument();
+ });
+});
diff --git a/vitest.setup.js b/vitest.setup.js
index 946eff05e..7129f114c 100644
--- a/vitest.setup.js
+++ b/vitest.setup.js
@@ -4,7 +4,8 @@ import { cleanup } from "@testing-library/react";
import { afterEach, vi } from "vitest";
afterEach(() => {
- cleanup();
+ // Node-environment tests shouldn't require jsdom; guard cleanup accordingly.
+ if (typeof document !== "undefined") cleanup();
});
// implement a couple of common formatters mocked in next-i18next
@@ -13,6 +14,10 @@ vi.mock("next-i18next", () => ({
t: (key, opts) => {
if (key === "common.number") return String(opts?.value ?? "");
if (key === "common.percent") return String(opts?.value ?? "");
+ if (key === "common.bytes") return String(opts?.value ?? "");
+ if (key === "common.bbytes") return String(opts?.value ?? "");
+ if (key === "common.byterate") return String(opts?.value ?? "");
+ if (key === "common.duration") return String(opts?.value ?? "");
return key;
},
}),