mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-12 15:15:58 -07:00
9aa5a79c41
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
267 lines
9.1 KiB
React
267 lines
9.1 KiB
React
// @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 <As {...props}>{content}</As>;
|
|
}
|
|
|
|
return {
|
|
Combobox: ({ onChange, children }) => (
|
|
<ComboboxContext.Provider value={{ onChange }}>
|
|
<div>{children}</div>
|
|
</ComboboxContext.Provider>
|
|
),
|
|
ComboboxInput: (props) => {
|
|
const ctx = useContext(ComboboxContext);
|
|
// real headlessui fires onChange(null) when the input is cleared
|
|
return (
|
|
<input
|
|
{...props}
|
|
onChange={(event) => {
|
|
props.onChange?.(event);
|
|
if (event.target.value === "") ctx?.onChange?.(null);
|
|
}}
|
|
/>
|
|
);
|
|
},
|
|
ComboboxOption: ({ as: As = "div", value, children, ...props }) => {
|
|
const ctx = useContext(ComboboxContext);
|
|
const content = typeof children === "function" ? children({ active: false }) : children;
|
|
return (
|
|
<As value={value} onMouseDown={() => ctx?.onChange?.(value)} {...props}>
|
|
{content}
|
|
</As>
|
|
);
|
|
},
|
|
ComboboxOptions: passthrough,
|
|
Listbox: ({ value, onChange, children, ...props }) => (
|
|
<ListboxContext.Provider value={{ value, onChange }}>
|
|
<div {...props}>{typeof children === "function" ? children({}) : children}</div>
|
|
</ListboxContext.Provider>
|
|
),
|
|
ListboxButton: (props) => <button type="button" {...props} />,
|
|
ListboxOption: ({ as: _as, value, children, ...props }) => {
|
|
const ctx = useContext(ListboxContext);
|
|
const content = typeof children === "function" ? children({ active: false }) : children;
|
|
return (
|
|
<div
|
|
role="option"
|
|
data-provider={value?.name}
|
|
aria-selected={ctx?.value === value}
|
|
onClick={() => ctx?.onChange?.(value)}
|
|
{...props}
|
|
>
|
|
{content}
|
|
</div>
|
|
);
|
|
},
|
|
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(<Search options={{ provider: ["google"], showSearchSuggestions: false, target: "_self" }} />, {
|
|
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("does not search when the input is cleared", () => {
|
|
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
|
|
|
renderWithProviders(<Search options={{ provider: ["google"], showSearchSuggestions: false, target: "_self" }} />, {
|
|
settings: {},
|
|
});
|
|
|
|
const input = screen.getByPlaceholderText("search.placeholder");
|
|
fireEvent.change(input, { target: { value: "a" } });
|
|
fireEvent.change(input, { target: { value: "" } });
|
|
|
|
expect(openSpy).not.toHaveBeenCalled();
|
|
openSpy.mockRestore();
|
|
});
|
|
|
|
it("accepts provider configured as a string", () => {
|
|
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
|
|
|
renderWithProviders(
|
|
<Search options={{ provider: "duckduckgo", showSearchSuggestions: false, target: "_self" }} />,
|
|
{
|
|
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(<Search options={{ provider: "nope", showSearchSuggestions: false }} />, {
|
|
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(
|
|
<Search options={{ provider: ["google", "duckduckgo"], showSearchSuggestions: false, target: "_self" }} />,
|
|
{
|
|
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(
|
|
<Search options={{ provider: ["google", "duckduckgo"], showSearchSuggestions: false, target: "_self" }} />,
|
|
{
|
|
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(
|
|
<Search
|
|
options={{
|
|
provider: ["custom"],
|
|
url: "https://example.com/search?q=",
|
|
showSearchSuggestions: false,
|
|
target: "_self",
|
|
}}
|
|
/>,
|
|
{ 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(
|
|
<Search options={{ provider: ["google", "duckduckgo", "brave"], showSearchSuggestions: false }} />,
|
|
{ settings: {} },
|
|
);
|
|
|
|
expect(screen.getAllByRole("option").map((option) => option.textContent)).toEqual([
|
|
"Google",
|
|
"DuckDuckGo",
|
|
"Brave",
|
|
]);
|
|
});
|
|
|
|
it("labels the search input and the provider button", () => {
|
|
renderWithProviders(<Search options={{ provider: ["google", "duckduckgo"], showSearchSuggestions: false }} />, {
|
|
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(<Search options={{ provider: ["google"], showSearchSuggestions: true, target: "_self" }} />, {
|
|
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;
|
|
});
|
|
});
|