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