// @vitest-environment jsdom /** * HighlightableProviderCard — the wrapper owning the back-navigation * highlight concern: per-instance highlighted-id state initialized from * history.state, ref wiring into ProviderCard's imperative handle, and * click-time navigation recording. * * Complements providerPageHighlightLogic.test.tsx (pure utils) and * providerCardHandle.test.tsx (imperative handle) by covering the wiring * between them. */ import React from "react"; import { createRoot } from "react-dom/client"; import { act } from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import HighlightableProviderCard from "@/app/(dashboard)/dashboard/providers/components/HighlightableProviderCard"; vi.mock("next-intl", () => ({ useTranslations: () => (k: string) => k })); vi.mock("@/shared/components/ProviderTestSlideOver", () => ({ default: () => null })); vi.mock("@/shared/components/ProviderIcon", () => ({ default: () => null })); // Deterministic anchor so the click path does not depend on next/link's router. vi.mock("next/link", () => ({ __esModule: true, default: React.forwardRef(function MockLink( { href, children, ...rest }: { href: string; children: React.ReactNode }, ref: React.Ref ) { return ( {children} ); }), })); // jsdom does not implement scrollIntoView or animate. Plain no-ops (not // vi.fn()) so per-test vi.spyOn never inherits call history through // vi.restoreAllMocks() restoring a shared mock. if (typeof Element.prototype.scrollIntoView === "undefined") { Object.defineProperty(Element.prototype, "scrollIntoView", { value: () => {}, writable: true, configurable: true, }); } if (typeof Element.prototype.animate === "undefined") { Object.defineProperty(Element.prototype, "animate", { value: () => ({ cancel: () => {}, finished: Promise.resolve() }), writable: true, configurable: true, }); } function renderCards(...providerIds: string[]) { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); act(() => { root.render( <> {providerIds.map((id) => ( {}} /> ))} ); }); return container; } describe("HighlightableProviderCard", () => { let container: HTMLDivElement | null = null; afterEach(() => { vi.restoreAllMocks(); window.history.replaceState(null, ""); if (container) { document.body.removeChild(container); container = null; } }); it("scrolls and highlights on mount when history.state.providerId matches", () => { window.history.replaceState({ providerId: "openai" }, ""); const scrollSpy = vi.spyOn(Element.prototype, "scrollIntoView"); const animateSpy = vi.spyOn(Element.prototype, "animate"); container = renderCards("openai"); expect(scrollSpy).toHaveBeenCalledTimes(1); expect(scrollSpy).toHaveBeenCalledWith({ behavior: "auto", block: "center" }); expect(animateSpy).toHaveBeenCalled(); }); it("does not scroll or highlight when history.state holds a different provider id", () => { window.history.replaceState({ providerId: "anthropic" }, ""); const scrollSpy = vi.spyOn(Element.prototype, "scrollIntoView"); const animateSpy = vi.spyOn(Element.prototype, "animate"); container = renderCards("openai"); expect(scrollSpy).not.toHaveBeenCalled(); expect(animateSpy).not.toHaveBeenCalled(); }); it("does nothing when history.state is null", () => { const scrollSpy = vi.spyOn(Element.prototype, "scrollIntoView"); const animateSpy = vi.spyOn(Element.prototype, "animate"); container = renderCards("openai"); expect(scrollSpy).not.toHaveBeenCalled(); expect(animateSpy).not.toHaveBeenCalled(); }); it("among several cards only the one matching history.state.providerId reacts", () => { window.history.replaceState({ providerId: "cursor" }, ""); const scrollSpy = vi.spyOn(Element.prototype, "scrollIntoView"); const animateSpy = vi.spyOn(Element.prototype, "animate"); container = renderCards("openai", "cursor", "kimi-coding"); expect(scrollSpy).toHaveBeenCalledTimes(1); expect(animateSpy).toHaveBeenCalledTimes(1); }); it("records the clicked provider id into history.state", () => { const replaceSpy = vi.spyOn(window.history, "replaceState"); container = renderCards("openai"); const link = container.querySelector("a"); expect(link).not.toBeNull(); act(() => { link!.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); }); expect(replaceSpy).toHaveBeenCalledWith({ providerId: "openai" }, ""); }); });