diff --git a/src/widgets/homebox/component.test.jsx b/src/widgets/homebox/component.test.jsx new file mode 100644 index 000000000..164247d73 --- /dev/null +++ b/src/widgets/homebox/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, { homeboxDefaultFields } 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/homebox/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("defaults fields and filters to 3 blocks while loading", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + const service = { widget: { type: "homebox", url: "http://x" } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(homeboxDefaultFields); + expect(container.querySelectorAll(".service-block")).toHaveLength(3); + expect(screen.getByText("homebox.items")).toBeInTheDocument(); + expect(screen.getByText("homebox.locations")).toBeInTheDocument(); + expect(screen.getByText("homebox.totalValue")).toBeInTheDocument(); + expect(screen.queryByText("homebox.labels")).toBeNull(); + expect(screen.queryByText("homebox.users")).toBeNull(); + expect(screen.queryByText("homebox.totalWithWarranty")).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("renders values when loaded (currency formatting delegated to i18n)", () => { + useWidgetAPI.mockReturnValue({ + data: { + items: 10, + totalWithWarranty: 2, + locations: 3, + labels: 4, + users: 5, + totalValue: 123.45, + currencyCode: "USD", + }, + error: undefined, + }); + + const { container } = renderWithProviders( + , + { + settings: { hideErrors: false }, + }, + ); + + expect(container.querySelectorAll(".service-block")).toHaveLength(3); + expectBlockValue(container, "homebox.items", 10); + expectBlockValue(container, "homebox.locations", 3); + expectBlockValue(container, "homebox.totalValue", 123.45); + }); +}); diff --git a/src/widgets/homebridge/component.test.jsx b/src/widgets/homebridge/component.test.jsx new file mode 100644 index 000000000..93b33f0e2 --- /dev/null +++ b/src/widgets/homebridge/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/homebridge/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("widget.status")).toBeInTheDocument(); + expect(screen.getByText("homebridge.updates")).toBeInTheDocument(); + expect(screen.getByText("homebridge.child_bridges")).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 update status and child bridge summary when child bridges exist", () => { + useWidgetAPI.mockReturnValue({ + data: { + status: "ok", + updateAvailable: true, + plugins: { updatesAvailable: 0 }, + childBridges: { total: 2, running: 1 }, + }, + error: undefined, + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("homebridge.ok")).toBeInTheDocument(); + expect(screen.getByText("homebridge.update_available")).toBeInTheDocument(); + // key is returned by the i18n mock; presence indicates the conditional block is rendered. + expect(screen.getByText("homebridge.child_bridges_status")).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/iframe/component.test.jsx b/src/widgets/iframe/component.test.jsx new file mode 100644 index 000000000..bec86dae3 --- /dev/null +++ b/src/widgets/iframe/component.test.jsx @@ -0,0 +1,31 @@ +// @vitest-environment jsdom + +import { describe, expect, it } from "vitest"; + +import { renderWithProviders } from "test-utils/render-with-providers"; + +import Component from "./component"; + +describe("widgets/iframe/component", () => { + it("renders an iframe with the configured src/title and classes", () => { + const service = { + widget: { + type: "iframe", + name: "My Frame", + src: "http://example.test", + classes: "h-10 w-10", + allowScrolling: "no", + }, + }; + + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + const iframe = container.querySelector("iframe"); + expect(iframe).toBeTruthy(); + expect(iframe.getAttribute("src")).toBe("http://example.test"); + expect(iframe.getAttribute("title")).toBe("My Frame"); + expect(iframe.getAttribute("name")).toBe("My Frame"); + expect(iframe.getAttribute("scrolling")).toBe("no"); + expect(iframe.className).toContain("h-10 w-10"); + }); +}); diff --git a/src/widgets/immich/component.test.jsx b/src/widgets/immich/component.test.jsx new file mode 100644 index 000000000..fa282ee05 --- /dev/null +++ b/src/widgets/immich/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/immich/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("uses v1 endpoints and renders placeholders while loading", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // version + .mockReturnValueOnce({ data: undefined, error: undefined }); // stats + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(useWidgetAPI.mock.calls[0][1]).toBe("version"); + expect(useWidgetAPI.mock.calls[1][1]).toBe("stats"); + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("immich.users")).toBeInTheDocument(); + expect(screen.getByText("immich.photos")).toBeInTheDocument(); + expect(screen.getByText("immich.videos")).toBeInTheDocument(); + expect(screen.getByText("immich.storage")).toBeInTheDocument(); + }); + + it("selects the v1 statistics endpoint when version is > 1.84", () => { + useWidgetAPI.mockReturnValueOnce({ data: { major: 1, minor: 85 }, error: undefined }).mockReturnValueOnce({ + data: { usageByUser: [{ id: 1 }, { id: 2 }], photos: 3, videos: 4, usage: "9 GiB" }, + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(useWidgetAPI.mock.calls[1][1]).toBe("statistics"); + expectBlockValue(container, "immich.users", 2); + expectBlockValue(container, "immich.photos", 3); + expectBlockValue(container, "immich.videos", 4); + expectBlockValue(container, "immich.storage", "9 GiB"); + }); + + it("uses v2 endpoints when widget.version === 2", () => { + useWidgetAPI.mockReturnValueOnce({ data: { major: 2, minor: 0 }, error: undefined }).mockReturnValueOnce({ + data: { usageByUser: [], photos: 0, videos: 0, usage: 0 }, + error: undefined, + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(useWidgetAPI.mock.calls[0][1]).toBe("version_v2"); + expect(useWidgetAPI.mock.calls[1][1]).toBe("statistics_v2"); + }); +}); diff --git a/src/widgets/jackett/component.test.jsx b/src/widgets/jackett/component.test.jsx new file mode 100644 index 000000000..99cb4ce02 --- /dev/null +++ b/src/widgets/jackett/component.test.jsx @@ -0,0 +1,71 @@ +// @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/jackett/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("jackett.configured")).toBeInTheDocument(); + expect(screen.getByText("jackett.errored")).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 configured and errored counts when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: [ + { id: 1, last_error: "" }, + { id: 2, last_error: "boom" }, + { id: 3, last_error: null }, + ], + error: undefined, + }); + + const { container } = renderWithProviders( + , + { + settings: { hideErrors: false }, + }, + ); + + expectBlockValue(container, "jackett.configured", 3); + expectBlockValue(container, "jackett.errored", 1); + }); +}); diff --git a/src/widgets/jdownloader/component.test.jsx b/src/widgets/jdownloader/component.test.jsx new file mode 100644 index 000000000..1a3fab6ff --- /dev/null +++ b/src/widgets/jdownloader/component.test.jsx @@ -0,0 +1,83 @@ +// @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/jdownloader/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("jdownloader.downloadCount")).toBeInTheDocument(); + expect(screen.getByText("jdownloader.downloadTotalBytes")).toBeInTheDocument(); + expect(screen.getByText("jdownloader.downloadBytesRemaining")).toBeInTheDocument(); + expect(screen.getByText("jdownloader.downloadSpeed")).toBeInTheDocument(); + }); + + it("calls the unified endpoint with a 30s refresh interval", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(useWidgetAPI.mock.calls[0][1]).toBe("unified"); + expect(useWidgetAPI.mock.calls[0][2]?.refreshInterval).toBe(30000); + }); + + 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 values when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: { + downloadCount: 1, + totalBytes: 100, + bytesRemaining: 40, + totalSpeed: 10, + }, + error: undefined, + }); + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expectBlockValue(container, "jdownloader.downloadCount", 1); + expectBlockValue(container, "jdownloader.downloadTotalBytes", 100); + expectBlockValue(container, "jdownloader.downloadBytesRemaining", 40); + expectBlockValue(container, "jdownloader.downloadSpeed", 10); + }); +}); diff --git a/src/widgets/jellyfin/component.test.jsx b/src/widgets/jellyfin/component.test.jsx new file mode 100644 index 000000000..b31193343 --- /dev/null +++ b/src/widgets/jellyfin/component.test.jsx @@ -0,0 +1,92 @@ +// @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/jellyfin/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders CountBlocks placeholders while loading when enableBlocks is true", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined, mutate: vi.fn() }) // sessions + .mockReturnValueOnce({ data: undefined, error: undefined }); // count + + renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(screen.getByText("jellyfin.movies")).toBeInTheDocument(); + expect(screen.getByText("jellyfin.series")).toBeInTheDocument(); + expect(screen.getByText("jellyfin.episodes")).toBeInTheDocument(); + expect(screen.getByText("jellyfin.songs")).toBeInTheDocument(); + expect(screen.getAllByText("-").length).toBeGreaterThan(0); + }); + + it("renders the no-active message when there are no playing sessions", () => { + useWidgetAPI + .mockReturnValueOnce({ data: [], error: undefined, mutate: vi.fn() }) // sessions + .mockReturnValueOnce({ + data: { MovieCount: 1, SeriesCount: 2, EpisodeCount: 3, SongCount: 4 }, + error: undefined, + }); // count + + renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(screen.getByText("jellyfin.no_active")).toBeInTheDocument(); + expect(screen.getByText("1")).toBeInTheDocument(); + expect(screen.getByText("2")).toBeInTheDocument(); + expect(screen.getByText("3")).toBeInTheDocument(); + expect(screen.getByText("4")).toBeInTheDocument(); + }); + + it("renders a single now-playing entry (expanded to two rows by default)", () => { + useWidgetAPI + .mockReturnValueOnce({ + data: [ + { + Id: "s1", + UserName: "u1", + NowPlayingItem: { Name: "Movie1", Type: "Movie", RunTimeTicks: 600000000 }, + PlayState: { PositionTicks: 0, IsPaused: false, IsMuted: false }, + TranscodingInfo: { IsVideoDirect: true }, + }, + ], + error: undefined, + mutate: vi.fn(), + }) + .mockReturnValueOnce({ + data: { MovieCount: 0, SeriesCount: 0, EpisodeCount: 0, SongCount: 0 }, + error: undefined, + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("Movie1")).toBeInTheDocument(); + // Time strings are rendered in a combined node (e.g. "00:00/01:00"). + expect(screen.getByText(/00:00/)).toBeInTheDocument(); + expect(screen.getByText(/01:00/)).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/jellyseerr/component.test.jsx b/src/widgets/jellyseerr/component.test.jsx new file mode 100644 index 000000000..16a3fccfb --- /dev/null +++ b/src/widgets/jellyseerr/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, { jellyseerrDefaultFields } from "./component"; + +describe("widgets/jellyseerr/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("defaults fields and filters to 3 blocks while loading when issues are not enabled", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // request/count + .mockReturnValueOnce({ data: undefined, error: undefined }); // issue/count disabled (endpoint = "") + + const service = { widget: { type: "jellyseerr", url: "http://x" } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(jellyseerrDefaultFields); + expect(useWidgetAPI.mock.calls[1][1]).toBe(""); + expect(container.querySelectorAll(".service-block")).toHaveLength(3); + expect(screen.getByText("jellyseerr.pending")).toBeInTheDocument(); + expect(screen.getByText("jellyseerr.approved")).toBeInTheDocument(); + expect(screen.getByText("jellyseerr.available")).toBeInTheDocument(); + expect(screen.queryByText("jellyseerr.issues")).toBeNull(); + }); + + it("renders issues when enabled (and calls the issue/count endpoint)", () => { + useWidgetAPI + .mockReturnValueOnce({ data: { pending: 1, approved: 2, available: 3 }, error: undefined }) + .mockReturnValueOnce({ data: { open: 1, total: 2 }, error: undefined }); + + const service = { + widget: { type: "jellyseerr", url: "http://x", fields: ["pending", "approved", "available", "issues"] }, + }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(useWidgetAPI.mock.calls[1][1]).toBe("issue/count"); + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("1 / 2")).toBeInTheDocument(); + }); + + it("renders error UI when issues are enabled and issue/count errors", () => { + useWidgetAPI + .mockReturnValueOnce({ data: { pending: 0, approved: 0, available: 0 }, error: undefined }) + .mockReturnValueOnce({ data: undefined, error: { message: "nope" } }); + + renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0); + expect(screen.getByText("nope")).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/karakeep/component.test.jsx b/src/widgets/karakeep/component.test.jsx new file mode 100644 index 000000000..bf6d7ca2c --- /dev/null +++ b/src/widgets/karakeep/component.test.jsx @@ -0,0 +1,79 @@ +// @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, { karakeepDefaultFields } 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/karakeep/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("defaults fields and filters to 4 blocks while loading", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + const service = { widget: { type: "karakeep", url: "http://x" } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(karakeepDefaultFields); + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("karakeep.bookmarks")).toBeInTheDocument(); + expect(screen.getByText("karakeep.favorites")).toBeInTheDocument(); + expect(screen.getByText("karakeep.archived")).toBeInTheDocument(); + expect(screen.getByText("karakeep.highlights")).toBeInTheDocument(); + expect(screen.queryByText("karakeep.lists")).toBeNull(); + expect(screen.queryByText("karakeep.tags")).toBeNull(); + }); + + it("caps widget.fields at 4 entries", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + const service = { widget: { type: "karakeep", fields: ["tags", "lists", "bookmarks", "favorites", "archived"] } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(["tags", "lists", "bookmarks", "favorites"]); + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("karakeep.tags")).toBeInTheDocument(); + expect(screen.getByText("karakeep.lists")).toBeInTheDocument(); + }); + + it("renders values when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: { + numBookmarks: 1, + numFavorites: 2, + numArchived: 3, + numHighlights: 4, + numLists: 5, + numTags: 6, + }, + error: undefined, + }); + + const { container } = renderWithProviders( + , + { + settings: { hideErrors: false }, + }, + ); + + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expectBlockValue(container, "karakeep.bookmarks", 1); + expectBlockValue(container, "karakeep.favorites", 2); + expectBlockValue(container, "karakeep.archived", 3); + expectBlockValue(container, "karakeep.highlights", 4); + }); +}); diff --git a/src/widgets/kavita/component.test.jsx b/src/widgets/kavita/component.test.jsx new file mode 100644 index 000000000..153715340 --- /dev/null +++ b/src/widgets/kavita/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/kavita/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("kavita.seriesCount")).toBeInTheDocument(); + expect(screen.getByText("kavita.totalFiles")).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 counts when loaded", () => { + useWidgetAPI.mockReturnValue({ data: { seriesCount: 12, totalFiles: 34 }, error: undefined }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("12")).toBeInTheDocument(); + expect(screen.getByText("34")).toBeInTheDocument(); + }); +});