From cbf6aef635ce80fe6e432e5b89d01223ceb13883 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Mon, 2 Feb 2026 22:38:54 -0800
Subject: [PATCH] Test: 10 more widget components (A)
---
src/widgets/adguard/component.test.jsx | 63 ++++++++++
src/widgets/apcups/component.test.jsx | 48 ++++++++
src/widgets/argocd/component.test.jsx | 65 +++++++++++
src/widgets/atsumeru/component.test.jsx | 51 +++++++++
src/widgets/audiobookshelf/component.test.jsx | 62 ++++++++++
src/widgets/authentik/component.test.jsx | 108 ++++++++++++++++++
src/widgets/autobrr/component.test.jsx | 60 ++++++++++
src/widgets/azuredevops/component.test.jsx | 101 ++++++++++++++++
src/widgets/backrest/component.test.jsx | 81 +++++++++++++
src/widgets/bazarr/component.test.jsx | 56 +++++++++
vitest.setup.js | 1 +
11 files changed, 696 insertions(+)
create mode 100644 src/widgets/adguard/component.test.jsx
create mode 100644 src/widgets/apcups/component.test.jsx
create mode 100644 src/widgets/argocd/component.test.jsx
create mode 100644 src/widgets/atsumeru/component.test.jsx
create mode 100644 src/widgets/audiobookshelf/component.test.jsx
create mode 100644 src/widgets/authentik/component.test.jsx
create mode 100644 src/widgets/autobrr/component.test.jsx
create mode 100644 src/widgets/azuredevops/component.test.jsx
create mode 100644 src/widgets/backrest/component.test.jsx
create mode 100644 src/widgets/bazarr/component.test.jsx
diff --git a/src/widgets/adguard/component.test.jsx b/src/widgets/adguard/component.test.jsx
new file mode 100644
index 000000000..90c6890f9
--- /dev/null
+++ b/src/widgets/adguard/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/adguard/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("adguard.queries")).toBeInTheDocument();
+ expect(screen.getByText("adguard.blocked")).toBeInTheDocument();
+ expect(screen.getByText("adguard.filtered")).toBeInTheDocument();
+ expect(screen.getByText("adguard.latency")).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 filtered and latency values", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ num_dns_queries: 100,
+ num_blocked_filtering: 20,
+ num_replaced_safebrowsing: 1,
+ num_replaced_safesearch: 2,
+ num_replaced_parental: 3,
+ avg_processing_time: 0.01,
+ },
+ error: undefined,
+ });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getByText("100")).toBeInTheDocument();
+ expect(screen.getByText("20")).toBeInTheDocument();
+ expect(screen.getByText("6")).toBeInTheDocument(); // filtered sum
+ expect(screen.getByText("10")).toBeInTheDocument(); // 0.01s -> 10ms
+ });
+});
diff --git a/src/widgets/apcups/component.test.jsx b/src/widgets/apcups/component.test.jsx
new file mode 100644
index 000000000..f9509d9b3
--- /dev/null
+++ b/src/widgets/apcups/component.test.jsx
@@ -0,0 +1,48 @@
+// @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/apcups/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("apcups.status")).toBeInTheDocument();
+ expect(screen.getByText("apcups.load")).toBeInTheDocument();
+ expect(screen.getByText("apcups.bcharge")).toBeInTheDocument();
+ expect(screen.getByText("apcups.timeleft")).toBeInTheDocument();
+ });
+
+ it("renders values when loaded", () => {
+ useWidgetAPI.mockReturnValue({
+ data: { status: "ONLINE", load: "12", bcharge: "99", timeleft: "30" },
+ error: undefined,
+ });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getByText("ONLINE")).toBeInTheDocument();
+ expect(screen.getByText("12")).toBeInTheDocument();
+ expect(screen.getByText("99")).toBeInTheDocument();
+ expect(screen.getByText("30")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/argocd/component.test.jsx b/src/widgets/argocd/component.test.jsx
new file mode 100644
index 000000000..e59175cf2
--- /dev/null
+++ b/src/widgets/argocd/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";
+
+function expectBlockValue(container, label, value) {
+ const blocks = Array.from(container.querySelectorAll(".service-block"));
+ const block = blocks.find((b) => b.textContent?.includes(label));
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/argocd/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("defaults and truncates widget.fields to 4 and renders placeholders while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const service = { widget: { type: "argocd", fields: ["apps", "synced", "outOfSync", "healthy", "extra"] } };
+ const { container } = renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(service.widget.fields).toEqual(["apps", "synced", "outOfSync", "healthy"]);
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("argocd.apps")).toBeInTheDocument();
+ expect(screen.getByText("argocd.synced")).toBeInTheDocument();
+ expect(screen.getByText("argocd.outOfSync")).toBeInTheDocument();
+ expect(screen.getByText("argocd.healthy")).toBeInTheDocument();
+ });
+
+ it("renders counts when loaded", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ items: [
+ { status: { sync: { status: "Synced" }, health: { status: "Healthy" } } },
+ { status: { sync: { status: "OutOfSync" }, health: { status: "Degraded" } } },
+ { status: { sync: { status: "Synced" }, health: { status: "Healthy" } } },
+ ],
+ },
+ error: undefined,
+ });
+
+ const service = { widget: { type: "argocd" } };
+ const { container } = renderWithProviders(, { settings: { hideErrors: false } });
+
+ // Default widget fields: apps/synced/outOfSync/healthy => all 4 should be visible.
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+
+ expectBlockValue(container, "argocd.apps", 3);
+ expectBlockValue(container, "argocd.synced", 2);
+ expectBlockValue(container, "argocd.outOfSync", 1);
+ expectBlockValue(container, "argocd.healthy", 2);
+ });
+});
diff --git a/src/widgets/atsumeru/component.test.jsx b/src/widgets/atsumeru/component.test.jsx
new file mode 100644
index 000000000..b2fd56b54
--- /dev/null
+++ b/src/widgets/atsumeru/component.test.jsx
@@ -0,0 +1,51 @@
+// @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/atsumeru/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("atsumeru.series")).toBeInTheDocument();
+ expect(screen.getByText("atsumeru.archives")).toBeInTheDocument();
+ expect(screen.getByText("atsumeru.chapters")).toBeInTheDocument();
+ expect(screen.getByText("atsumeru.categories")).toBeInTheDocument();
+ });
+
+ it("renders values when loaded", () => {
+ useWidgetAPI.mockReturnValue({
+ data: { stats: { total_series: 1, total_archives: 2, total_chapters: 3, total_categories: 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/audiobookshelf/component.test.jsx b/src/widgets/audiobookshelf/component.test.jsx
new file mode 100644
index 000000000..9f74a89d2
--- /dev/null
+++ b/src/widgets/audiobookshelf/component.test.jsx
@@ -0,0 +1,62 @@
+// @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";
+
+function expectBlockValue(container, label, value) {
+ const blocks = Array.from(container.querySelectorAll(".service-block"));
+ const block = blocks.find((b) => b.textContent?.includes(label));
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/audiobookshelf/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("audiobookshelf.podcasts")).toBeInTheDocument();
+ expect(screen.getByText("audiobookshelf.podcastsDuration")).toBeInTheDocument();
+ expect(screen.getByText("audiobookshelf.books")).toBeInTheDocument();
+ expect(screen.getByText("audiobookshelf.booksDuration")).toBeInTheDocument();
+ });
+
+ it("aggregates totals across libraries by mediaType", () => {
+ useWidgetAPI.mockReturnValue({
+ data: [
+ { mediaType: "podcast", stats: { totalItems: "2", totalDuration: "100" } },
+ { mediaType: "podcast", stats: { totalItems: "1", totalDuration: "200" } },
+ { mediaType: "book", stats: { totalItems: "4", totalDuration: "300" } },
+ ],
+ error: undefined,
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expectBlockValue(container, "audiobookshelf.podcasts", 3);
+ expectBlockValue(container, "audiobookshelf.podcastsDuration", 300);
+ expectBlockValue(container, "audiobookshelf.books", 4);
+ expectBlockValue(container, "audiobookshelf.booksDuration", 300);
+ });
+});
diff --git a/src/widgets/authentik/component.test.jsx b/src/widgets/authentik/component.test.jsx
new file mode 100644
index 000000000..43a4f39c4
--- /dev/null
+++ b/src/widgets/authentik/component.test.jsx
@@ -0,0 +1,108 @@
+// @vitest-environment jsdom
+
+import { screen } from "@testing-library/react";
+import { afterEach, 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";
+
+function expectBlockValue(container, label, value) {
+ const blocks = Array.from(container.querySelectorAll(".service-block"));
+ const block = blocks.find((b) => b.textContent?.includes(label));
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/authentik/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("renders placeholders while loading", () => {
+ useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined }));
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(3);
+ expect(screen.getByText("authentik.users")).toBeInTheDocument();
+ expect(screen.getByText("authentik.loginsLast24H")).toBeInTheDocument();
+ expect(screen.getByText("authentik.failedLoginsLast24H")).toBeInTheDocument();
+ });
+
+ it("computes v2 login/failed counts from action data", () => {
+ useWidgetAPI.mockImplementation((widget, endpoint) => {
+ if (endpoint === "users") return { data: { pagination: { count: 10 } }, error: undefined };
+ if (endpoint === "loginv2")
+ return {
+ data: [
+ { action: "login", count: 2 },
+ { action: "logout", count: 9 },
+ ],
+ error: undefined,
+ };
+ if (endpoint === "login_failedv2") return { data: [{ count: 3 }, { count: null }], error: undefined };
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(3);
+ expectBlockValue(container, "authentik.users", 10);
+ expectBlockValue(container, "authentik.loginsLast24H", 2);
+ expectBlockValue(container, "authentik.failedLoginsLast24H", 3);
+ });
+
+ it("computes v1 login/failed counts for entries within the last 24h window", () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2026-01-02T00:00:00Z"));
+
+ const now = Date.now();
+ const oneHourAgo = now - 60 * 60 * 1000;
+ const twentyFiveHoursAgo = now - 25 * 60 * 60 * 1000;
+
+ useWidgetAPI.mockImplementation((widget, endpoint) => {
+ if (endpoint === "users") return { data: { pagination: { count: 5 } }, error: undefined };
+ if (endpoint === "login")
+ return {
+ data: [
+ { x_cord: oneHourAgo, y_cord: 2 },
+ { x_cord: twentyFiveHoursAgo, y_cord: 100 },
+ ],
+ error: undefined,
+ };
+ if (endpoint === "login_failed")
+ return {
+ data: [
+ { x_cord: oneHourAgo, y_cord: 1 },
+ { x_cord: twentyFiveHoursAgo, y_cord: 50 },
+ ],
+ error: undefined,
+ };
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(3);
+ expectBlockValue(container, "authentik.users", 5);
+ expectBlockValue(container, "authentik.loginsLast24H", 2);
+ expectBlockValue(container, "authentik.failedLoginsLast24H", 1);
+ });
+});
diff --git a/src/widgets/autobrr/component.test.jsx b/src/widgets/autobrr/component.test.jsx
new file mode 100644
index 000000000..8f828b2eb
--- /dev/null
+++ b/src/widgets/autobrr/component.test.jsx
@@ -0,0 +1,60 @@
+// @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";
+
+function expectBlockValue(container, label, value) {
+ const blocks = Array.from(container.querySelectorAll(".service-block"));
+ const block = blocks.find((b) => b.textContent?.includes(label));
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/autobrr/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders placeholders while loading", () => {
+ useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined }));
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("autobrr.approvedPushes")).toBeInTheDocument();
+ expect(screen.getByText("autobrr.rejectedPushes")).toBeInTheDocument();
+ expect(screen.getByText("autobrr.filters")).toBeInTheDocument();
+ expect(screen.getByText("autobrr.indexers")).toBeInTheDocument();
+ });
+
+ it("renders values when loaded", () => {
+ useWidgetAPI.mockImplementation((widget, endpoint) => {
+ if (endpoint === "stats") return { data: { push_approved_count: 1, push_rejected_count: 2 }, error: undefined };
+ if (endpoint === "filters") return { data: [{}, {}], error: undefined };
+ if (endpoint === "indexers") return { data: [{}], error: undefined };
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expectBlockValue(container, "autobrr.approvedPushes", 1);
+ expectBlockValue(container, "autobrr.rejectedPushes", 2);
+ expectBlockValue(container, "autobrr.filters", 2);
+ expectBlockValue(container, "autobrr.indexers", 1);
+ });
+});
diff --git a/src/widgets/azuredevops/component.test.jsx b/src/widgets/azuredevops/component.test.jsx
new file mode 100644
index 000000000..e540e85d7
--- /dev/null
+++ b/src/widgets/azuredevops/component.test.jsx
@@ -0,0 +1,101 @@
+// @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";
+
+function expectBlockValue(container, label, value) {
+ const blocks = Array.from(container.querySelectorAll(".service-block"));
+ const block = blocks.find((b) => b.textContent?.includes(label));
+ expect(block, `missing block for ${label}`).toBeTruthy();
+ expect(block.textContent).toContain(String(value));
+}
+
+describe("widgets/azuredevops/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders placeholders while loading", () => {
+ useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined }));
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("azuredevops.result")).toBeInTheDocument();
+ expect(screen.getByText("azuredevops.totalPrs")).toBeInTheDocument();
+ expect(screen.getByText("azuredevops.myPrs")).toBeInTheDocument();
+ expect(screen.getByText("azuredevops.approved")).toBeInTheDocument();
+ });
+
+ it("renders pipeline result without PR blocks when includePR is false", () => {
+ useWidgetAPI.mockImplementation((widget, endpoint) => {
+ if (endpoint === null) return { data: undefined, error: undefined };
+ if (endpoint === "pipeline")
+ return { data: { value: [{ result: "succeeded", status: "completed" }] }, error: undefined };
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(1);
+ expectBlockValue(container, "azuredevops.result", "azuredevops.succeeded");
+ });
+
+ it("renders pipeline status and PR aggregates when includePR is true", () => {
+ useWidgetAPI.mockImplementation((widget, endpoint) => {
+ if (endpoint === "pipeline") return { data: { value: [{ status: "inProgress" }] }, error: undefined };
+ if (endpoint === "pr")
+ return {
+ data: {
+ count: 3,
+ value: [
+ { createdBy: { uniqueName: "me@example.com" }, reviewers: [{ vote: 5 }] },
+ { createdBy: { uniqueName: "me@example.com" }, reviewers: [{ vote: 0 }] },
+ { createdBy: { uniqueName: "other@example.com" }, reviewers: [{ vote: 10 }] },
+ ],
+ },
+ error: undefined,
+ };
+ return { data: undefined, error: undefined };
+ });
+
+ const service = {
+ widget: { type: "azuredevops", userEmail: "me@example.com", repositoryId: "repo1" },
+ };
+
+ const { container } = renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expectBlockValue(container, "azuredevops.status", "azuredevops.inProgress");
+ expectBlockValue(container, "azuredevops.totalPrs", 3);
+ expectBlockValue(container, "azuredevops.myPrs", 2);
+ expectBlockValue(container, "azuredevops.approved", 1);
+ });
+
+ it("renders PR error message when PR call returns an errorCode", () => {
+ useWidgetAPI.mockImplementation((widget, endpoint) => {
+ if (endpoint === "pipeline") return { data: { value: [{ result: "succeeded" }] }, error: undefined };
+ if (endpoint === "pr") return { data: { errorCode: 1, message: "Bad PR" }, error: undefined };
+ return { data: undefined, error: undefined };
+ });
+
+ const service = { widget: { type: "azuredevops", userEmail: "me@example.com", repositoryId: "repo1" } };
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getByText("Bad PR")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/backrest/component.test.jsx b/src/widgets/backrest/component.test.jsx
new file mode 100644
index 000000000..85616c601
--- /dev/null
+++ b/src/widgets/backrest/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,
+}));
+
+import Component from "./component";
+
+describe("widgets/backrest/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("defaults widget.fields and filters placeholders down to 4 blocks while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const service = { widget: { type: "backrest" } };
+ const { container } = renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(service.widget.fields).toEqual([
+ "num_success_latest",
+ "num_failure_latest",
+ "num_failure_30",
+ "bytes_added_30",
+ ]);
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+
+ expect(screen.getByText("backrest.num_success_latest")).toBeInTheDocument();
+ expect(screen.getByText("backrest.num_failure_latest")).toBeInTheDocument();
+ expect(screen.getByText("backrest.num_failure_30")).toBeInTheDocument();
+ expect(screen.getByText("backrest.bytes_added_30")).toBeInTheDocument();
+ expect(screen.queryByText("backrest.num_plans")).toBeNull();
+ });
+
+ it("truncates widget.fields to 4", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const service = {
+ widget: { type: "backrest", fields: ["a", "b", "c", "d", "e"] },
+ };
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(service.widget.fields).toEqual(["a", "b", "c", "d"]);
+ });
+
+ it("renders values and respects field filtering", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ numPlans: 10,
+ numSuccessLatest: 1,
+ numFailureLatest: 2,
+ numSuccess30Days: 3,
+ numFailure30Days: 4,
+ bytesAdded30Days: 500,
+ },
+ error: undefined,
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ // Default fields exclude num_plans and num_success_30
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.queryByText("backrest.num_plans")).toBeNull();
+ expect(screen.queryByText("backrest.num_success_30")).toBeNull();
+
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.getByText("2")).toBeInTheDocument();
+ expect(screen.getByText("4")).toBeInTheDocument();
+ expect(screen.getByText("500")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/bazarr/component.test.jsx b/src/widgets/bazarr/component.test.jsx
new file mode 100644
index 000000000..33e03c8c3
--- /dev/null
+++ b/src/widgets/bazarr/component.test.jsx
@@ -0,0 +1,56 @@
+// @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/bazarr/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders placeholders while loading", () => {
+ useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined }));
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(2);
+ expect(screen.getByText("bazarr.missingEpisodes")).toBeInTheDocument();
+ expect(screen.getByText("bazarr.missingMovies")).toBeInTheDocument();
+ });
+
+ it("renders error UI when either endpoint errors", () => {
+ useWidgetAPI
+ .mockReturnValueOnce({ data: undefined, error: { message: "episodes bad" } })
+ .mockReturnValueOnce({ data: undefined, error: undefined });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ });
+
+ it("renders counts when loaded", () => {
+ useWidgetAPI
+ .mockReturnValueOnce({ data: { total: 11 }, error: undefined })
+ .mockReturnValueOnce({ data: { total: 22 }, error: undefined });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(2);
+ expect(screen.getByText("11")).toBeInTheDocument();
+ expect(screen.getByText("22")).toBeInTheDocument();
+ });
+});
diff --git a/vitest.setup.js b/vitest.setup.js
index 7129f114c..f26371a98 100644
--- a/vitest.setup.js
+++ b/vitest.setup.js
@@ -18,6 +18,7 @@ vi.mock("next-i18next", () => ({
if (key === "common.bbytes") return String(opts?.value ?? "");
if (key === "common.byterate") return String(opts?.value ?? "");
if (key === "common.duration") return String(opts?.value ?? "");
+ if (key === "common.ms") return String(opts?.value ?? "");
return key;
},
}),