mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Clicking a provider card hitting back loses scroll position (#8349)
* clicking a provider card hitting back loses scroll position * clean up code * Rename some funcitons * minor UX updates * add null-guard in highlight() * Add providerCardHandle tests * add highlight tests. refactored code into separate utility functions * Minor change to skip a firstElementChild call - use a ref to access the Link inside ProviderCard.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { MouseEvent, ReactNode } from "react";
|
||||
import { useState } from "react";
|
||||
import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -74,6 +74,7 @@ interface ProviderCardProps {
|
||||
stats: ProviderStats;
|
||||
authType?: string;
|
||||
onToggle: (active: boolean) => void;
|
||||
onCardClick?: (id: string) => void;
|
||||
}
|
||||
|
||||
const DOT_COLORS: Record<string, string> = {
|
||||
@@ -152,17 +153,51 @@ function getStatusDisplay(
|
||||
return parts;
|
||||
}
|
||||
|
||||
export default function ProviderCard({
|
||||
providerId,
|
||||
provider,
|
||||
stats,
|
||||
authType = "apikey",
|
||||
onToggle,
|
||||
}: ProviderCardProps) {
|
||||
export type ProviderCardHandle = {
|
||||
highlight: () => void;
|
||||
getProviderId(): string;
|
||||
scrollIntoView: (options?: ScrollIntoViewOptions) => void;
|
||||
};
|
||||
|
||||
const ProviderCard = forwardRef<ProviderCardHandle, ProviderCardProps>(function ProviderCard(
|
||||
{ providerId, provider, stats, authType = "apikey", onToggle, onCardClick },
|
||||
ref
|
||||
) {
|
||||
const t = useTranslations("providers");
|
||||
const tc = useTranslations("common");
|
||||
const tp = useTranslations("miniPlayground");
|
||||
const [testExpanded, setTestExpanded] = useState<boolean>(false);
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
const linkElementRef = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getProviderId() {
|
||||
return providerId;
|
||||
},
|
||||
highlight() {
|
||||
const el = innerRef.current;
|
||||
if (!el) return;
|
||||
linkElementRef.current?.focus();
|
||||
const surface = linkElementRef.current?.firstElementChild;
|
||||
surface?.animate(
|
||||
[
|
||||
{ backgroundColor: "rgba(59,130,246,0.22)" },
|
||||
{ backgroundColor: "rgba(59,130,246,0.08)" },
|
||||
{ backgroundColor: "transparent" },
|
||||
],
|
||||
{ duration: 3000, easing: "ease-in-out" }
|
||||
);
|
||||
},
|
||||
scrollIntoView() {
|
||||
const el = innerRef.current;
|
||||
if (!el) return;
|
||||
el.scrollIntoView({ behavior: "auto", block: "center" });
|
||||
},
|
||||
}),
|
||||
[providerId, innerRef, linkElementRef]
|
||||
);
|
||||
|
||||
// Show the Test button for LLM providers (when serviceKinds includes "llm"
|
||||
// OR when the provider has no explicit serviceKinds but is a regular LLM provider
|
||||
@@ -260,9 +295,18 @@ export default function ProviderCard({
|
||||
onToggle(allDisabled);
|
||||
};
|
||||
|
||||
const handleCardClick = useCallback(() => {
|
||||
onCardClick?.(providerId);
|
||||
}, [onCardClick, providerId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<Link href={`/dashboard/providers/${providerId}`} className="group flex-1 flex flex-col">
|
||||
<div ref={innerRef} id={`provider-${providerId}`} className="flex flex-col h-full">
|
||||
<Link
|
||||
ref={linkElementRef}
|
||||
href={`/dashboard/providers/${providerId}`}
|
||||
className="group flex-1 flex flex-col focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary/60"
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
<Card
|
||||
padding="xs"
|
||||
className={`h-full flex flex-col hover:bg-black/5 dark:hover:bg-white/5 transition-colors cursor-pointer ${
|
||||
@@ -422,7 +466,7 @@ export default function ProviderCard({
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={!allDisabled}
|
||||
onChange={() => {}}
|
||||
onChange={undefined}
|
||||
title={allDisabled ? t("enableProvider") : t("disableProvider")}
|
||||
/>
|
||||
</div>
|
||||
@@ -461,4 +505,6 @@ export default function ProviderCard({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ProviderCard;
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
loadProviderPageData,
|
||||
} from "./providerPageUtils";
|
||||
import type { ProviderEntry } from "./providerPageUtils";
|
||||
import { recordProviderNavigation, resolveHighlightedCard } from "./providerPageHighlightUtils";
|
||||
import {
|
||||
readProviderDisplayModePreference,
|
||||
shouldSyncProviderDisplayMode,
|
||||
@@ -50,6 +51,7 @@ import { CategoryDot } from "./components/CategoryDot";
|
||||
import { ImportProvidersFromFileModal } from "./components/ImportProvidersFromFileModal";
|
||||
import NoAuthProvidersSection from "./components/NoAuthProvidersSection";
|
||||
import ProviderCard from "./components/ProviderCard";
|
||||
import type { ProviderCardHandle } from "./components/ProviderCard";
|
||||
import ProviderCountBadge from "./components/ProviderCountBadge";
|
||||
import ProviderSummaryCard from "./components/ProviderSummaryCard";
|
||||
import {
|
||||
@@ -205,6 +207,22 @@ export default function ProvidersPage() {
|
||||
// #4240: media-category (serviceKind) filter — composes with activeCategory,
|
||||
// search and configured-only. null = no serviceKind filter.
|
||||
const [activeServiceKind, setActiveServiceKind] = useState<string | null>(null);
|
||||
|
||||
// a highlighted card is one that we should scroll into view and highlight upon navigation
|
||||
const [highlightedProviderId, setHighlightedProviderId] = useState<string | null>(() => {
|
||||
return history.state?.providerId ?? null;
|
||||
});
|
||||
|
||||
const handleCardClick = useCallback((id: string) => {
|
||||
recordProviderNavigation(id);
|
||||
}, []);
|
||||
|
||||
const highlightedCardRef = useCallback(
|
||||
(handle: ProviderCardHandle | null) => {
|
||||
resolveHighlightedCard(handle, highlightedProviderId, () => setHighlightedProviderId(null));
|
||||
},
|
||||
[highlightedProviderId, setHighlightedProviderId]
|
||||
);
|
||||
const notify = useNotificationStore();
|
||||
const sectionCategoryAliases: Record<string, string> = {
|
||||
cloud: "cloudagent",
|
||||
@@ -918,6 +936,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(entry.providerId, entry.toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1004,6 +1025,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(providerId, toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -1078,6 +1102,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(providerId, toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1136,6 +1163,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(providerId, toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -1184,6 +1214,9 @@ export default function ProvidersPage() {
|
||||
stats={stats}
|
||||
authType="web-cookie"
|
||||
onToggle={(active) => handleToggleProvider(providerId, toggleAuthType, active)}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1232,6 +1265,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(providerId, toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -1285,6 +1321,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(providerId, toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -1351,6 +1390,9 @@ export default function ProvidersPage() {
|
||||
stats={stats}
|
||||
authType="upstream-proxy"
|
||||
onToggle={(active) => handleToggleProvider(providerId, toggleAuthType, active)}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1383,6 +1425,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(providerId, toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -1416,6 +1461,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(providerId, toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -1449,6 +1497,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(providerId, toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -1499,6 +1550,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(providerId, toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -1546,6 +1600,9 @@ export default function ProvidersPage() {
|
||||
stats={stats}
|
||||
authType="local"
|
||||
onToggle={(active) => handleToggleProvider(providerId, toggleAuthType, active)}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1592,6 +1649,9 @@ export default function ProvidersPage() {
|
||||
stats={stats}
|
||||
authType="search"
|
||||
onToggle={(active) => handleToggleProvider(providerId, toggleAuthType, active)}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1624,6 +1684,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(providerId, toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -1657,6 +1720,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(providerId, toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -1704,6 +1770,9 @@ export default function ProvidersPage() {
|
||||
stats={stats}
|
||||
authType="audio"
|
||||
onToggle={(active) => handleToggleProvider(providerId, toggleAuthType, active)}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1736,6 +1805,9 @@ export default function ProvidersPage() {
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(providerId, toggleAuthType, active)
|
||||
}
|
||||
|
||||
onCardClick={handleCardClick}
|
||||
ref={highlightedCardRef}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ProviderCardHandle } from "./components/ProviderCard";
|
||||
|
||||
/**
|
||||
* Called before navigating to a provider detail page. Persists the provider
|
||||
* id in history.state so the list page can scroll to it on back-navigation.
|
||||
*/
|
||||
export function recordProviderNavigation(id: string) {
|
||||
window.history.replaceState({ providerId: id }, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ref callback for ProviderCard. When the rendered card's provider id
|
||||
* matches the highlighted id, scrolls it into view and triggers the
|
||||
* highlight animation. Always clears the highlighted id afterward so
|
||||
* subsequent re-renders don't re-scroll.
|
||||
*/
|
||||
export function resolveHighlightedCard(
|
||||
handle: ProviderCardHandle | null,
|
||||
highlightedProviderId: string | null,
|
||||
onAfterHighlight: () => void
|
||||
) {
|
||||
if (handle?.getProviderId() === highlightedProviderId) {
|
||||
handle.scrollIntoView({ behavior: "auto", block: "center" });
|
||||
handle.highlight();
|
||||
}
|
||||
onAfterHighlight();
|
||||
}
|
||||
134
tests/unit/ui/providerCardHandle.test.tsx
Normal file
134
tests/unit/ui/providerCardHandle.test.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* ProviderCardHandle imperative API — highlight(), scrollIntoView(), getProviderId().
|
||||
*
|
||||
* NOTE on placement: see providerCardKimiPartnerAccent.test.tsx for rationale
|
||||
* on keeping tests in tests/unit/ui/ (both vitest configs discover it here).
|
||||
*/
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { act } from "react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import ProviderCard, {
|
||||
type ProviderCardHandle,
|
||||
} from "@/app/(dashboard)/dashboard/providers/components/ProviderCard";
|
||||
|
||||
vi.mock("next-intl", () => ({ useTranslations: () => (k: string) => k }));
|
||||
vi.mock("@/shared/components/ProviderTestSlideOver", () => ({ default: () => null }));
|
||||
vi.mock("@/shared/components/ProviderIcon", () => ({ default: () => null }));
|
||||
|
||||
// jsdom does not implement scrollIntoView or animate
|
||||
if (typeof Element.prototype.scrollIntoView === "undefined") {
|
||||
Object.defineProperty(Element.prototype, "scrollIntoView", {
|
||||
value: vi.fn(),
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
if (typeof Element.prototype.animate === "undefined") {
|
||||
Object.defineProperty(Element.prototype, "animate", {
|
||||
value: () => ({ cancel: () => {}, finished: Promise.resolve() }),
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
describe("ProviderCardHandle imperative API", () => {
|
||||
let container: HTMLDivElement | null = null;
|
||||
let handle: ProviderCardHandle | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
handle = null;
|
||||
if (container) {
|
||||
document.body.removeChild(container);
|
||||
container = null;
|
||||
}
|
||||
});
|
||||
|
||||
const PROVIDER_ID = "openai";
|
||||
const PROVIDER_NAME = "OpenAI";
|
||||
|
||||
function renderAndCapture() {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
act(() => {
|
||||
root.render(
|
||||
<ProviderCard
|
||||
ref={(h) => {
|
||||
handle = h;
|
||||
}}
|
||||
providerId={PROVIDER_ID}
|
||||
provider={{ id: PROVIDER_ID, name: PROVIDER_NAME }}
|
||||
stats={{ total: 1, connected: 1, error: 0, warning: 0 }}
|
||||
authType="apikey"
|
||||
onToggle={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it("getProviderId returns the providerId passed as a prop", () => {
|
||||
renderAndCapture();
|
||||
expect(handle).not.toBeNull();
|
||||
expect(handle!.getProviderId()).toBe(PROVIDER_ID);
|
||||
});
|
||||
|
||||
it("scrollIntoView calls Element.scrollIntoView on the wrapper div", () => {
|
||||
renderAndCapture();
|
||||
const spy = vi.spyOn(Element.prototype, "scrollIntoView");
|
||||
act(() => {
|
||||
handle!.scrollIntoView();
|
||||
});
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith({ behavior: "auto", block: "center" });
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("highlight calls animate on the Card surface", () => {
|
||||
renderAndCapture();
|
||||
const spy = vi.spyOn(Element.prototype, "animate");
|
||||
act(() => {
|
||||
handle!.highlight();
|
||||
});
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const keyframes = spy.mock.calls[0][0] as Keyframe[];
|
||||
expect(keyframes).toHaveLength(3);
|
||||
expect((keyframes[0] as Record<string, string>).backgroundColor).toBe("rgba(59,130,246,0.22)");
|
||||
expect((keyframes[2] as Record<string, string>).backgroundColor).toBe("transparent");
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("highlight focuses the link element", () => {
|
||||
renderAndCapture();
|
||||
const focusSpy = vi.fn();
|
||||
const link = container!.querySelector("a");
|
||||
if (link) link.focus = focusSpy;
|
||||
act(() => {
|
||||
handle!.highlight();
|
||||
});
|
||||
expect(focusSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("getProviderId returns the correct id for a different provider", () => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
let h: ProviderCardHandle | null = null;
|
||||
act(() => {
|
||||
root.render(
|
||||
<ProviderCard
|
||||
ref={(n) => {
|
||||
h = n;
|
||||
}}
|
||||
providerId="kimi-coding"
|
||||
provider={{ id: "kimi-coding", name: "Kimi Code CLI" }}
|
||||
stats={{ total: 0, connected: 0, error: 0, warning: 0 }}
|
||||
authType="oauth"
|
||||
onToggle={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(h!.getProviderId()).toBe("kimi-coding");
|
||||
});
|
||||
});
|
||||
129
tests/unit/ui/providerPageHighlightLogic.test.tsx
Normal file
129
tests/unit/ui/providerPageHighlightLogic.test.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* Tests for the page-level highlighted-card matching/clearing logic
|
||||
* extracted into providerPageHighlightUtils.ts.
|
||||
*
|
||||
* These import the real production functions — not copied code.
|
||||
*/
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProviderCardHandle } from "@/app/(dashboard)/dashboard/providers/components/ProviderCard";
|
||||
import {
|
||||
recordProviderNavigation,
|
||||
resolveHighlightedCard,
|
||||
} from "@/app/(dashboard)/dashboard/providers/providerPageHighlightUtils";
|
||||
|
||||
function createMockHandle(
|
||||
id: string,
|
||||
callbacks?: { onScroll?(): void; onHighlight?(): void }
|
||||
): ProviderCardHandle {
|
||||
return {
|
||||
getProviderId() {
|
||||
return id;
|
||||
},
|
||||
scrollIntoView() {
|
||||
callbacks?.onScroll?.();
|
||||
},
|
||||
highlight() {
|
||||
callbacks?.onHighlight?.();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveHighlightedCard", () => {
|
||||
it("calls scrollIntoView + highlight when handle matches highlighted id", () => {
|
||||
const onScroll = vi.fn();
|
||||
const onHighlight = vi.fn();
|
||||
const onAfterHighlight = vi.fn();
|
||||
const handle = createMockHandle("openai", { onScroll, onHighlight });
|
||||
|
||||
resolveHighlightedCard(handle, "openai", onAfterHighlight);
|
||||
|
||||
expect(onScroll).toHaveBeenCalledTimes(1);
|
||||
expect(onHighlight).toHaveBeenCalledTimes(1);
|
||||
expect(onAfterHighlight).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does NOT call scrollIntoView or highlight when ids do not match", () => {
|
||||
const onScroll = vi.fn();
|
||||
const onHighlight = vi.fn();
|
||||
const onAfterHighlight = vi.fn();
|
||||
const handle = createMockHandle("openai", { onScroll, onHighlight });
|
||||
|
||||
resolveHighlightedCard(handle, "anthropic", onAfterHighlight);
|
||||
|
||||
expect(onScroll).not.toHaveBeenCalled();
|
||||
expect(onHighlight).not.toHaveBeenCalled();
|
||||
expect(onAfterHighlight).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onAfterHighlight even when handle is null", () => {
|
||||
const onAfterHighlight = vi.fn();
|
||||
resolveHighlightedCard(null, "openai", onAfterHighlight);
|
||||
expect(onAfterHighlight).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onAfterHighlight even when ids do not match", () => {
|
||||
const onAfterHighlight = vi.fn();
|
||||
const handle = createMockHandle("kimi-coding");
|
||||
resolveHighlightedCard(handle, "openai", onAfterHighlight);
|
||||
expect(onAfterHighlight).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("only the matching handle triggers scroll + highlight among multiple", () => {
|
||||
const calls: string[] = [];
|
||||
const onAfterHighlight = vi.fn();
|
||||
const handles = [
|
||||
createMockHandle("openai", {
|
||||
onScroll: () => calls.push("openai-scroll"),
|
||||
onHighlight: () => calls.push("openai-highlight"),
|
||||
}),
|
||||
createMockHandle("anthropic", {
|
||||
onScroll: () => calls.push("anthropic-scroll"),
|
||||
onHighlight: () => calls.push("anthropic-highlight"),
|
||||
}),
|
||||
createMockHandle("cursor", {
|
||||
onScroll: () => calls.push("cursor-scroll"),
|
||||
onHighlight: () => calls.push("cursor-highlight"),
|
||||
}),
|
||||
];
|
||||
|
||||
resolveHighlightedCard(handles[0], "cursor", onAfterHighlight);
|
||||
resolveHighlightedCard(handles[1], "cursor", onAfterHighlight);
|
||||
resolveHighlightedCard(handles[2], "cursor", onAfterHighlight);
|
||||
|
||||
expect(calls).toEqual(["cursor-scroll", "cursor-highlight"]);
|
||||
expect(onAfterHighlight).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("recordProviderNavigation", () => {
|
||||
const originalReplaceState = window.history.replaceState;
|
||||
|
||||
afterEach(() => {
|
||||
window.history.replaceState = originalReplaceState;
|
||||
});
|
||||
|
||||
it("calls history.replaceState with the provider id", () => {
|
||||
const replaceSpy = vi.fn();
|
||||
window.history.replaceState = replaceSpy;
|
||||
|
||||
recordProviderNavigation("openai");
|
||||
|
||||
expect(replaceSpy).toHaveBeenCalledTimes(1);
|
||||
expect(replaceSpy).toHaveBeenCalledWith({ providerId: "openai" }, "");
|
||||
});
|
||||
|
||||
it("sets different provider ids on successive calls", () => {
|
||||
const replaceSpy = vi.fn();
|
||||
window.history.replaceState = replaceSpy;
|
||||
|
||||
recordProviderNavigation("openai");
|
||||
recordProviderNavigation("anthropic");
|
||||
recordProviderNavigation("kimi-coding");
|
||||
|
||||
expect(replaceSpy).toHaveBeenCalledTimes(3);
|
||||
expect(replaceSpy.mock.calls[0][0]).toEqual({ providerId: "openai" });
|
||||
expect(replaceSpy.mock.calls[1][0]).toEqual({ providerId: "anthropic" });
|
||||
expect(replaceSpy.mock.calls[2][0]).toEqual({ providerId: "kimi-coding" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user