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");
+ });
+});