diff --git a/src/widgets/gitea/component.test.jsx b/src/widgets/gitea/component.test.jsx new file mode 100644 index 000000000..c54267677 --- /dev/null +++ b/src/widgets/gitea/component.test.jsx @@ -0,0 +1,75 @@ +// @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/gitea/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders placeholders while loading", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // notifications + .mockReturnValueOnce({ data: undefined, error: undefined }) // issues + .mockReturnValueOnce({ data: undefined, error: undefined }); // repositories + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("gitea.notifications")).toBeInTheDocument(); + expect(screen.getByText("gitea.issues")).toBeInTheDocument(); + expect(screen.getByText("gitea.pulls")).toBeInTheDocument(); + expect(screen.getByText("gitea.repositories")).toBeInTheDocument(); + expect(screen.getAllByText("-")).toHaveLength(4); + }); + + it("renders error UI when any endpoint errors", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) + .mockReturnValueOnce({ data: undefined, error: { message: "nope" } }) + .mockReturnValueOnce({ data: undefined, error: undefined }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0); + expect(screen.getByText("nope")).toBeInTheDocument(); + }); + + it("renders computed counts when loaded", () => { + useWidgetAPI + .mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }], error: undefined }) + .mockReturnValueOnce({ + data: { issues: [{ id: 1 }], pulls: [{ id: 1 }, { id: 2 }, { id: 3 }] }, + error: undefined, + }) + .mockReturnValueOnce({ data: { data: [{ id: 1 }] }, error: undefined }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "gitea.notifications", 2); + expectBlockValue(container, "gitea.issues", 1); + expectBlockValue(container, "gitea.pulls", 3); + expectBlockValue(container, "gitea.repositories", 1); + }); +}); diff --git a/src/widgets/gitlab/component.test.jsx b/src/widgets/gitlab/component.test.jsx new file mode 100644 index 000000000..f19cbf3bc --- /dev/null +++ b/src/widgets/gitlab/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 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/gitlab/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("gitlab.groups")).toBeInTheDocument(); + expect(screen.getByText("gitlab.issues")).toBeInTheDocument(); + expect(screen.getByText("gitlab.merges")).toBeInTheDocument(); + expect(screen.getByText("gitlab.projects")).toBeInTheDocument(); + expect(screen.getAllByText("-")).toHaveLength(4); + }); + + 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); + expect(screen.getByText("nope")).toBeInTheDocument(); + }); + + it("renders counts when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: { groups_count: 1, issues_count: 2, merge_requests_count: 3, projects_count: 4 }, + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "gitlab.groups", 1); + expectBlockValue(container, "gitlab.issues", 2); + expectBlockValue(container, "gitlab.merges", 3); + expectBlockValue(container, "gitlab.projects", 4); + }); +}); diff --git a/src/widgets/glances/component.test.jsx b/src/widgets/glances/component.test.jsx new file mode 100644 index 000000000..dabe779c2 --- /dev/null +++ b/src/widgets/glances/component.test.jsx @@ -0,0 +1,59 @@ +// @vitest-environment jsdom + +import { screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { renderWithProviders } from "test-utils/render-with-providers"; + +vi.mock("./metrics/info", () => ({ default: () =>
glances-info
})); +vi.mock("./metrics/memory", () => ({ default: () =>
glances-memory
})); +vi.mock("./metrics/process", () => ({ default: () =>
glances-process
})); +vi.mock("./metrics/containers", () => ({ default: () =>
glances-containers
})); +vi.mock("./metrics/cpu", () => ({ default: () =>
glances-cpu
})); +vi.mock("./metrics/net", () => ({ default: () =>
glances-net
})); +vi.mock("./metrics/sensor", () => ({ default: () =>
glances-sensor
})); +vi.mock("./metrics/disk", () => ({ default: () =>
glances-disk
})); +vi.mock("./metrics/gpu", () => ({ default: () =>
glances-gpu
})); +vi.mock("./metrics/fs", () => ({ default: () =>
glances-fs
})); + +import Component from "./component"; + +describe("widgets/glances/component", () => { + it("routes metric=info to Info", () => { + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("glances-info")).toBeInTheDocument(); + }); + + it("routes metric=cpu to Cpu", () => { + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("glances-cpu")).toBeInTheDocument(); + }); + + it("routes metric patterns (network:, sensor:, disk:, gpu:, fs:) to their modules", () => { + const { rerender } = renderWithProviders( + , + { + settings: { hideErrors: false }, + }, + ); + expect(screen.getByText("glances-net")).toBeInTheDocument(); + + rerender(); + expect(screen.getByText("glances-sensor")).toBeInTheDocument(); + + rerender(); + expect(screen.getByText("glances-disk")).toBeInTheDocument(); + + rerender(); + expect(screen.getByText("glances-gpu")).toBeInTheDocument(); + + rerender(); + expect(screen.getByText("glances-fs")).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/gluetun/component.test.jsx b/src/widgets/gluetun/component.test.jsx new file mode 100644 index 000000000..0e6c1b35a --- /dev/null +++ b/src/widgets/gluetun/component.test.jsx @@ -0,0 +1,75 @@ +// @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/gluetun/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("defaults fields and filters to 3 blocks while loading (no port_forwarded)", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + const service = { widget: { type: "gluetun", url: "http://x" } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(["public_ip", "region", "country"]); + expect(container.querySelectorAll(".service-block")).toHaveLength(3); + expect(screen.getByText("gluetun.public_ip")).toBeInTheDocument(); + expect(screen.getByText("gluetun.region")).toBeInTheDocument(); + expect(screen.getByText("gluetun.country")).toBeInTheDocument(); + expect(screen.queryByText("gluetun.port_forwarded")).toBeNull(); + }); + + 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); + expect(screen.getByText("nope")).toBeInTheDocument(); + }); + + it("includes port_forwarded and uses the v2 endpoint when widget.version > 1", () => { + useWidgetAPI + .mockReturnValueOnce({ data: { public_ip: "1.2.3.4", region: "CA", country: "US" }, error: undefined }) + .mockReturnValueOnce({ data: { port: 12345 }, error: undefined }); + + const service = { + widget: { + type: "gluetun", + url: "http://x", + version: 2, + fields: ["public_ip", "region", "country", "port_forwarded"], + }, + }; + + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(useWidgetAPI.mock.calls[0][1]).toBe("ip"); + expect(useWidgetAPI.mock.calls[1][1]).toBe("port_forwarded_v2"); + + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expectBlockValue(container, "gluetun.public_ip", "1.2.3.4"); + expectBlockValue(container, "gluetun.region", "CA"); + expectBlockValue(container, "gluetun.country", "US"); + expectBlockValue(container, "gluetun.port_forwarded", 12345); + }); +}); diff --git a/src/widgets/gotify/component.test.jsx b/src/widgets/gotify/component.test.jsx new file mode 100644 index 000000000..b83f7cc4e --- /dev/null +++ b/src/widgets/gotify/component.test.jsx @@ -0,0 +1,70 @@ +// @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/gotify/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders placeholders while loading", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // application + .mockReturnValueOnce({ data: undefined, error: undefined }) // message + .mockReturnValueOnce({ data: undefined, error: undefined }); // client + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(container.querySelectorAll(".service-block")).toHaveLength(3); + expect(screen.getByText("gotify.apps")).toBeInTheDocument(); + expect(screen.getByText("gotify.clients")).toBeInTheDocument(); + expect(screen.getByText("gotify.messages")).toBeInTheDocument(); + expect(screen.getAllByText("-")).toHaveLength(3); + }); + + it("renders error UI when any endpoint errors", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) + .mockReturnValueOnce({ data: undefined, error: { message: "nope" } }) + .mockReturnValueOnce({ data: undefined, error: undefined }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0); + expect(screen.getByText("nope")).toBeInTheDocument(); + }); + + it("renders computed counts when loaded", () => { + useWidgetAPI + .mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }], error: undefined }) + .mockReturnValueOnce({ data: { messages: [{ id: 1 }] }, error: undefined }) + .mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }, { id: 3 }], error: undefined }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "gotify.apps", 2); + expectBlockValue(container, "gotify.clients", 3); + expectBlockValue(container, "gotify.messages", 1); + }); +}); diff --git a/src/widgets/grafana/component.test.jsx b/src/widgets/grafana/component.test.jsx new file mode 100644 index 000000000..30e39c1ea --- /dev/null +++ b/src/widgets/grafana/component.test.jsx @@ -0,0 +1,95 @@ +// @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/grafana/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders placeholders while loading (stats missing)", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // stats + .mockReturnValueOnce({ data: undefined, error: undefined }) // alerts + .mockReturnValueOnce({ data: undefined, error: undefined }); // grafana + + const { container } = renderWithProviders( + , + { + settings: { hideErrors: false }, + }, + ); + + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("grafana.dashboards")).toBeInTheDocument(); + expect(screen.getByText("grafana.datasources")).toBeInTheDocument(); + expect(screen.getByText("grafana.totalalerts")).toBeInTheDocument(); + expect(screen.getByText("grafana.alertstriggered")).toBeInTheDocument(); + }); + + it("computes triggered alerts for v1 from alert state=alerting", () => { + useWidgetAPI + .mockReturnValueOnce({ data: { dashboards: 1, datasources: 2, alerts: 3 }, error: undefined }) // stats + .mockReturnValueOnce( + { + data: [{ state: "ok" }, { state: "alerting" }, { state: "alerting" }], + error: undefined, + }, // alerts + ) + .mockReturnValueOnce({ data: [{ id: 1 }], error: undefined }); // grafana (unused) + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expectBlockValue(container, "grafana.dashboards", 1); + expectBlockValue(container, "grafana.datasources", 2); + expectBlockValue(container, "grafana.totalalerts", 3); + expectBlockValue(container, "grafana.alertstriggered", 2); + }); + + it("falls back to the secondary endpoint for v1 when the primary alerts endpoint errors", () => { + useWidgetAPI + .mockReturnValueOnce({ data: { dashboards: 0, datasources: 0, alerts: 0 }, error: undefined }) // stats + .mockReturnValueOnce({ data: undefined, error: { message: "primary down" } }) // alerts + .mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }, { id: 3 }], error: undefined }); // grafana + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + // Should not error if only the primary endpoint failed. + expect(screen.queryAllByText(/widget\.api_error/i)).toHaveLength(0); + expectBlockValue(container, "grafana.alertstriggered", 3); + }); + + it("uses the configured alerts endpoint for v2 and counts all returned alerts", () => { + useWidgetAPI + .mockReturnValueOnce({ data: { dashboards: 9, datasources: 8, alerts: 7 }, error: undefined }) // stats + .mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }], error: undefined }) // primary (custom) + .mockReturnValueOnce({ data: undefined, error: undefined }); // secondary (disabled) + + const service = { widget: { type: "grafana", url: "http://x", version: 2, alerts: "custom" } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(useWidgetAPI.mock.calls[1][1]).toBe("custom"); + expectBlockValue(container, "grafana.alertstriggered", 2); + }); +}); diff --git a/src/widgets/hdhomerun/component.test.jsx b/src/widgets/hdhomerun/component.test.jsx new file mode 100644 index 000000000..7f4ef5452 --- /dev/null +++ b/src/widgets/hdhomerun/component.test.jsx @@ -0,0 +1,80 @@ +// @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/hdhomerun/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders placeholders while loading", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // lineup + .mockReturnValueOnce({ data: undefined, error: undefined }); // status + + const { container } = renderWithProviders( + , + { + settings: { hideErrors: false }, + }, + ); + + expect(container.querySelectorAll(".service-block")).toHaveLength(2); + expect(screen.getByText("hdhomerun.channels")).toBeInTheDocument(); + expect(screen.getByText("hdhomerun.hd")).toBeInTheDocument(); + expect(screen.getAllByText("-")).toHaveLength(2); + }); + + it("caps widget.fields at 4 and filters blocks accordingly", () => { + useWidgetAPI + .mockReturnValueOnce({ + data: [{ HD: 1 }, { HD: 0 }, { HD: 1 }], + error: undefined, + }) + .mockReturnValueOnce({ + data: [ + { VctNumber: "5.1", VctName: "ABC", SignalStrengthPercent: 90 }, + { VctNumber: null, VctName: null, SignalStrengthPercent: null }, + ], + error: undefined, + }); + + const service = { + widget: { + type: "hdhomerun", + url: "http://x", + fields: ["channels", "hd", "tunerCount", "channelNumber", "signalStrength"], + }, + }; + + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(["channels", "hd", "tunerCount", "channelNumber"]); + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("hdhomerun.channels")).toBeInTheDocument(); + expect(screen.getByText("hdhomerun.hd")).toBeInTheDocument(); + expect(screen.getByText("hdhomerun.tunerCount")).toBeInTheDocument(); + expect(screen.getByText("hdhomerun.channelNumber")).toBeInTheDocument(); + expect(screen.queryByText("hdhomerun.signalStrength")).toBeNull(); + + expectBlockValue(container, "hdhomerun.channels", 3); + expectBlockValue(container, "hdhomerun.hd", 2); + expectBlockValue(container, "hdhomerun.tunerCount", "1 / 2"); + expectBlockValue(container, "hdhomerun.channelNumber", "5.1"); + }); +}); diff --git a/src/widgets/headscale/component.test.jsx b/src/widgets/headscale/component.test.jsx new file mode 100644 index 000000000..39715ec1f --- /dev/null +++ b/src/widgets/headscale/component.test.jsx @@ -0,0 +1,78 @@ +// @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/headscale/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("headscale.name")).toBeInTheDocument(); + expect(screen.getByText("headscale.address")).toBeInTheDocument(); + expect(screen.getByText("headscale.last_seen")).toBeInTheDocument(); + expect(screen.getByText("headscale.status")).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); + expect(screen.getByText("nope")).toBeInTheDocument(); + }); + + it("renders node details when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: { + node: { + givenName: "node1", + ipAddresses: ["100.64.0.1"], + lastSeen: 123, + online: true, + }, + }, + error: undefined, + }); + + const { container } = renderWithProviders( + , + { + settings: { hideErrors: false }, + }, + ); + + expectBlockValue(container, "headscale.name", "node1"); + expectBlockValue(container, "headscale.address", "100.64.0.1"); + expectBlockValue(container, "headscale.last_seen", 123); + expectBlockValue(container, "headscale.status", "headscale.online"); + }); +}); diff --git a/src/widgets/healthchecks/component.test.jsx b/src/widgets/healthchecks/component.test.jsx new file mode 100644 index 000000000..92a11f105 --- /dev/null +++ b/src/widgets/healthchecks/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/healthchecks/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("healthchecks.status")).toBeInTheDocument(); + expect(screen.getByText("healthchecks.last_ping")).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); + expect(screen.getByText("nope")).toBeInTheDocument(); + }); + + it("renders up/down counts when widget.uuid is not set", () => { + useWidgetAPI.mockReturnValue({ + data: { + checks: [{ status: "up" }, { status: "down" }, { status: "up" }, { status: "paused" }], + }, + error: undefined, + }); + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(container.querySelectorAll(".service-block")).toHaveLength(2); + expectBlockValue(container, "healthchecks.up", 2); + expectBlockValue(container, "healthchecks.down", 1); + }); + + it("renders status and never when widget.uuid is set but last_ping is missing", () => { + useWidgetAPI.mockReturnValue({ + data: { status: "up", last_ping: null, checks: [] }, + error: undefined, + }); + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(container.querySelectorAll(".service-block")).toHaveLength(2); + expectBlockValue(container, "healthchecks.status", "healthchecks.up"); + expectBlockValue(container, "healthchecks.last_ping", "healthchecks.never"); + }); +}); diff --git a/src/widgets/homeassistant/component.test.jsx b/src/widgets/homeassistant/component.test.jsx new file mode 100644 index 000000000..97d26db57 --- /dev/null +++ b/src/widgets/homeassistant/component.test.jsx @@ -0,0 +1,47 @@ +// @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/homeassistant/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + 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); + expect(screen.getByText("nope")).toBeInTheDocument(); + }); + + it("renders blocks returned from the API", () => { + useWidgetAPI.mockReturnValue({ + data: [ + { label: "ha.temp", value: "72" }, + { label: "ha.mode", value: "cool" }, + ], + error: undefined, + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("ha.temp")).toBeInTheDocument(); + expect(screen.getByText("72")).toBeInTheDocument(); + expect(screen.getByText("ha.mode")).toBeInTheDocument(); + expect(screen.getByText("cool")).toBeInTheDocument(); + }); +});