Files
OmniRoute/tests/unit/ui/highlightableProviderCard.test.tsx
MumuTW 56185a8a49 refactor(dashboard): clear file-size base-red by extracting provider-card highlight wiring (#8524)
* chore(auth): condense #8407 note in tokenHealthCheck to fit frozen file-size baseline

The 3-line comment added by #8426 pushed tokenHealthCheck.ts to 843 LOC,
2 over its frozen baseline of 841 (base-red on release/v3.8.49). Fold the
devin-cli rationale into one line — same meaning, gate green again.

* refactor(dashboard): extract provider card highlight into HighlightableProviderCard

#8349 left the back-navigation highlight concern inline in providers/page.tsx
(state + two callbacks + onCardClick/ref props repeated at 18 card call
sites), pushing the page to 1990 LOC over its frozen baseline of 1927.

Move the concern into a HighlightableProviderCard wrapper: each card instance
keeps its own copy of the highlighted id and only the card whose provider id
matches scrolls into view and highlights — same semantics as the single
page-level state, since resolveHighlightedCard already gates on the id match.

page.tsx drops to 1917 LOC (under the frozen baseline, no rebaseline needed).

* test(dashboard): cover HighlightableProviderCard wiring

Direct coverage for the wrapper extracted in the previous refactor:
mount-time scroll+highlight only when history.state.providerId matches,
no reaction on mismatch/null state, isolation across multiple card
instances, and click-time navigation recording via history.replaceState.

Note: the jsdom scrollIntoView shim is a plain no-op rather than a
vi.fn() — vi.restoreAllMocks() would otherwise restore the shared mock
and let the next test's spy inherit its accumulated call count.
2026-07-26 03:52:15 -03:00

146 lines
4.9 KiB
TypeScript

// @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<HTMLAnchorElement>
) {
return (
<a href={href} ref={ref} {...rest}>
{children}
</a>
);
}),
}));
// 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) => (
<HighlightableProviderCard
key={id}
providerId={id}
provider={{ id, name: id }}
stats={{ total: 1, connected: 1, error: 0, warning: 0 }}
authType="apikey"
onToggle={() => {}}
/>
))}
</>
);
});
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" }, "");
});
});