// @vitest-environment jsdom
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
// HeadlessUI is hard to test reliably; stub the primitives to simple pass-through components.
vi.mock("@headlessui/react", async () => {
const React = await import("react");
const { Fragment, createContext, useContext } = React;
const ComboboxContext = createContext(null);
const ListboxContext = createContext(null);
function passthrough({ as: As = "div", children, ...props }) {
if (As === Fragment) return <>{typeof children === "function" ? children({ active: false }) : children}>;
const content = typeof children === "function" ? children({ active: false }) : children;
return {content};
}
return {
Combobox: ({ onChange, children }) => (
{children}
),
ComboboxInput: (props) => ,
ComboboxOption: ({ as: As = "div", value, children, ...props }) => {
const ctx = useContext(ComboboxContext);
const content = typeof children === "function" ? children({ active: false }) : children;
return (
ctx?.onChange?.(value)} {...props}>
{content}
);
},
ComboboxOptions: passthrough,
Listbox: ({ value, onChange, children, ...props }) => (
{typeof children === "function" ? children({}) : children}
),
ListboxButton: (props) => ,
ListboxOption: ({ as: _as, value, children, ...props }) => {
const ctx = useContext(ListboxContext);
const content = typeof children === "function" ? children({ active: false }) : children;
return (
ctx?.onChange?.(value)}
{...props}
>
{content}
);
},
ListboxOptions: passthrough,
Transition: ({ children }) => <>{children}>,
};
});
import Search from "./search";
describe("components/widgets/search", () => {
beforeEach(() => {
localStorage.clear();
});
it("opens a search URL when Enter is pressed", () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
renderWithProviders(, {
settings: { target: "_blank" },
});
const input = screen.getByPlaceholderText("search.placeholder");
fireEvent.change(input, { target: { value: "hello world" } });
fireEvent.keyDown(input, { key: "Enter" });
expect(openSpy).toHaveBeenCalledWith("https://www.google.com/search?q=hello%20world", "_self");
openSpy.mockRestore();
});
it("accepts provider configured as a string", () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
renderWithProviders(
,
{
settings: {},
},
);
const input = screen.getByPlaceholderText("search.placeholder");
fireEvent.change(input, { target: { value: "hello" } });
fireEvent.keyDown(input, { key: "Enter" });
expect(openSpy).toHaveBeenCalledWith("https://duckduckgo.com/?q=hello", "_self");
openSpy.mockRestore();
});
it("returns null when the configured provider list contains no supported providers", () => {
const { container } = renderWithProviders(, {
settings: {},
});
expect(container).toBeEmptyDOMElement();
});
it("stores the selected provider in localStorage when it is changed", async () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
renderWithProviders(
,
{
settings: {},
},
);
const option = document.querySelector('[data-provider="DuckDuckGo"]');
expect(option).not.toBeNull();
fireEvent.click(option);
await waitFor(() => {
expect(localStorage.getItem("search-name")).toBe("DuckDuckGo");
});
const input = screen.getByPlaceholderText("search.placeholder");
fireEvent.change(input, { target: { value: "hello" } });
fireEvent.keyDown(input, { key: "Enter" });
expect(openSpy).toHaveBeenCalledWith("https://duckduckgo.com/?q=hello", "_self");
openSpy.mockRestore();
});
it("uses a stored provider from localStorage when it is available and allowed", () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
localStorage.setItem("search-name", "DuckDuckGo");
renderWithProviders(
,
{
settings: {},
},
);
const input = screen.getByPlaceholderText("search.placeholder");
fireEvent.change(input, { target: { value: "hello" } });
fireEvent.keyDown(input, { key: "Enter" });
expect(openSpy).toHaveBeenCalledWith("https://duckduckgo.com/?q=hello", "_self");
openSpy.mockRestore();
});
it("uses a custom provider URL when the selected provider is custom", () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
renderWithProviders(
,
{ settings: {} },
);
const input = screen.getByPlaceholderText("search.placeholder");
fireEvent.change(input, { target: { value: "hello world" } });
fireEvent.keyDown(input, { key: "Enter" });
expect(openSpy).toHaveBeenCalledWith("https://example.com/search?q=hello%20world", "_self");
openSpy.mockRestore();
});
it("gives every provider option an accessible name", () => {
renderWithProviders(
,
{ settings: {} },
);
expect(screen.getAllByRole("option").map((option) => option.textContent)).toEqual([
"Google",
"DuckDuckGo",
"Brave",
]);
});
it("labels the search input and the provider button", () => {
renderWithProviders(, {
settings: {},
});
expect(screen.getByRole("textbox")).toHaveAccessibleName("search.placeholder");
expect(screen.getByRole("button")).toHaveAccessibleName(/search\.provider/);
});
it("fetches search suggestions and triggers a search when a suggestion is selected", async () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
const originalFetch = globalThis.fetch;
const fetchSpy = vi.fn(async () => ({
json: async () => ["hel", ["hello", "help", "helm", "helium", "held"]],
}));
fetch = fetchSpy;
renderWithProviders(, {
settings: {},
});
const input = screen.getByPlaceholderText("search.placeholder");
fireEvent.change(input, { target: { value: "hel" } });
await waitFor(() => {
expect(fetchSpy).toHaveBeenCalledWith(
expect.stringContaining("/api/search/searchSuggestion?query=hel&providerName=Google"),
expect.objectContaining({ signal: expect.any(AbortSignal) }),
);
});
await waitFor(() => {
expect(document.querySelector('[value="hello"]')).toBeTruthy();
});
expect(document.querySelector('[value="held"]')).toBeNull();
fireEvent.mouseDown(document.querySelector('[value="hello"]'));
expect(openSpy).toHaveBeenCalledWith("https://www.google.com/search?q=hello", "_self");
openSpy.mockRestore();
fetch = originalFetch;
});
});