From a262e7ec5cf2cb77831bd95b0e8b6eda6a5bfbac Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Tue, 3 Feb 2026 11:47:56 -0800
Subject: [PATCH] Add widget component tests (medusa..netalertx)
---
src/widgets/medusa/component.test.jsx | 81 +++++++++++++++++++++
src/widgets/mikrotik/component.test.jsx | 82 +++++++++++++++++++++
src/widgets/minecraft/component.test.jsx | 65 +++++++++++++++++
src/widgets/miniflux/component.test.jsx | 56 +++++++++++++++
src/widgets/mjpeg/component.test.jsx | 29 ++++++++
src/widgets/moonraker/component.test.jsx | 90 ++++++++++++++++++++++++
src/widgets/mylar/component.test.jsx | 68 ++++++++++++++++++
src/widgets/myspeed/component.test.jsx | 59 ++++++++++++++++
src/widgets/navidrome/component.test.jsx | 53 ++++++++++++++
src/widgets/netalertx/component.test.jsx | 55 +++++++++++++++
10 files changed, 638 insertions(+)
create mode 100644 src/widgets/medusa/component.test.jsx
create mode 100644 src/widgets/mikrotik/component.test.jsx
create mode 100644 src/widgets/minecraft/component.test.jsx
create mode 100644 src/widgets/miniflux/component.test.jsx
create mode 100644 src/widgets/mjpeg/component.test.jsx
create mode 100644 src/widgets/moonraker/component.test.jsx
create mode 100644 src/widgets/mylar/component.test.jsx
create mode 100644 src/widgets/myspeed/component.test.jsx
create mode 100644 src/widgets/navidrome/component.test.jsx
create mode 100644 src/widgets/netalertx/component.test.jsx
diff --git a/src/widgets/medusa/component.test.jsx b/src/widgets/medusa/component.test.jsx
new file mode 100644
index 000000000..ae6bfa752
--- /dev/null
+++ b/src/widgets/medusa/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";
+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/medusa/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("medusa.wanted")).toBeInTheDocument();
+ expect(screen.getByText("medusa.queued")).toBeInTheDocument();
+ expect(screen.getByText("medusa.series")).toBeInTheDocument();
+ });
+
+ it("renders error UI when either endpoint errors", () => {
+ useWidgetAPI.mockImplementation((_widget, endpoint) => {
+ if (endpoint === "future") return { data: undefined, error: { message: "nope" } };
+ return { data: undefined, error: undefined };
+ });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ expect(screen.getByText("nope")).toBeInTheDocument();
+ });
+
+ it("computes wanted total from future lists and renders stats", () => {
+ useWidgetAPI.mockImplementation((_widget, endpoint) => {
+ if (endpoint === "future") {
+ return {
+ data: {
+ data: {
+ later: [{ id: 1 }],
+ missed: [{ id: 2 }, { id: 3 }],
+ soon: [],
+ today: [{ id: 4 }, { id: 5 }, { id: 6 }],
+ },
+ },
+ error: undefined,
+ };
+ }
+
+ if (endpoint === "stats") {
+ return { data: { data: { ep_snatched: 7, shows_active: 8 } }, error: undefined };
+ }
+
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expectBlockValue(container, "medusa.wanted", 6);
+ expectBlockValue(container, "medusa.queued", 7);
+ expectBlockValue(container, "medusa.series", 8);
+ });
+});
diff --git a/src/widgets/mikrotik/component.test.jsx b/src/widgets/mikrotik/component.test.jsx
new file mode 100644
index 000000000..87f4af44c
--- /dev/null
+++ b/src/widgets/mikrotik/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/mikrotik/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("mikrotik.uptime")).toBeInTheDocument();
+ expect(screen.getByText("mikrotik.cpuLoad")).toBeInTheDocument();
+ expect(screen.getByText("mikrotik.memoryUsed")).toBeInTheDocument();
+ expect(screen.getByText("mikrotik.numberOfLeases")).toBeInTheDocument();
+ });
+
+ it("renders error UI when either endpoint errors", () => {
+ useWidgetAPI.mockImplementation((_widget, endpoint) => {
+ if (endpoint === "leases") return { data: undefined, error: { message: "nope" } };
+ return { data: undefined, error: undefined };
+ });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ expect(screen.getByText("nope")).toBeInTheDocument();
+ });
+
+ it("renders uptime, cpu load, memory used, and lease count", () => {
+ useWidgetAPI.mockImplementation((_widget, endpoint) => {
+ if (endpoint === "system") {
+ return {
+ data: {
+ uptime: "1d",
+ "cpu-load": 10,
+ "free-memory": 25,
+ "total-memory": 100,
+ },
+ error: undefined,
+ };
+ }
+
+ if (endpoint === "leases") {
+ return { data: [{ id: 1 }, { id: 2 }, { id: 3 }], error: undefined };
+ }
+
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ // memoryUsed = 100 - (25/100)*100 = 75
+ expectBlockValue(container, "mikrotik.uptime", "1d");
+ expectBlockValue(container, "mikrotik.cpuLoad", 10);
+ expectBlockValue(container, "mikrotik.memoryUsed", 75);
+ expectBlockValue(container, "mikrotik.numberOfLeases", 3);
+ });
+});
diff --git a/src/widgets/minecraft/component.test.jsx b/src/widgets/minecraft/component.test.jsx
new file mode 100644
index 000000000..399056232
--- /dev/null
+++ b/src/widgets/minecraft/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";
+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/minecraft/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("minecraft.status")).toBeInTheDocument();
+ expect(screen.getByText("minecraft.players")).toBeInTheDocument();
+ expect(screen.getByText("minecraft.version")).toBeInTheDocument();
+ });
+
+ it("renders error UI when status endpoint errors", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ expect(screen.getByText("nope")).toBeInTheDocument();
+ });
+
+ it("renders status, players, and version when loaded", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ online: true,
+ players: { online: 2, max: 10 },
+ version: "1.20.1",
+ },
+ error: undefined,
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expectBlockValue(container, "minecraft.status", "minecraft.up");
+ expectBlockValue(container, "minecraft.players", "2 / 10");
+ expectBlockValue(container, "minecraft.version", "1.20.1");
+ });
+});
diff --git a/src/widgets/miniflux/component.test.jsx b/src/widgets/miniflux/component.test.jsx
new file mode 100644
index 000000000..0a3ec4d1c
--- /dev/null
+++ b/src/widgets/miniflux/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";
+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/miniflux/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("miniflux.unread")).toBeInTheDocument();
+ expect(screen.getByText("miniflux.read")).toBeInTheDocument();
+ });
+
+ it("renders error UI when counters endpoint errors", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ expect(screen.getByText("nope")).toBeInTheDocument();
+ });
+
+ it("renders unread and read counters when loaded", () => {
+ useWidgetAPI.mockReturnValue({ data: { unread: 3, read: 7 }, error: undefined });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expectBlockValue(container, "miniflux.unread", 3);
+ expectBlockValue(container, "miniflux.read", 7);
+ });
+});
diff --git a/src/widgets/mjpeg/component.test.jsx b/src/widgets/mjpeg/component.test.jsx
new file mode 100644
index 000000000..1246d9b89
--- /dev/null
+++ b/src/widgets/mjpeg/component.test.jsx
@@ -0,0 +1,29 @@
+// @vitest-environment jsdom
+
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+// next/image requires Next runtime features; stub it for component tests.
+vi.mock("next/image", () => ({
+ default: (props) => {
+ const { src, alt, objectFit, className, onError } = props;
+ return
;
+ },
+}));
+
+import Component from "./component";
+
+describe("widgets/mjpeg/component", () => {
+ it("renders the stream images", () => {
+ render();
+
+ const imgs = screen.getAllByAltText("stream");
+ expect(imgs).toHaveLength(2);
+ expect(imgs[0].getAttribute("src")).toBe("http://example/stream.jpg");
+ expect(imgs[1].getAttribute("src")).toBe("http://example/stream.jpg");
+
+ // Both renders pass through objectFit; the first is "fill", the second uses widget.fit.
+ expect(imgs[0].getAttribute("data-object-fit")).toBe("fill");
+ expect(imgs[1].getAttribute("data-object-fit")).toBe("cover");
+ });
+});
diff --git a/src/widgets/moonraker/component.test.jsx b/src/widgets/moonraker/component.test.jsx
new file mode 100644
index 000000000..dab81e317
--- /dev/null
+++ b/src/widgets/moonraker/component.test.jsx
@@ -0,0 +1,90 @@
+// @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/moonraker/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(1);
+ expect(screen.getByText("moonraker.printer_state")).toBeInTheDocument();
+ });
+
+ it("renders printer state as shutdown when webhook reports shutdown", () => {
+ useWidgetAPI.mockImplementation((_widget, endpoint) => {
+ if (endpoint === "print_stats") {
+ return { data: { result: { status: { print_stats: { state: "standby", info: {} } } } }, error: undefined };
+ }
+ if (endpoint === "display_status") {
+ return { data: { result: { status: { display_status: { progress: 0 } } } }, error: undefined };
+ }
+ if (endpoint === "webhooks") {
+ return { data: { result: { status: { webhooks: { state: "shutdown" } } } }, error: undefined };
+ }
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(1);
+ expectBlockValue(container, "moonraker.printer_state", "shutdown");
+ });
+
+ it("renders layers, progress and print status when active", () => {
+ useWidgetAPI.mockImplementation((_widget, endpoint) => {
+ if (endpoint === "print_stats") {
+ return {
+ data: {
+ result: {
+ status: {
+ print_stats: { state: "printing", info: { current_layer: 1, total_layer: 2 } },
+ },
+ },
+ },
+ error: undefined,
+ };
+ }
+ if (endpoint === "display_status") {
+ return { data: { result: { status: { display_status: { progress: 0.25 } } } }, error: undefined };
+ }
+ if (endpoint === "webhooks") {
+ return { data: { result: { status: { webhooks: { state: "ready" } } } }, error: undefined };
+ }
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(3);
+ expectBlockValue(container, "moonraker.layers", "1 / 2");
+ expectBlockValue(container, "moonraker.print_progress", 25);
+ expectBlockValue(container, "moonraker.print_status", "printing");
+ });
+});
diff --git a/src/widgets/mylar/component.test.jsx b/src/widgets/mylar/component.test.jsx
new file mode 100644
index 000000000..4e7846525
--- /dev/null
+++ b/src/widgets/mylar/component.test.jsx
@@ -0,0 +1,68 @@
+// @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/mylar/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("mylar.series")).toBeInTheDocument();
+ expect(screen.getByText("mylar.issues")).toBeInTheDocument();
+ expect(screen.getByText("mylar.wanted")).toBeInTheDocument();
+ });
+
+ it("renders error UI when any endpoint errors", () => {
+ useWidgetAPI.mockImplementation((_widget, endpoint) => {
+ if (endpoint === "issues") return { data: undefined, error: { message: "nope" } };
+ return { data: undefined, error: undefined };
+ });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ expect(screen.getByText("nope")).toBeInTheDocument();
+ });
+
+ it("renders series count, total issues, and wanted issues", () => {
+ useWidgetAPI.mockImplementation((_widget, endpoint) => {
+ if (endpoint === "series") return { data: { data: [{ id: 1 }, { id: 2 }] }, error: undefined };
+ if (endpoint === "issues") {
+ return { data: { data: [{ totalIssues: 3 }, { totalIssues: 4 }] }, error: undefined };
+ }
+ if (endpoint === "wanted") return { data: { issues: [{ id: 1 }] }, error: undefined };
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expectBlockValue(container, "mylar.series", 2);
+ expectBlockValue(container, "mylar.issues", 7);
+ expectBlockValue(container, "mylar.wanted", 1);
+ });
+});
diff --git a/src/widgets/myspeed/component.test.jsx b/src/widgets/myspeed/component.test.jsx
new file mode 100644
index 000000000..5bc5fb3b5
--- /dev/null
+++ b/src/widgets/myspeed/component.test.jsx
@@ -0,0 +1,59 @@
+// @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/myspeed/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("myspeed.download")).toBeInTheDocument();
+ expect(screen.getByText("myspeed.upload")).toBeInTheDocument();
+ expect(screen.getByText("myspeed.ping")).toBeInTheDocument();
+ });
+
+ it("renders error UI when endpoint errors", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ expect(screen.getByText("nope")).toBeInTheDocument();
+ });
+
+ it("renders download, upload and ping when loaded", () => {
+ useWidgetAPI.mockReturnValue({ data: [{ download: 1, upload: 2, ping: 3 }], error: undefined });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ // t("common.bitrate") returns the raw value from setup; widget multiplies by 1e6.
+ expectBlockValue(container, "myspeed.download", 1000 * 1000);
+ expectBlockValue(container, "myspeed.upload", 2 * 1000 * 1000);
+ expectBlockValue(container, "myspeed.ping", 3);
+ });
+});
diff --git a/src/widgets/navidrome/component.test.jsx b/src/widgets/navidrome/component.test.jsx
new file mode 100644
index 000000000..064b92742
--- /dev/null
+++ b/src/widgets/navidrome/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/navidrome/component", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("renders a waiting row while loading", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getByText("navidrome.please_wait")).toBeInTheDocument();
+ });
+
+ it("renders an error container when the API errors", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ expect(screen.getByText("nope")).toBeInTheDocument();
+ });
+
+ it("renders now playing entries when present", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ "subsonic-response": {
+ nowPlaying: {
+ entry: {
+ 0: { id: "a", title: "Song", artist: "Artist", album: "Album", username: "user" },
+ },
+ },
+ },
+ },
+ error: undefined,
+ });
+
+ renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(screen.getByText("Artist - Song — Album (user)")).toBeInTheDocument();
+ });
+});
diff --git a/src/widgets/netalertx/component.test.jsx b/src/widgets/netalertx/component.test.jsx
new file mode 100644
index 000000000..a4b077dfc
--- /dev/null
+++ b/src/widgets/netalertx/component.test.jsx
@@ -0,0 +1,55 @@
+// @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/netalertx/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("netalertx.total")).toBeInTheDocument();
+ expect(screen.getByText("netalertx.connected")).toBeInTheDocument();
+ expect(screen.getByText("netalertx.new_devices")).toBeInTheDocument();
+ expect(screen.getByText("netalertx.down_alerts")).toBeInTheDocument();
+ });
+
+ it("uses datav2 endpoint for version > 1 and renders parsed totals", () => {
+ useWidgetAPI.mockImplementation((_widget, endpoint) => {
+ if (endpoint === "datav2") return { data: ["10", "5", "0", "2", "1"], error: undefined };
+ return { data: undefined, error: undefined };
+ });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(useWidgetAPI).toHaveBeenCalled();
+ expectBlockValue(container, "netalertx.total", 10);
+ expectBlockValue(container, "netalertx.connected", 5);
+ expectBlockValue(container, "netalertx.new_devices", 2);
+ expectBlockValue(container, "netalertx.down_alerts", 1);
+ });
+});