From 4ffe757e33aa439a16c92d9ab0641d81a35596e2 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Tue, 3 Feb 2026 12:29:43 -0800
Subject: [PATCH] Add widget component tests (tandoor..unmanic)
---
src/widgets/tandoor/component.test.jsx | 53 +++++++++++++
src/widgets/tautulli/component.test.jsx | 69 ++++++++++++++++
src/widgets/tdarr/component.test.jsx | 64 +++++++++++++++
src/widgets/technitium/component.test.jsx | 66 ++++++++++++++++
src/widgets/traefik/component.test.jsx | 52 +++++++++++++
src/widgets/transmission/component.test.jsx | 61 +++++++++++++++
src/widgets/trilium/component.test.jsx | 52 +++++++++++++
src/widgets/tubearchivist/component.test.jsx | 57 ++++++++++++++
src/widgets/unifi/component.test.jsx | 82 ++++++++++++++++++++
src/widgets/unmanic/component.test.jsx | 49 ++++++++++++
10 files changed, 605 insertions(+)
create mode 100644 src/widgets/tandoor/component.test.jsx
create mode 100644 src/widgets/tautulli/component.test.jsx
create mode 100644 src/widgets/tdarr/component.test.jsx
create mode 100644 src/widgets/technitium/component.test.jsx
create mode 100644 src/widgets/traefik/component.test.jsx
create mode 100644 src/widgets/transmission/component.test.jsx
create mode 100644 src/widgets/trilium/component.test.jsx
create mode 100644 src/widgets/tubearchivist/component.test.jsx
create mode 100644 src/widgets/unifi/component.test.jsx
create mode 100644 src/widgets/unmanic/component.test.jsx
diff --git a/src/widgets/tandoor/component.test.jsx b/src/widgets/tandoor/component.test.jsx
new file mode 100644
index 000000000..e3ab09efc
--- /dev/null
+++ b/src/widgets/tandoor/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";
+import { findServiceBlockByLabel } from "test-utils/widget-assertions";
+
+const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./component";
+
+function expectBlockValue(container, label, value) {
+ const block = findServiceBlockByLabel(container, label);
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/tandoor/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("tandoor.users")).toBeInTheDocument();
+ expect(screen.getByText("tandoor.recipes")).toBeInTheDocument();
+ expect(screen.getByText("tandoor.keywords")).toBeInTheDocument();
+ });
+
+ it("renders values when loaded (spaceData.results shape)", () => {
+ useWidgetAPI.mockImplementation((_widget, endpoint) => {
+ if (endpoint === "space") return { data: { results: [{ user_count: 1, recipe_count: 2 }] }, error: undefined };
+ if (endpoint === "keyword") return { data: { count: 3 }, error: undefined };
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expectBlockValue(container, "tandoor.users", 1);
+ expectBlockValue(container, "tandoor.recipes", 2);
+ expectBlockValue(container, "tandoor.keywords", 3);
+ });
+});
diff --git a/src/widgets/tautulli/component.test.jsx b/src/widgets/tautulli/component.test.jsx
new file mode 100644
index 000000000..32724107e
--- /dev/null
+++ b/src/widgets/tautulli/component.test.jsx
@@ -0,0 +1,69 @@
+// @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/tautulli/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders placeholder rows while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ // Default behavior shows 2 placeholder rows, but just assert we see at least one.
+ expect(screen.getAllByText("-").length).toBeGreaterThan(0);
+ });
+
+ it("renders no-active message when there are no sessions", () => {
+ useWidgetAPI.mockReturnValue({
+ data: { response: { data: { sessions: [] } } },
+ error: undefined,
+ });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getByText("tautulli.no_active")).toBeInTheDocument();
+ });
+
+ it("renders an expanded two-row entry when a single session is playing", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ response: {
+ data: {
+ sessions: [
+ {
+ session_key: "1",
+ full_title: "Movie",
+ media_type: "movie",
+ duration: 2000,
+ view_offset: 1000,
+ progress_percent: 50,
+ state: "playing",
+ video_decision: "direct play",
+ audio_decision: "direct play",
+ },
+ ],
+ },
+ },
+ },
+ error: undefined,
+ });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getByText("Movie")).toBeInTheDocument();
+ // view_offset 1s => "00:01", duration 2s => "00:02"
+ expect(screen.getByText(/00:01/)).toBeInTheDocument();
+ expect(screen.getByText(/00:02/)).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/tdarr/component.test.jsx b/src/widgets/tdarr/component.test.jsx
new file mode 100644
index 000000000..051a5d366
--- /dev/null
+++ b/src/widgets/tdarr/component.test.jsx
@@ -0,0 +1,64 @@
+// @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";
+import { findServiceBlockByLabel } from "test-utils/widget-assertions";
+
+const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./component";
+
+function expectBlockValue(container, label, value) {
+ const block = findServiceBlockByLabel(container, label);
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/tdarr/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("tdarr.queue")).toBeInTheDocument();
+ expect(screen.getByText("tdarr.processed")).toBeInTheDocument();
+ expect(screen.getByText("tdarr.errored")).toBeInTheDocument();
+ expect(screen.getByText("tdarr.saved")).toBeInTheDocument();
+ });
+
+ it("computes queue/processed/errored/saved when loaded", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ table1Count: "1",
+ table2Count: "2",
+ table3Count: "3",
+ table4Count: "4",
+ table5Count: "5",
+ table6Count: "6",
+ sizeDiff: "1.5",
+ },
+ error: undefined,
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ // queue = 1+4, processed = 2+5, errored = 3+6
+ expectBlockValue(container, "tdarr.queue", 5);
+ expectBlockValue(container, "tdarr.processed", 7);
+ expectBlockValue(container, "tdarr.errored", 9);
+ // saved = 1.5 * 1e9
+ expectBlockValue(container, "tdarr.saved", 1_500_000_000);
+ });
+});
diff --git a/src/widgets/technitium/component.test.jsx b/src/widgets/technitium/component.test.jsx
new file mode 100644
index 000000000..af54b493a
--- /dev/null
+++ b/src/widgets/technitium/component.test.jsx
@@ -0,0 +1,66 @@
+// @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";
+import { findServiceBlockByLabel } from "test-utils/widget-assertions";
+
+const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component, { technitiumDefaultFields } from "./component";
+
+function expectBlockValue(container, label, value) {
+ const block = findServiceBlockByLabel(container, label);
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/technitium/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("defaults fields to 4 and filters loading placeholders accordingly", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const service = { widget: { type: "technitium" } };
+ const { container } = renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(service.widget.fields).toEqual(technitiumDefaultFields);
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("technitium.totalQueries")).toBeInTheDocument();
+ expect(screen.getByText("technitium.totalAuthoritative")).toBeInTheDocument();
+ expect(screen.getByText("technitium.totalCached")).toBeInTheDocument();
+ expect(screen.getByText("technitium.totalServerFailure")).toBeInTheDocument();
+ expect(screen.queryByText("technitium.totalNoError")).toBeNull();
+ });
+
+ it("renders selected totals with percentages when loaded", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ totalQueries: 100,
+ totalNoError: 50,
+ totalServerFailure: 25,
+ totalNxDomain: 25,
+ },
+ error: undefined,
+ });
+
+ const service = {
+ widget: { type: "technitium", fields: ["totalQueries", "totalNoError", "totalServerFailure", "totalNxDomain"] },
+ };
+
+ const { container } = renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expectBlockValue(container, "technitium.totalQueries", 100);
+ expectBlockValue(container, "technitium.totalNoError", "50");
+ expectBlockValue(container, "technitium.totalNoError", "50");
+ expectBlockValue(container, "technitium.totalServerFailure", "25");
+ expectBlockValue(container, "technitium.totalNxDomain", "25");
+ // Percent strings are included in parens, e.g. "50 (50)"
+ expect(findServiceBlockByLabel(container, "technitium.totalNoError")?.textContent).toContain("(");
+ });
+});
diff --git a/src/widgets/traefik/component.test.jsx b/src/widgets/traefik/component.test.jsx
new file mode 100644
index 000000000..9f1c9313a
--- /dev/null
+++ b/src/widgets/traefik/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";
+import { findServiceBlockByLabel } from "test-utils/widget-assertions";
+
+const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./component";
+
+function expectBlockValue(container, label, value) {
+ const block = findServiceBlockByLabel(container, label);
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/traefik/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("traefik.routers")).toBeInTheDocument();
+ expect(screen.getByText("traefik.services")).toBeInTheDocument();
+ expect(screen.getByText("traefik.middleware")).toBeInTheDocument();
+ });
+
+ it("renders totals when loaded", () => {
+ useWidgetAPI.mockReturnValue({
+ data: { http: { routers: { total: 1 }, services: { total: 2 }, middlewares: { total: 3 } } },
+ error: undefined,
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expectBlockValue(container, "traefik.routers", 1);
+ expectBlockValue(container, "traefik.services", 2);
+ expectBlockValue(container, "traefik.middleware", 3);
+ });
+});
diff --git a/src/widgets/transmission/component.test.jsx b/src/widgets/transmission/component.test.jsx
new file mode 100644
index 000000000..95d2593e4
--- /dev/null
+++ b/src/widgets/transmission/component.test.jsx
@@ -0,0 +1,61 @@
+// @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";
+import { findServiceBlockByLabel } from "test-utils/widget-assertions";
+
+const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./component";
+
+function expectBlockValue(container, label, value) {
+ const block = findServiceBlockByLabel(container, label);
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/transmission/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("transmission.leech")).toBeInTheDocument();
+ expect(screen.getByText("transmission.download")).toBeInTheDocument();
+ expect(screen.getByText("transmission.seed")).toBeInTheDocument();
+ expect(screen.getByText("transmission.upload")).toBeInTheDocument();
+ });
+
+ it("computes leech/seed counts and upload/download rates when loaded", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ arguments: {
+ torrents: [
+ { rateDownload: 10, rateUpload: 1, percentDone: 1 },
+ { rateDownload: 5, rateUpload: 2, percentDone: 0.5 },
+ ],
+ },
+ },
+ error: undefined,
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expectBlockValue(container, "transmission.leech", 1);
+ expectBlockValue(container, "transmission.seed", 1);
+ expectBlockValue(container, "transmission.download", 15);
+ expectBlockValue(container, "transmission.upload", 3);
+ });
+});
diff --git a/src/widgets/trilium/component.test.jsx b/src/widgets/trilium/component.test.jsx
new file mode 100644
index 000000000..6d930b45d
--- /dev/null
+++ b/src/widgets/trilium/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";
+import { findServiceBlockByLabel } from "test-utils/widget-assertions";
+
+const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./component";
+
+function expectBlockValue(container, label, value) {
+ const block = findServiceBlockByLabel(container, label);
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/trilium/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("trilium.version")).toBeInTheDocument();
+ expect(screen.getByText("trilium.notesCount")).toBeInTheDocument();
+ expect(screen.getByText("trilium.dbSize")).toBeInTheDocument();
+ });
+
+ it("renders metrics when loaded", () => {
+ useWidgetAPI.mockReturnValue({
+ data: { version: { app: "1.0.0" }, database: { activeNotes: 2 }, statistics: { databaseSizeBytes: 1024 } },
+ error: undefined,
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expectBlockValue(container, "trilium.version", "v1.0.0");
+ expectBlockValue(container, "trilium.notesCount", 2);
+ expectBlockValue(container, "trilium.dbSize", 1024);
+ });
+});
diff --git a/src/widgets/tubearchivist/component.test.jsx b/src/widgets/tubearchivist/component.test.jsx
new file mode 100644
index 000000000..5f181d233
--- /dev/null
+++ b/src/widgets/tubearchivist/component.test.jsx
@@ -0,0 +1,57 @@
+// @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";
+import { findServiceBlockByLabel } from "test-utils/widget-assertions";
+
+const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./component";
+
+function expectBlockValue(container, label, value) {
+ const block = findServiceBlockByLabel(container, label);
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/tubearchivist/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("tubearchivist.downloads")).toBeInTheDocument();
+ expect(screen.getByText("tubearchivist.videos")).toBeInTheDocument();
+ expect(screen.getByText("tubearchivist.channels")).toBeInTheDocument();
+ expect(screen.getByText("tubearchivist.playlists")).toBeInTheDocument();
+ });
+
+ it("renders counts when loaded", () => {
+ useWidgetAPI.mockImplementation((_widget, endpoint) => {
+ if (endpoint === "downloads") return { data: { pending: 1 }, error: undefined };
+ if (endpoint === "videos") return { data: { doc_count: 2 }, error: undefined };
+ if (endpoint === "channels") return { data: { doc_count: 3 }, error: undefined };
+ if (endpoint === "playlists") return { data: { doc_count: 4 }, error: undefined };
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expectBlockValue(container, "tubearchivist.downloads", 1);
+ expectBlockValue(container, "tubearchivist.videos", 2);
+ expectBlockValue(container, "tubearchivist.channels", 3);
+ expectBlockValue(container, "tubearchivist.playlists", 4);
+ });
+});
diff --git a/src/widgets/unifi/component.test.jsx b/src/widgets/unifi/component.test.jsx
new file mode 100644
index 000000000..2947a25ef
--- /dev/null
+++ b/src/widgets/unifi/component.test.jsx
@@ -0,0 +1,82 @@
+// @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";
+import { findServiceBlockByLabel } from "test-utils/widget-assertions";
+
+const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./component";
+
+function expectBlockValue(container, label, value) {
+ const block = findServiceBlockByLabel(container, label);
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/unifi/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders placeholders when default site isn't available yet", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(screen.getByText("unifi.uptime")).toBeInTheDocument();
+ expect(screen.getByText("unifi.wan")).toBeInTheDocument();
+ expect(screen.getByText("unifi.lan_users")).toBeInTheDocument();
+ expect(screen.getByText("unifi.wlan_users")).toBeInTheDocument();
+ // 4 blocks if all are rendered.
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ });
+
+ it("renders a site-not-found error when widget.site doesn't match", () => {
+ useWidgetAPI.mockReturnValue({
+ data: { data: [{ name: "default", desc: "Default", health: [] }] },
+ error: undefined,
+ });
+
+ renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ expect(screen.getByText("Site 'Nope' not found")).toBeInTheDocument();
+ });
+
+ it("renders uptime, wan and user counts when site data is present", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ data: [
+ {
+ name: "default",
+ desc: "Default",
+ health: [
+ { subsystem: "wan", status: "ok", num_user: 0, num_adopted: 0, "gw_system-stats": { uptime: 86400 } },
+ { subsystem: "lan", status: "ok", num_user: 2, num_adopted: 5 },
+ { subsystem: "wlan", status: "ok", num_user: 3, num_adopted: 6 },
+ ],
+ },
+ ],
+ },
+ error: undefined,
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ // uptime includes unifi.days suffix.
+ expect(findServiceBlockByLabel(container, "unifi.uptime")?.textContent).toContain("unifi.days");
+ expectBlockValue(container, "unifi.wan", "unifi.up");
+ expectBlockValue(container, "unifi.lan_users", 2);
+ expectBlockValue(container, "unifi.wlan_users", 3);
+ });
+});
diff --git a/src/widgets/unmanic/component.test.jsx b/src/widgets/unmanic/component.test.jsx
new file mode 100644
index 000000000..94b2acea8
--- /dev/null
+++ b/src/widgets/unmanic/component.test.jsx
@@ -0,0 +1,49 @@
+// @vitest-environment jsdom
+
+import { screen, waitFor } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders } from "test-utils/render-with-providers";
+import { findServiceBlockByLabel } from "test-utils/widget-assertions";
+
+const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
+vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
+
+import Component from "./component";
+
+function expectBlockValue(container, label, value) {
+ const block = findServiceBlockByLabel(container, label);
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/unmanic/component", () => {
+ const originalFetch = globalThis.fetch;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ globalThis.fetch = vi.fn(async () => ({ json: async () => ({ recordsTotal: 7 }) }));
+ });
+
+ afterEach(() => {
+ globalThis.fetch = originalFetch;
+ });
+
+ it("renders placeholders while loading pending data, then renders worker + pending stats", async () => {
+ useWidgetAPI.mockReturnValue({ data: { active_workers: 1, total_workers: 2 }, error: undefined });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(screen.getByText("unmanic.active_workers")).toBeInTheDocument();
+ expect(screen.getByText("unmanic.total_workers")).toBeInTheDocument();
+ expect(screen.getByText("unmanic.records_total")).toBeInTheDocument();
+
+ await waitFor(() => {
+ expectBlockValue(container, "unmanic.active_workers", 1);
+ expectBlockValue(container, "unmanic.total_workers", 2);
+ expectBlockValue(container, "unmanic.records_total", 7);
+ });
+ });
+});