import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createSearchDestinationController } from "./searchDestination";
describe("search destination controller", () => {
beforeEach(() => {
vi.useFakeTimers();
document.body.innerHTML = "";
Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
configurable: true,
value: vi.fn(),
});
Object.defineProperty(window, "matchMedia", {
configurable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
})),
});
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
document.body.innerHTML = "";
});
it("matches punctuation exactly and focuses the first enabled interactive descendant", () => {
const fieldId = 'extension-field\"]:not([data-safe])';
const nearMatch = document.createElement("div");
nearMatch.dataset.searchFieldId = "extension-field";
const destination = document.createElement("div");
destination.dataset.searchFieldId = fieldId;
destination.innerHTML = `
Destination summary
`;
document.body.append(nearMatch, destination);
const controller = createSearchDestinationController({
setHighlightedFieldId: vi.fn(),
initialDelayMs: 0,
});
controller.navigate(fieldId);
vi.advanceTimersByTime(0);
expect(document.activeElement).toBe(destination.querySelector("summary"));
expect(destination.scrollIntoView).toHaveBeenCalledWith({
behavior: "smooth",
block: "center",
});
});
it("retries a bounded lazy destination and falls back to a focusable wrapper", () => {
const controller = createSearchDestinationController({
setHighlightedFieldId: vi.fn(),
initialDelayMs: 10,
retryDelayMs: 10,
maxRetries: 2,
});
controller.navigate("lazy-field");
vi.advanceTimersByTime(10);
const destination = document.createElement("div");
destination.dataset.searchFieldId = "lazy-field";
document.body.append(destination);
vi.advanceTimersByTime(10);
expect(document.activeElement).toBe(destination);
expect(destination.hasAttribute("tabindex")).toBe(false);
});
it("starts the full highlight duration after a lazy destination is discovered", () => {
const highlights = vi.fn();
const controller = createSearchDestinationController({
setHighlightedFieldId: highlights,
initialDelayMs: 10,
retryDelayMs: 10,
maxRetries: 2,
highlightDurationMs: 20,
});
controller.navigate("late-field");
vi.advanceTimersByTime(10);
const destination = document.createElement("div");
destination.dataset.searchFieldId = "late-field";
document.body.append(destination);
vi.advanceTimersByTime(10);
vi.advanceTimersByTime(19);
expect(highlights.mock.calls).toEqual([[null], ["late-field"]]);
vi.advanceTimersByTime(1);
expect(highlights.mock.calls[highlights.mock.calls.length - 1]).toEqual([
null,
]);
});
it("restarts same-field navigation and cancels the previous highlight owner", () => {
const destination = document.createElement("div");
destination.dataset.searchFieldId = "repeat-field";
document.body.append(destination);
const highlights = vi.fn();
const controller = createSearchDestinationController({
setHighlightedFieldId: highlights,
initialDelayMs: 10,
highlightDurationMs: 20,
});
controller.navigate("repeat-field");
vi.advanceTimersByTime(10);
vi.advanceTimersByTime(10);
controller.navigate("repeat-field");
vi.advanceTimersByTime(10);
vi.advanceTimersByTime(19);
expect(
highlights.mock.calls.filter(([value]) => value === null),
).toHaveLength(2);
expect(destination.scrollIntoView).toHaveBeenCalledTimes(2);
vi.advanceTimersByTime(1);
expect(
highlights.mock.calls.filter(([value]) => value === null),
).toHaveLength(3);
});
it("clears the provisional highlight when bounded retries are exhausted", () => {
const highlights = vi.fn();
const controller = createSearchDestinationController({
setHighlightedFieldId: highlights,
initialDelayMs: 10,
retryDelayMs: 10,
maxRetries: 2,
});
controller.navigate("missing-field");
vi.advanceTimersByTime(30);
expect(highlights.mock.calls).toEqual([[null], ["missing-field"], [null]]);
expect(vi.getTimerCount()).toBe(0);
});
it("uses non-animated scrolling when reduced motion is requested", () => {
vi.mocked(window.matchMedia).mockImplementation(
(query: string) =>
({
matches: query === "(prefers-reduced-motion: reduce)",
media: query,
}) as MediaQueryList,
);
const destination = document.createElement("div");
destination.dataset.searchFieldId = "reduced-motion";
document.body.append(destination);
const controller = createSearchDestinationController({
setHighlightedFieldId: vi.fn(),
initialDelayMs: 0,
});
controller.navigate("reduced-motion");
vi.advanceTimersByTime(0);
expect(destination.scrollIntoView).toHaveBeenCalledWith({
behavior: "auto",
block: "center",
});
});
it("cancels pending retries and highlight cleanup when disposed", () => {
const highlights = vi.fn();
const controller = createSearchDestinationController({
setHighlightedFieldId: highlights,
initialDelayMs: 10,
retryDelayMs: 10,
highlightDurationMs: 20,
});
controller.navigate("never-mounted");
controller.dispose();
vi.runAllTimers();
expect(highlights.mock.calls).toEqual([[null], ["never-mounted"]]);
const destination = document.createElement("div");
destination.dataset.searchFieldId = "mounted";
document.body.append(destination);
controller.navigate("mounted");
vi.advanceTimersByTime(10);
controller.dispose();
vi.runAllTimers();
expect(highlights.mock.calls[highlights.mock.calls.length - 1]).toEqual([
"mounted",
]);
});
});