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); + }); + }); +});