From a9f284548f10f95d3261390548430f5300d2463d Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:42:32 -0800 Subject: [PATCH] Fix linkwarden widget stats + add component test --- src/widgets/linkwarden/component.jsx | 36 ++++------ src/widgets/linkwarden/component.test.jsx | 81 +++++++++++++++++++++++ 2 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 src/widgets/linkwarden/component.test.jsx diff --git a/src/widgets/linkwarden/component.jsx b/src/widgets/linkwarden/component.jsx index b2b0d91a8..d2bb40eb9 100644 --- a/src/widgets/linkwarden/component.jsx +++ b/src/widgets/linkwarden/component.jsx @@ -1,35 +1,23 @@ import Block from "components/services/widget/block"; import Container from "components/services/widget/container"; -import { useEffect, useState } from "react"; import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Component({ service }) { const { widget } = service; - const [stats, setStats] = useState({ - totalLinks: null, - collections: { total: null }, - tags: { total: null }, - }); - const { data: collectionsStatsData, error: collectionsStatsError } = useWidgetAPI(widget, "collections"); const { data: tagsStatsData, error: tagsStatsError } = useWidgetAPI(widget, "tags"); - useEffect(() => { - if (collectionsStatsData?.response && tagsStatsData?.response) { - setStats({ - // eslint-disable-next-line no-underscore-dangle - totalLinks: collectionsStatsData.response.reduce((sum, collection) => sum + (collection._count?.links || 0), 0), - collections: { - total: collectionsStatsData.response.length, - }, - tags: { - total: tagsStatsData.response.length, - }, - }); - } - }, [collectionsStatsData, tagsStatsData]); + // Some APIs return raw arrays, others wrap the payload (e.g. { response: [...] }). + const collections = collectionsStatsData?.response ?? collectionsStatsData; + const tags = tagsStatsData?.response ?? tagsStatsData; + + const totalLinks = Array.isArray(collections) + ? collections.reduce((sum, collection) => sum + (collection._count?.links || 0), 0) + : null; + const collectionsTotal = Array.isArray(collections) ? collections.length : null; + const tagsTotal = Array.isArray(tags) ? tags.length : null; if (collectionsStatsError || tagsStatsError) { return ; @@ -47,9 +35,9 @@ export default function Component({ service }) { return ( - - - + + + ); } diff --git a/src/widgets/linkwarden/component.test.jsx b/src/widgets/linkwarden/component.test.jsx new file mode 100644 index 000000000..9b565a6ae --- /dev/null +++ b/src/widgets/linkwarden/component.test.jsx @@ -0,0 +1,81 @@ +// @vitest-environment jsdom + +import { screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { renderWithProviders } from "test-utils/render-with-providers"; +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.mockReturnValue({ data: undefined, error: undefined }); + + 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.mockImplementation((_widget, endpoint) => { + if (endpoint === "tags") return { data: undefined, error: { message: "nope" } }; + return { 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 totals from collections + tags arrays", async () => { + useWidgetAPI.mockImplementation((_widget, endpoint) => { + if (endpoint === "collections") { + return { + data: [ + // eslint-disable-next-line no-underscore-dangle + { _count: { links: 2 } }, + // eslint-disable-next-line no-underscore-dangle + { _count: { links: 3 } }, + ], + error: undefined, + }; + } + + if (endpoint === "tags") { + return { data: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }], error: undefined }; + } + + return { data: undefined, error: undefined }; + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expectBlockValue(container, "linkwarden.links", 5); + expectBlockValue(container, "linkwarden.collections", 2); + expectBlockValue(container, "linkwarden.tags", 4); + }); +});