From 103c20dfd640e60df3e38d0dd9588472d3c1649d Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:47:58 -0800 Subject: [PATCH] Test: 10 more widget components (B) --- src/test-utils/widget-assertions.js | 4 + src/widgets/beszel/component.test.jsx | 97 +++++++++++++++++++ src/widgets/caddy/component.test.jsx | 64 ++++++++++++ src/widgets/calendar/component.test.jsx | 89 +++++++++++++++++ src/widgets/calibreweb/component.test.jsx | 48 +++++++++ .../changedetectionio/component.test.jsx | 56 +++++++++++ .../channelsdvrserver/component.test.jsx | 55 +++++++++++ src/widgets/checkmk/component.test.jsx | 69 +++++++++++++ src/widgets/cloudflared/component.test.jsx | 68 +++++++++++++ src/widgets/coinmarketcap/component.test.jsx | 86 ++++++++++++++++ src/widgets/crowdsec/component.test.jsx | 61 ++++++++++++ vitest.setup.js | 2 + 12 files changed, 699 insertions(+) create mode 100644 src/test-utils/widget-assertions.js create mode 100644 src/widgets/beszel/component.test.jsx create mode 100644 src/widgets/caddy/component.test.jsx create mode 100644 src/widgets/calendar/component.test.jsx create mode 100644 src/widgets/calibreweb/component.test.jsx create mode 100644 src/widgets/changedetectionio/component.test.jsx create mode 100644 src/widgets/channelsdvrserver/component.test.jsx create mode 100644 src/widgets/checkmk/component.test.jsx create mode 100644 src/widgets/cloudflared/component.test.jsx create mode 100644 src/widgets/coinmarketcap/component.test.jsx create mode 100644 src/widgets/crowdsec/component.test.jsx diff --git a/src/test-utils/widget-assertions.js b/src/test-utils/widget-assertions.js new file mode 100644 index 000000000..67bd2ac44 --- /dev/null +++ b/src/test-utils/widget-assertions.js @@ -0,0 +1,4 @@ +export function findServiceBlockByLabel(container, label) { + const blocks = Array.from(container.querySelectorAll(".service-block")); + return blocks.find((b) => b.textContent?.includes(label)); +} diff --git a/src/widgets/beszel/component.test.jsx b/src/widgets/beszel/component.test.jsx new file mode 100644 index 000000000..ee6218f2d --- /dev/null +++ b/src/widgets/beszel/component.test.jsx @@ -0,0 +1,97 @@ +// @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/beszel/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders placeholders while loading (systems view)", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + const service = { widget: { type: "beszel" } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(["systems", "up"]); + expect(container.querySelectorAll(".service-block")).toHaveLength(2); + expect(screen.getByText("beszel.systems")).toBeInTheDocument(); + expect(screen.getByText("beszel.up")).toBeInTheDocument(); + }); + + it("renders system totals when loaded (systems view)", () => { + useWidgetAPI.mockReturnValue({ + data: { + totalItems: 3, + items: [{ status: "up" }, { status: "down" }, { status: "up" }], + }, + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "beszel.systems", 3); + expectBlockValue(container, "beszel.up", "2 / 3"); + }); + + it("renders selected system details and filters to 4 default fields", () => { + useWidgetAPI.mockReturnValue({ + data: { + totalItems: 1, + items: [ + { + id: "sys1", + name: "MySystem", + status: "up", + updated: 123, + info: { cpu: 10, mp: 20, dp: 30, b: 40 }, + }, + ], + }, + error: undefined, + }); + + const service = { widget: { type: "beszel", systemId: "sys1" } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(["name", "status", "cpu", "memory"]); + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + + expectBlockValue(container, "beszel.name", "MySystem"); + expectBlockValue(container, "beszel.status", "beszel.up"); + expectBlockValue(container, "beszel.cpu", 10); + expectBlockValue(container, "beszel.memory", 20); + expect(screen.queryByText("beszel.updated")).toBeNull(); + }); + + it("renders error when systemId is not found", () => { + useWidgetAPI.mockReturnValue({ + data: { totalItems: 1, items: [{ id: "sys1", name: "MySystem", status: "up", info: {} }] }, + error: undefined, + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("System with id missing not found")).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/caddy/component.test.jsx b/src/widgets/caddy/component.test.jsx new file mode 100644 index 000000000..43a060455 --- /dev/null +++ b/src/widgets/caddy/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/caddy/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("caddy.upstreams")).toBeInTheDocument(); + expect(screen.getByText("caddy.requests")).toBeInTheDocument(); + expect(screen.getByText("caddy.requests_failed")).toBeInTheDocument(); + }); + + it("renders error UI when widget API errors", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } }); + + renderWithProviders(, { settings: { hideErrors: false } }); + + expect(screen.getByText("nope")).toBeInTheDocument(); + }); + + it("computes upstream/request totals when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: [ + { num_requests: 10, fails: 1 }, + { num_requests: 5, fails: 2 }, + ], + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "caddy.upstreams", 2); + expectBlockValue(container, "caddy.requests", 15); + expectBlockValue(container, "caddy.requests_failed", 3); + }); +}); diff --git a/src/widgets/calendar/component.test.jsx b/src/widgets/calendar/component.test.jsx new file mode 100644 index 000000000..f15825488 --- /dev/null +++ b/src/widgets/calendar/component.test.jsx @@ -0,0 +1,89 @@ +// @vitest-environment jsdom + +import { screen, waitFor } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { renderWithProviders } from "test-utils/render-with-providers"; + +vi.mock("next/dynamic", () => ({ + default: () => (props) => ( +
+ ), +})); + +vi.mock("./monthly", () => ({ + default: ({ showDate }) =>
, +})); + +vi.mock("./agenda", () => ({ + default: ({ showDate }) =>
, +})); + +import Component from "./component"; + +describe("widgets/calendar/component", () => { + it("renders monthly view by default", async () => { + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByTestId("calendar-monthly")).toBeInTheDocument(); + expect(screen.queryByTestId("calendar-agenda")).toBeNull(); + + // showDate is set asynchronously in an effect; ensure it eventually resolves to a date string. + await waitFor(() => { + expect(screen.getByTestId("calendar-monthly").getAttribute("data-show")).not.toBe(""); + }); + }); + + it("renders agenda view when configured", async () => { + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByTestId("calendar-agenda")).toBeInTheDocument(); + expect(screen.queryByTestId("calendar-monthly")).toBeNull(); + + await waitFor(() => { + expect(screen.getByTestId("calendar-agenda").getAttribute("data-show")).not.toBe(""); + }); + }); + + it("loads configured integrations and passes calculated params", async () => { + renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + const integration = screen.getByTestId("calendar-integration"); + expect(integration.getAttribute("data-type")).toBe("sonarr"); + expect(integration.getAttribute("data-timezone")).toBe("UTC"); + + await waitFor(() => { + // start/end should be yyyy-MM-dd after showDate is set. + expect(integration.getAttribute("data-start")).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(integration.getAttribute("data-end")).toMatch(/^\d{4}-\d{2}-\d{2}$/); + }); + }); +}); diff --git a/src/widgets/calibreweb/component.test.jsx b/src/widgets/calibreweb/component.test.jsx new file mode 100644 index 000000000..211ce5c9d --- /dev/null +++ b/src/widgets/calibreweb/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/calibreweb/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("calibreweb.books")).toBeInTheDocument(); + expect(screen.getByText("calibreweb.authors")).toBeInTheDocument(); + expect(screen.getByText("calibreweb.categories")).toBeInTheDocument(); + expect(screen.getByText("calibreweb.series")).toBeInTheDocument(); + }); + + it("renders values when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: { books: 1, authors: 2, categories: 3, series: 4 }, + error: undefined, + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + 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/changedetectionio/component.test.jsx b/src/widgets/changedetectionio/component.test.jsx new file mode 100644 index 000000000..ef777d16c --- /dev/null +++ b/src/widgets/changedetectionio/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/changedetectionio/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("changedetectionio.diffsDetected")).toBeInTheDocument(); + expect(screen.getByText("changedetectionio.totalObserved")).toBeInTheDocument(); + }); + + it("computes diffs detected (last_changed > 0 and not viewed)", () => { + useWidgetAPI.mockReturnValue({ + data: { + a: { last_changed: 1, viewed: false }, + b: { last_changed: 0, viewed: false }, + c: { last_changed: 2, viewed: true }, + d: { last_changed: 3, viewed: false }, + }, + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "changedetectionio.totalObserved", 4); + expectBlockValue(container, "changedetectionio.diffsDetected", 2); + }); +}); diff --git a/src/widgets/channelsdvrserver/component.test.jsx b/src/widgets/channelsdvrserver/component.test.jsx new file mode 100644 index 000000000..5198999e0 --- /dev/null +++ b/src/widgets/channelsdvrserver/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/channelsdvrserver/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("channelsdvrserver.shows")).toBeInTheDocument(); + expect(screen.getByText("channelsdvrserver.recordings")).toBeInTheDocument(); + expect(screen.getByText("channelsdvrserver.scheduled")).toBeInTheDocument(); + expect(screen.getByText("channelsdvrserver.passes")).toBeInTheDocument(); + }); + + it("renders values when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: { stats: { groups: 1, files: 2, jobs: 3, rules: 4 } }, + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "channelsdvrserver.shows", 1); + expectBlockValue(container, "channelsdvrserver.recordings", 2); + expectBlockValue(container, "channelsdvrserver.scheduled", 3); + expectBlockValue(container, "channelsdvrserver.passes", 4); + }); +}); diff --git a/src/widgets/checkmk/component.test.jsx b/src/widgets/checkmk/component.test.jsx new file mode 100644 index 000000000..25e5e529b --- /dev/null +++ b/src/widgets/checkmk/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"; +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/checkmk/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("calls both endpoints with the expected query params and renders placeholders while loading", () => { + useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined })); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(useWidgetAPI).toHaveBeenNthCalledWith( + 1, + expect.any(Object), + "services_info", + expect.objectContaining({ + columns: "state", + query: '{"op": "!=", "left": "state", "right": "0"}', + }), + ); + expect(useWidgetAPI).toHaveBeenNthCalledWith( + 2, + expect.any(Object), + "hosts_info", + expect.objectContaining({ + columns: "state", + query: '{"op": "!=", "left": "state", "right": "0"}', + }), + ); + + expect(container.querySelectorAll(".service-block")).toHaveLength(2); + expect(screen.getByText("checkmk.serviceErrors")).toBeInTheDocument(); + expect(screen.getByText("checkmk.hostErrors")).toBeInTheDocument(); + }); + + it("renders counts when loaded", () => { + useWidgetAPI + .mockReturnValueOnce({ data: { value: [{}, {}] }, error: undefined }) + .mockReturnValueOnce({ data: { value: [{}] }, error: undefined }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "checkmk.serviceErrors", 2); + expectBlockValue(container, "checkmk.hostErrors", 1); + }); +}); diff --git a/src/widgets/cloudflared/component.test.jsx b/src/widgets/cloudflared/component.test.jsx new file mode 100644 index 000000000..e6904536a --- /dev/null +++ b/src/widgets/cloudflared/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/cloudflared/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("cloudflared.status")).toBeInTheDocument(); + expect(screen.getByText("cloudflared.origin_ip")).toBeInTheDocument(); + }); + + it("renders status capitalization and origin_ip from nested connections", () => { + useWidgetAPI.mockReturnValue({ + data: { + result: { status: "healthy", connections: { origin_ip: "1.2.3.4" } }, + }, + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "cloudflared.status", "Healthy"); + expectBlockValue(container, "cloudflared.origin_ip", "1.2.3.4"); + }); + + it("falls back to origin_ip from first connection entry", () => { + useWidgetAPI.mockReturnValue({ + data: { + result: { status: "down", connections: [{ origin_ip: "5.6.7.8" }] }, + }, + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "cloudflared.origin_ip", "5.6.7.8"); + }); +}); diff --git a/src/widgets/coinmarketcap/component.test.jsx b/src/widgets/coinmarketcap/component.test.jsx new file mode 100644 index 000000000..5fcce1c4f --- /dev/null +++ b/src/widgets/coinmarketcap/component.test.jsx @@ -0,0 +1,86 @@ +// @vitest-environment jsdom + +import { fireEvent, 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 })); + +// HeadlessUI dropdown is hard to test reliably; stub to a simple button. +vi.mock("components/services/dropdown", () => ({ + default: ({ value, setValue }) => ( + + ), +})); + +import Component from "./component"; + +describe("widgets/coinmarketcap/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders a configure message when no symbols/slugs are provided", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("coinmarketcap.configure")).toBeInTheDocument(); + }); + + it("renders error UI when widget API errors", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + // Error component normalizes the error into a message line we can assert on. + expect(screen.getByText("nope")).toBeInTheDocument(); + }); + + it("renders valid cryptos and updates percent change when date range changes", () => { + useWidgetAPI.mockReturnValue({ + data: { + data: { + BTC: { + id: 1, + name: "Bitcoin", + quote: { USD: { price: 30000, percent_change_1h: 1.234, percent_change_24h: -2.5 } }, + }, + ETH: { + id: 2, + name: "Ethereum", + quote: { USD: { price: 2000, percent_change_1h: null, percent_change_24h: null } }, + }, + }, + }, + error: undefined, + }); + + renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + // Only BTC is valid for 1h, ETH is filtered out due to null percent change. + expect(screen.getByTestId("cmc-dropdown")).toHaveTextContent("1h"); + expect(screen.getByText("Bitcoin")).toBeInTheDocument(); + expect(screen.queryByText("Ethereum")).toBeNull(); + expect(screen.getByText("30000")).toBeInTheDocument(); + expect(screen.getByText("1.23%")).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId("cmc-dropdown")); + expect(screen.getByTestId("cmc-dropdown")).toHaveTextContent("24h"); + expect(screen.getByText("-2.50%")).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/crowdsec/component.test.jsx b/src/widgets/crowdsec/component.test.jsx new file mode 100644 index 000000000..4c2d4f2b7 --- /dev/null +++ b/src/widgets/crowdsec/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/crowdsec/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("selects alerts24h endpoint when limit24h is enabled", () => { + useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined })); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(useWidgetAPI).toHaveBeenNthCalledWith(1, expect.any(Object), "alerts24h"); + expect(useWidgetAPI).toHaveBeenNthCalledWith(2, expect.any(Object), "bans"); + }); + + it("renders placeholders when both alerts and bans are missing", () => { + useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined })); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(container.querySelectorAll(".service-block")).toHaveLength(2); + expect(screen.getByText("crowdsec.alerts")).toBeInTheDocument(); + expect(screen.getByText("crowdsec.bans")).toBeInTheDocument(); + }); + + it("renders 0-length arrays as 0 counts", () => { + useWidgetAPI + .mockReturnValueOnce({ data: [], error: undefined }) + .mockReturnValueOnce({ data: [], error: undefined }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "crowdsec.alerts", 0); + expectBlockValue(container, "crowdsec.bans", 0); + }); +}); diff --git a/vitest.setup.js b/vitest.setup.js index f26371a98..8d5165b5a 100644 --- a/vitest.setup.js +++ b/vitest.setup.js @@ -11,6 +11,7 @@ afterEach(() => { // implement a couple of common formatters mocked in next-i18next vi.mock("next-i18next", () => ({ useTranslation: () => ({ + i18n: { language: "en" }, t: (key, opts) => { if (key === "common.number") return String(opts?.value ?? ""); if (key === "common.percent") return String(opts?.value ?? ""); @@ -19,6 +20,7 @@ vi.mock("next-i18next", () => ({ if (key === "common.byterate") return String(opts?.value ?? ""); if (key === "common.duration") return String(opts?.value ?? ""); if (key === "common.ms") return String(opts?.value ?? ""); + if (key === "common.relativeDate") return String(opts?.value ?? ""); return key; }, }),