diff --git a/src/widgets/netdata/component.test.jsx b/src/widgets/netdata/component.test.jsx new file mode 100644 index 000000000..be0341a16 --- /dev/null +++ b/src/widgets/netdata/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/netdata/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("netdata.warnings")).toBeInTheDocument(); + expect(screen.getByText("netdata.criticals")).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 warning and critical alarm counts", () => { + useWidgetAPI.mockReturnValue({ + data: { alarms: { warning: 3, critical: 1 } }, + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "netdata.warnings", 3); + expectBlockValue(container, "netdata.criticals", 1); + }); +}); diff --git a/src/widgets/nextdns/component.test.jsx b/src/widgets/nextdns/component.test.jsx new file mode 100644 index 000000000..cdc661fb1 --- /dev/null +++ b/src/widgets/nextdns/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/nextdns/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders waiting status while loading", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + renderWithProviders(, { settings: { hideErrors: false } }); + + expect(screen.getByText("widget.status")).toBeInTheDocument(); + expect(screen.getByText("nextdns.wait")).toBeInTheDocument(); + }); + + it("renders no-devices status when data array is empty", () => { + useWidgetAPI.mockReturnValue({ data: { data: [] }, error: undefined }); + + renderWithProviders(, { settings: { hideErrors: false } }); + + expect(screen.getByText("nextdns.no_devices")).toBeInTheDocument(); + }); + + it("renders a block per device status with query counts", () => { + useWidgetAPI.mockReturnValue({ + data: { + data: [ + { status: "nextdns.active", queries: 10 }, + { status: "nextdns.offline", queries: 2 }, + ], + }, + error: undefined, + }); + + renderWithProviders(, { settings: { hideErrors: false } }); + + expect(screen.getByText("nextdns.active")).toBeInTheDocument(); + expect(screen.getByText("nextdns.offline")).toBeInTheDocument(); + expect(screen.getByText("10")).toBeInTheDocument(); + expect(screen.getByText("2")).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/npm/component.test.jsx b/src/widgets/npm/component.test.jsx new file mode 100644 index 000000000..b8316f584 --- /dev/null +++ b/src/widgets/npm/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/npm/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("npm.enabled")).toBeInTheDocument(); + expect(screen.getByText("npm.disabled")).toBeInTheDocument(); + expect(screen.getByText("npm.total")).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 enabled/disabled/total host counts", () => { + useWidgetAPI.mockReturnValue({ + data: [{ enabled: true }, { enabled: false }, { enabled: 1 }, { enabled: 0 }, { enabled: true }], + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "npm.enabled", 3); + expectBlockValue(container, "npm.disabled", 2); + expectBlockValue(container, "npm.total", 5); + }); +}); diff --git a/src/widgets/nzbget/component.test.jsx b/src/widgets/nzbget/component.test.jsx new file mode 100644 index 000000000..bfb076606 --- /dev/null +++ b/src/widgets/nzbget/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/nzbget/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("nzbget.rate")).toBeInTheDocument(); + expect(screen.getByText("nzbget.remaining")).toBeInTheDocument(); + expect(screen.getByText("nzbget.downloaded")).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 rate and sizes when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: { DownloadRate: 1234, RemainingSizeMB: 2, DownloadedSizeMB: 3 }, + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "nzbget.rate", 1234); + expectBlockValue(container, "nzbget.remaining", 2 * 1024 * 1024); + expectBlockValue(container, "nzbget.downloaded", 3 * 1024 * 1024); + }); +}); diff --git a/src/widgets/octoprint/component.test.jsx b/src/widgets/octoprint/component.test.jsx new file mode 100644 index 000000000..e55582be4 --- /dev/null +++ b/src/widgets/octoprint/component.test.jsx @@ -0,0 +1,76 @@ +// @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/octoprint/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders minimal placeholder while loading", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(container.querySelectorAll(".service-block")).toHaveLength(1); + expect(screen.getByText("octoprint.printer_state")).toBeInTheDocument(); + }); + + it("renders state from job_stats when printer_stats errors but job_stats is available", () => { + useWidgetAPI.mockImplementation((_widget, endpoint) => { + if (endpoint === "printer_stats") return { data: undefined, error: { message: "printer nope" } }; + if (endpoint === "job_stats") return { data: { state: "Paused" }, error: undefined }; + return { data: undefined, error: undefined }; + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "octoprint.printer_state", "Paused"); + expect(screen.queryByText("printer nope")).toBeNull(); + }); + + it("renders job completion block when printing and completion is present", () => { + useWidgetAPI.mockImplementation((_widget, endpoint) => { + if (endpoint === "printer_stats") { + return { + data: { + state: { text: "Printing" }, + temperature: { tool0: { actual: 200 }, bed: { actual: 60 } }, + }, + error: undefined, + }; + } + if (endpoint === "job_stats") return { data: { progress: { completion: 12.3456 } }, error: undefined }; + return { data: undefined, error: undefined }; + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expectBlockValue(container, "octoprint.printer_state", "Printing"); + expectBlockValue(container, "octoprint.temp_tool", "200"); + expectBlockValue(container, "octoprint.temp_bed", "60"); + expectBlockValue(container, "octoprint.job_completion", "12.35%"); + }); +}); diff --git a/src/widgets/ombi/component.test.jsx b/src/widgets/ombi/component.test.jsx new file mode 100644 index 000000000..972aa596e --- /dev/null +++ b/src/widgets/ombi/component.test.jsx @@ -0,0 +1,58 @@ +// @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/ombi/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("ombi.pending")).toBeInTheDocument(); + expect(screen.getByText("ombi.approved")).toBeInTheDocument(); + expect(screen.getByText("ombi.available")).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 request counts when loaded", () => { + useWidgetAPI.mockReturnValue({ data: { pending: 1, approved: 2, available: 3 }, error: undefined }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "ombi.pending", 1); + expectBlockValue(container, "ombi.approved", 2); + expectBlockValue(container, "ombi.available", 3); + }); +}); diff --git a/src/widgets/opendtu/component.test.jsx b/src/widgets/opendtu/component.test.jsx new file mode 100644 index 000000000..d2652986b --- /dev/null +++ b/src/widgets/opendtu/component.test.jsx @@ -0,0 +1,71 @@ +// @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/opendtu/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("opendtu.yieldDay")).toBeInTheDocument(); + expect(screen.getByText("opendtu.relativePower")).toBeInTheDocument(); + expect(screen.getByText("opendtu.absolutePower")).toBeInTheDocument(); + expect(screen.getByText("opendtu.limit")).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 totals and computed relative power", () => { + useWidgetAPI.mockReturnValue({ + data: { + total: { + YieldDay: { v: 12.4, u: "kWh" }, + Power: { v: 250, u: "W" }, + }, + inverters: [{ limit_absolute: 200 }, { limit_absolute: 300 }], + }, + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + // yieldDay is rounded and has unit appended. + expectBlockValue(container, "opendtu.yieldDay", "12kWh"); + // relative power is percent of power / totalLimit (250/500*100 = 50) + expectBlockValue(container, "opendtu.relativePower", "50"); + expectBlockValue(container, "opendtu.absolutePower", "250W"); + expectBlockValue(container, "opendtu.limit", "500W"); + }); +}); diff --git a/src/widgets/openmediavault/component.test.jsx b/src/widgets/openmediavault/component.test.jsx new file mode 100644 index 000000000..5770c32ad --- /dev/null +++ b/src/widgets/openmediavault/component.test.jsx @@ -0,0 +1,38 @@ +// @vitest-environment jsdom + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +const { ServicesGetStatus, SmartGetList, DownloaderGetDownloadList } = vi.hoisted(() => ({ + ServicesGetStatus: vi.fn(() =>
), + SmartGetList: vi.fn(() =>
), + DownloaderGetDownloadList: vi.fn(() =>
), +})); + +vi.mock("./methods/services_get_status", () => ({ default: ServicesGetStatus })); +vi.mock("./methods/smart_get_list", () => ({ default: SmartGetList })); +vi.mock("./methods/downloader_get_downloadlist", () => ({ default: DownloaderGetDownloadList })); + +import Component from "./component"; + +describe("widgets/openmediavault/component", () => { + it("routes services.getStatus method to ServicesGetStatus", () => { + render(); + expect(screen.getByTestId("services.getStatus")).toBeInTheDocument(); + }); + + it("routes smart.getListBg method to SmartGetList", () => { + render(); + expect(screen.getByTestId("smart.getListBg")).toBeInTheDocument(); + }); + + it("routes downloader.getDownloadList method to DownloaderGetDownloadList", () => { + render(); + expect(screen.getByTestId("downloader.getDownloadList")).toBeInTheDocument(); + }); + + it("returns null for unknown methods", () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/src/widgets/openwrt/component.test.jsx b/src/widgets/openwrt/component.test.jsx new file mode 100644 index 000000000..0b3dac69d --- /dev/null +++ b/src/widgets/openwrt/component.test.jsx @@ -0,0 +1,26 @@ +// @vitest-environment jsdom + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +const { Interface, System } = vi.hoisted(() => ({ + Interface: vi.fn(() =>
), + System: vi.fn(() =>
), +})); + +vi.mock("./methods/interface", () => ({ default: Interface })); +vi.mock("./methods/system", () => ({ default: System })); + +import Component from "./component"; + +describe("widgets/openwrt/component", () => { + it("renders System when interfaceName is not set", () => { + render(); + expect(screen.getByTestId("openwrt.system")).toBeInTheDocument(); + }); + + it("renders Interface when interfaceName is set", () => { + render(); + expect(screen.getByTestId("openwrt.interface")).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/opnsense/component.test.jsx b/src/widgets/opnsense/component.test.jsx new file mode 100644 index 000000000..2fa972ce0 --- /dev/null +++ b/src/widgets/opnsense/component.test.jsx @@ -0,0 +1,86 @@ +// @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/opnsense/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("opnsense.cpu")).toBeInTheDocument(); + expect(screen.getByText("opnsense.memory")).toBeInTheDocument(); + expect(screen.getByText("opnsense.wanUpload")).toBeInTheDocument(); + expect(screen.getByText("opnsense.wanDownload")).toBeInTheDocument(); + }); + + it("renders error UI when either endpoint errors", () => { + useWidgetAPI.mockImplementation((_widget, endpoint) => { + if (endpoint === "activity") 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("parses activity headers and renders WAN rx/tx for selected interface", () => { + useWidgetAPI.mockImplementation((_widget, endpoint) => { + if (endpoint === "activity") { + return { + data: { + headers: ["", "", "CPU: 75.00% idle", "Mem: 123M Active, 456M Inact, 789M Wired"], + }, + error: undefined, + }; + } + + if (endpoint === "interface") { + return { + data: { + interfaces: { + wan2: { "bytes transmitted": 1000, "bytes received": 2000 }, + wan: { "bytes transmitted": 1, "bytes received": 2 }, + }, + }, + error: undefined, + }; + } + + return { data: undefined, error: undefined }; + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "opnsense.cpu", "25.00"); + expectBlockValue(container, "opnsense.memory", "123M"); + expectBlockValue(container, "opnsense.wanUpload", 1000); + expectBlockValue(container, "opnsense.wanDownload", 2000); + }); +});