diff --git a/src/widgets/komga/component.test.jsx b/src/widgets/komga/component.test.jsx new file mode 100644 index 000000000..4459b21bc --- /dev/null +++ b/src/widgets/komga/component.test.jsx @@ -0,0 +1,67 @@ +// @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/komga/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("komga.libraries")).toBeInTheDocument(); + expect(screen.getByText("komga.series")).toBeInTheDocument(); + expect(screen.getByText("komga.books")).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 library/series/book totals when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: { + libraries: [{ id: 1 }, { id: 2 }], + series: { totalElements: 10 }, + books: { totalElements: 20 }, + }, + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "komga.libraries", 2); + expectBlockValue(container, "komga.series", 10); + expectBlockValue(container, "komga.books", 20); + }); +}); diff --git a/src/widgets/komodo/component.test.jsx b/src/widgets/komodo/component.test.jsx new file mode 100644 index 000000000..55856eabc --- /dev/null +++ b/src/widgets/komodo/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"; + +describe("widgets/komodo/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("defaults fields for stacks view and skips containers endpoint when showStacks=true and showSummary=false", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // containers (disabled) + .mockReturnValueOnce({ data: undefined, error: undefined }) // stacks + .mockReturnValueOnce({ data: undefined, error: undefined }); // servers (disabled) + + const service = { widget: { type: "komodo", showStacks: true, showSummary: false } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(["total", "running", "down", "unhealthy"]); + expect(useWidgetAPI.mock.calls[0][1]).toBe(""); // containersEndpoint + expect(useWidgetAPI.mock.calls[1][1]).toBe("stacks"); + expect(useWidgetAPI.mock.calls[2][1]).toBe(""); // serversEndpoint + + // Default fields filter out "unknown" which is rendered but not in widget.fields. + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("komodo.total")).toBeInTheDocument(); + expect(screen.getByText("komodo.running")).toBeInTheDocument(); + expect(screen.getByText("komodo.down")).toBeInTheDocument(); + expect(screen.getByText("komodo.unhealthy")).toBeInTheDocument(); + expect(screen.queryByText("komodo.unknown")).toBeNull(); + }); + + it("renders computed down=stopped+down for stacks view", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // containers (disabled) + .mockReturnValueOnce({ + data: { total: 10, running: 7, stopped: 1, down: 2, unhealthy: 3, unknown: 4 }, + error: undefined, + }) + .mockReturnValueOnce({ data: undefined, error: undefined }); // servers (disabled) + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("10")).toBeInTheDocument(); + expect(screen.getByText("7")).toBeInTheDocument(); + const downBlock = findServiceBlockByLabel(container, "komodo.down"); + expect(downBlock).toBeTruthy(); + expect(downBlock.textContent).toContain("3"); // stopped(1) + down(2) + }); + + it("renders summary view ratios when showSummary=true", () => { + useWidgetAPI + .mockReturnValueOnce({ data: { total: 5, running: 4 }, error: undefined }) // containers + .mockReturnValueOnce({ data: { total: 2, running: 1 }, error: undefined }) // stacks + .mockReturnValueOnce({ data: { total: 1, healthy: 1 }, error: undefined }); // servers + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(container.querySelectorAll(".service-block")).toHaveLength(3); + expect(screen.getByText("1 / 1")).toBeInTheDocument(); + expect(screen.getByText("1 / 2")).toBeInTheDocument(); + expect(screen.getByText("4 / 5")).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/kopia/component.test.jsx b/src/widgets/kopia/component.test.jsx new file mode 100644 index 000000000..96cd4cb36 --- /dev/null +++ b/src/widgets/kopia/component.test.jsx @@ -0,0 +1,83 @@ +// @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"; +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/kopia/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime(new Date("2020-01-01T00:00:00Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("renders placeholders when status data is missing or source filter finds nothing", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("kopia.status")).toBeInTheDocument(); + expect(screen.getByText("kopia.size")).toBeInTheDocument(); + expect(screen.getByText("kopia.lastrun")).toBeInTheDocument(); + expect(screen.getByText("kopia.nextrun")).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 filtered snapshot status, size, and relative last/next run times", () => { + useWidgetAPI.mockReturnValue({ + data: { + sources: [ + { + source: { host: "hostA", path: "/data" }, + status: "OK", + lastSnapshot: { + startTime: "2019-12-31T22:00:00Z", // 2 hours ago + stats: { errorCount: 0, totalSize: 1024 }, + }, + nextSnapshotTime: "2020-01-01T00:30:00Z", // 30 minutes ahead + }, + ], + }, + error: undefined, + }); + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expectBlockValue(container, "kopia.status", "OK"); + expectBlockValue(container, "kopia.size", 1024); + expectBlockValue(container, "kopia.lastrun", "2 h"); + expectBlockValue(container, "kopia.nextrun", "30 m"); + }); +}); diff --git a/src/widgets/kubernetes/component.test.jsx b/src/widgets/kubernetes/component.test.jsx new file mode 100644 index 000000000..9c55178b6 --- /dev/null +++ b/src/widgets/kubernetes/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"; + +const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() })); +vi.mock("swr", () => ({ default: useSWR })); + +import Component from "./component"; + +describe("widgets/kubernetes/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders placeholders while loading", () => { + useSWR.mockReturnValue({ data: undefined, error: undefined }); + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(useSWR.mock.calls[0][0]).toContain("/api/kubernetes/status/ns/app?"); + expect(useSWR.mock.calls[1][0]).toContain("/api/kubernetes/stats/ns/app?"); + + expect(container.querySelectorAll(".service-block")).toHaveLength(2); + expect(screen.getByText("docker.cpu")).toBeInTheDocument(); + expect(screen.getByText("docker.mem")).toBeInTheDocument(); + }); + + it("renders offline status when status endpoint reports non-running state", () => { + useSWR.mockImplementation((key) => { + if (String(key).includes("/status/")) return { data: { status: "stopped" }, error: undefined }; + if (String(key).includes("/stats/")) return { data: { stats: { cpu: 0.1, mem: 10 } }, error: undefined }; + return { data: undefined, error: undefined }; + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("docker.offline")).toBeInTheDocument(); + expect(screen.getByText("widget.status")).toBeInTheDocument(); + }); + + it("renders cpu percent when cpuLimit is present, otherwise raw cpu number", () => { + useSWR.mockImplementation((key) => { + if (String(key).includes("/status/")) return { data: { status: "running" }, error: undefined }; + if (String(key).includes("/stats/")) + return { + data: { stats: { cpuLimit: true, cpuUsage: 12.3, cpu: 0.0001, mem: 1024 } }, + error: undefined, + }; + return { data: undefined, error: undefined }; + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + // With cpuLimit=true, cpuUsage is formatted via common.percent mock -> string value. + expect(screen.getByText("12.3")).toBeInTheDocument(); + expect(screen.getByText("1024")).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/lidarr/component.test.jsx b/src/widgets/lidarr/component.test.jsx new file mode 100644 index 000000000..ecbacfd38 --- /dev/null +++ b/src/widgets/lidarr/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/lidarr/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders placeholders while loading", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // artist + .mockReturnValueOnce({ data: undefined, error: undefined }) // wanted/missing + .mockReturnValueOnce({ data: undefined, error: undefined }); // queue/status + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(container.querySelectorAll(".service-block")).toHaveLength(3); + expect(screen.getByText("lidarr.wanted")).toBeInTheDocument(); + expect(screen.getByText("lidarr.queued")).toBeInTheDocument(); + expect(screen.getByText("lidarr.artists")).toBeInTheDocument(); + }); + + 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 wanted/queued/artist counts when loaded", () => { + useWidgetAPI + .mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }], error: undefined }) + .mockReturnValueOnce({ data: { totalRecords: 10 }, error: undefined }) + .mockReturnValueOnce({ data: { totalCount: 3 }, error: undefined }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "lidarr.wanted", 10); + expectBlockValue(container, "lidarr.queued", 3); + expectBlockValue(container, "lidarr.artists", 2); + }); +}); diff --git a/src/widgets/linkwarden/component.test.jsx b/src/widgets/linkwarden/component.test.jsx new file mode 100644 index 000000000..735b27d2a --- /dev/null +++ b/src/widgets/linkwarden/component.test.jsx @@ -0,0 +1,88 @@ +// @vitest-environment jsdom + +import { screen, waitFor } 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/linkwarden/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders placeholders while loading", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // collections + .mockReturnValueOnce({ data: undefined, error: undefined }); // tags + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(container.querySelectorAll(".service-block")).toHaveLength(3); + expect(screen.getByText("linkwarden.links")).toBeInTheDocument(); + expect(screen.getByText("linkwarden.collections")).toBeInTheDocument(); + expect(screen.getByText("linkwarden.tags")).toBeInTheDocument(); + }); + + it("renders error UI when either endpoint errors", () => { + useWidgetAPI + .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("computes totalLinks from collection _count.links once both endpoints are loaded", async () => { + useWidgetAPI.mockImplementation((widget, endpoint) => { + if (endpoint === "collections") { + return { + data: { + response: [ + // eslint-disable-next-line no-underscore-dangle + { _count: { links: 2 } }, + // eslint-disable-next-line no-underscore-dangle + { _count: { links: 4 } }, + ], + }, + error: undefined, + }; + } + + if (endpoint === "tags") { + return { data: { response: [{ id: 1 }, { id: 2 }, { id: 3 }] }, error: undefined }; + } + + return { data: undefined, error: undefined }; + }); + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + await waitFor(() => { + expectBlockValue(container, "linkwarden.links", 6); + }); + expectBlockValue(container, "linkwarden.collections", 2); + expectBlockValue(container, "linkwarden.tags", 3); + }); +}); diff --git a/src/widgets/lubelogger/component.test.jsx b/src/widgets/lubelogger/component.test.jsx new file mode 100644 index 000000000..2890a0f88 --- /dev/null +++ b/src/widgets/lubelogger/component.test.jsx @@ -0,0 +1,106 @@ +// @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/lubelogger/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("lubelogger.vehicles")).toBeInTheDocument(); + expect(screen.getByText("lubelogger.serviceRecords")).toBeInTheDocument(); + expect(screen.getByText("lubelogger.reminders")).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("filters to vehicleID and renders next reminder details when found", () => { + useWidgetAPI.mockReturnValue({ + data: [ + { + vehicleData: { id: 1, year: 2020, model: "Model A" }, + veryUrgentReminderCount: 1, + urgentReminderCount: 2, + notUrgentReminderCount: 3, + serviceRecordCount: 5, + nextReminder: { dueDate: 123 }, + }, + { + vehicleData: { id: 2, year: 2021, model: "Model B" }, + veryUrgentReminderCount: 0, + urgentReminderCount: 0, + notUrgentReminderCount: 0, + serviceRecordCount: 1, + nextReminder: null, + }, + ], + error: undefined, + }); + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expectBlockValue(container, "lubelogger.vehicle", "2020 Model A"); + expectBlockValue(container, "lubelogger.serviceRecords", 5); + expectBlockValue(container, "lubelogger.reminders", 6); + expectBlockValue(container, "lubelogger.nextReminder", 123); + }); + + it("shows an error when vehicleID is set but not found", () => { + useWidgetAPI.mockReturnValue({ + data: [ + { + vehicleData: { id: 2, year: 2021, model: "Model B" }, + veryUrgentReminderCount: 0, + urgentReminderCount: 0, + notUrgentReminderCount: 0, + serviceRecordCount: 0, + }, + ], + error: undefined, + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0); + expect(screen.getByText("Vehicle not found")).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/mailcow/component.test.jsx b/src/widgets/mailcow/component.test.jsx new file mode 100644 index 000000000..adc5794fb --- /dev/null +++ b/src/widgets/mailcow/component.test.jsx @@ -0,0 +1,73 @@ +// @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/mailcow/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("mailcow.mailboxes")).toBeInTheDocument(); + expect(screen.getByText("mailcow.aliases")).toBeInTheDocument(); + expect(screen.getByText("mailcow.quarantined")).toBeInTheDocument(); + }); + + it("shows a helpful error when the API returns no domains", () => { + useWidgetAPI.mockReturnValue({ data: [], error: undefined }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0); + expect(screen.getByText("No domains found")).toBeInTheDocument(); + }); + + it("renders computed totals when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: [ + { mboxes_in_domain: "2", msgs_total: "10", bytes_total: "100" }, + { mboxes_in_domain: "1", msgs_total: "5", bytes_total: "50" }, + ], + error: undefined, + }); + + const { container } = renderWithProviders( + , + { + settings: { hideErrors: false }, + }, + ); + + expectBlockValue(container, "mailcow.domains", 2); + expectBlockValue(container, "mailcow.mailboxes", 3); + expectBlockValue(container, "mailcow.mails", 15); + expectBlockValue(container, "mailcow.storage", 150); + }); +}); diff --git a/src/widgets/mastodon/component.test.jsx b/src/widgets/mastodon/component.test.jsx new file mode 100644 index 000000000..589dc6fe1 --- /dev/null +++ b/src/widgets/mastodon/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/mastodon/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("mastodon.user_count")).toBeInTheDocument(); + expect(screen.getByText("mastodon.status_count")).toBeInTheDocument(); + expect(screen.getByText("mastodon.domain_count")).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 instance stats when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: { stats: { user_count: 1, status_count: 2, domain_count: 3 } }, + error: undefined, + }); + + const { container } = renderWithProviders( + , + { + settings: { hideErrors: false }, + }, + ); + + expectBlockValue(container, "mastodon.user_count", 1); + expectBlockValue(container, "mastodon.status_count", 2); + expectBlockValue(container, "mastodon.domain_count", 3); + }); +}); diff --git a/src/widgets/mealie/component.test.jsx b/src/widgets/mealie/component.test.jsx new file mode 100644 index 000000000..7cb2fe660 --- /dev/null +++ b/src/widgets/mealie/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/mealie/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("uses v1 endpoint by default and renders placeholders while loading", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(useWidgetAPI.mock.calls[0][1]).toBe("statisticsv1"); + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("mealie.recipes")).toBeInTheDocument(); + expect(screen.getByText("mealie.users")).toBeInTheDocument(); + expect(screen.getByText("mealie.categories")).toBeInTheDocument(); + expect(screen.getByText("mealie.tags")).toBeInTheDocument(); + }); + + it("uses v2 endpoint when widget.version === 2 and renders counts", () => { + useWidgetAPI.mockReturnValue({ + data: { totalRecipes: 1, totalUsers: 2, totalCategories: 3, totalTags: 4 }, + error: undefined, + }); + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(useWidgetAPI.mock.calls[0][1]).toBe("statisticsv2"); + expectBlockValue(container, "mealie.recipes", 1); + expectBlockValue(container, "mealie.users", 2); + expectBlockValue(container, "mealie.categories", 3); + expectBlockValue(container, "mealie.tags", 4); + }); +});