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