diff --git a/src/components/bookmarks/group.test.jsx b/src/components/bookmarks/group.test.jsx index 6548a5127..02d917f4e 100644 --- a/src/components/bookmarks/group.test.jsx +++ b/src/components/bookmarks/group.test.jsx @@ -23,7 +23,7 @@ vi.mock("@headlessui/react", async () => { const DisclosurePanel = React.forwardRef(function DisclosurePanel(props, ref) { // HeadlessUI uses a boolean `static` prop; avoid forwarding it to the DOM. - const { static: isStatic, ...rest } = props; // eslint-disable-line no-unused-vars + const { static: _static, ...rest } = props; return
; }); diff --git a/src/components/quicklaunch.test.jsx b/src/components/quicklaunch.test.jsx new file mode 100644 index 000000000..1480df02d --- /dev/null +++ b/src/components/quicklaunch.test.jsx @@ -0,0 +1,84 @@ +// @vitest-environment jsdom + +import { act, fireEvent, screen, waitFor } from "@testing-library/react"; +import { useState } from "react"; +import { 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, +})); + +vi.mock("./resolvedicon", () => ({ + default: function ResolvedIconMock() { + return
; + }, +})); + +vi.mock("./widgets/search/search", () => ({ + getStoredProvider: () => null, + searchProviders: { + duckduckgo: { + name: "DuckDuckGo", + url: "https://duckduckgo.example/?q=", + suggestionUrl: "https://duckduckgo.example/ac/?q=", + }, + }, +})); + +import QuickLaunch from "./quicklaunch"; + +function Wrapper({ servicesAndBookmarks = [] } = {}) { + const [searchString, setSearchString] = useState(""); + const [isOpen, setSearching] = useState(true); + + return ( + + ); +} + +describe("components/quicklaunch", () => { + it("renders results for urls and opens the selected result on Enter", async () => { + const openSpy = vi.spyOn(window, "open").mockImplementation(() => null); + + useSWR.mockReturnValue({ data: {}, error: undefined }); + + renderWithProviders(, { + settings: { + target: "_self", + quicklaunch: { + provider: "duckduckgo", + showSearchSuggestions: false, + }, + }, + }); + + const input = screen.getByPlaceholderText("Search"); + await waitFor(() => expect(input).toHaveFocus()); + + fireEvent.change(input, { target: { value: "example.com" } }); + + expect(await screen.findByText("quicklaunch.visit URL")).toBeInTheDocument(); + expect(screen.getByText("DuckDuckGo quicklaunch.search")).toBeInTheDocument(); + + fireEvent.keyDown(input, { key: "Enter" }); + + await act(async () => { + // Close/reset schedules timeouts (200ms + 300ms); flush them to avoid state updates after cleanup. + await new Promise((r) => setTimeout(r, 350)); + }); + + expect(openSpy).toHaveBeenCalledWith("https://example.com/", "_self", "noreferrer"); + + openSpy.mockRestore(); + }); +}); diff --git a/src/components/services/widget.jsx b/src/components/services/widget.jsx index 6e3a63608..98b3bfdbf 100644 --- a/src/components/services/widget.jsx +++ b/src/components/services/widget.jsx @@ -8,8 +8,7 @@ export default function Widget({ widget, service }) { const ServiceWidget = components[widget.type]; - const fullService = Object.apply({}, service); - fullService.widget = widget; + const fullService = { ...service, widget }; if (ServiceWidget) { return ( diff --git a/src/components/services/widget.test.jsx b/src/components/services/widget.test.jsx new file mode 100644 index 000000000..ef4d233ee --- /dev/null +++ b/src/components/services/widget.test.jsx @@ -0,0 +1,38 @@ +// @vitest-environment jsdom + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("components/errorboundry", () => ({ + default: function ErrorBoundaryMock({ children }) { + return <>{children}; + }, +})); + +vi.mock("widgets/components", () => ({ + default: { + mock: function MockWidget({ service }) { + return ( +
+ {service.name}:{service.widget?.type} +
+ ); + }, + }, +})); + +import Widget from "./widget"; + +describe("components/services/widget", () => { + it("renders the mapped widget component and passes merged service.widget", () => { + render(); + + expect(screen.getByTestId("mock-service-widget")).toHaveTextContent("Svc:mock"); + }); + + it("renders a missing widget message when the type is unknown", () => { + render(); + + expect(screen.getByText("widget.missing_type")).toBeInTheDocument(); + }); +}); diff --git a/src/components/services/widget/error.test.jsx b/src/components/services/widget/error.test.jsx new file mode 100644 index 000000000..11896ca8f --- /dev/null +++ b/src/components/services/widget/error.test.jsx @@ -0,0 +1,45 @@ +// @vitest-environment jsdom + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import Error from "./error"; + +describe("components/services/widget/error", () => { + it("normalizes string errors to an object with a message", () => { + render(); + + expect(screen.getByText((_, el) => el?.textContent === "widget.api_error:")).toBeInTheDocument(); + expect(screen.getByText(/boom/)).toBeInTheDocument(); + }); + + it("normalizes numeric errors to an object with a message", () => { + render(); + + expect(screen.getByText(/Error 500/)).toBeInTheDocument(); + }); + + it("unwraps nested response errors and renders raw/data sections", () => { + render( + , + ); + + expect(screen.getByText(/inner/)).toBeInTheDocument(); + expect(screen.getByText("https://example.com")).toBeInTheDocument(); + expect(screen.getByText(/\"code\": 1/)).toBeInTheDocument(); + // Buffer.from({type:"Buffer",data:[97,98]}).toString() === "ab" + expect(screen.getByText(/ab/)).toBeInTheDocument(); + }); +}); diff --git a/src/components/services/widget/highlight-context.test.jsx b/src/components/services/widget/highlight-context.test.jsx new file mode 100644 index 000000000..6640fabe6 --- /dev/null +++ b/src/components/services/widget/highlight-context.test.jsx @@ -0,0 +1,29 @@ +// @vitest-environment jsdom + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { useContext } from "react"; + +import { BlockHighlightContext } from "./highlight-context"; + +function Reader() { + const value = useContext(BlockHighlightContext); + return
{value === null ? "null" : value}
; +} + +describe("components/services/widget/highlight-context", () => { + it("defaults to null", () => { + render(); + expect(screen.getByTestId("value")).toHaveTextContent("null"); + }); + + it("provides a value to consumers", () => { + render( + + + , + ); + expect(screen.getByTestId("value")).toHaveTextContent("on"); + }); +});