fix(tests): vitest UI suite back to green (69 fails triaged — WS6.1) (#7127)

test:vitest:ui was advisory/parked with 70 failing tests across 30 files (of
159 total). Triaged by grouping failures by root cause instead of fixing
one-by-one:

- 15 files (use-virtual-list, use-traffic-stream, use-system-proxy-exit-guard,
  use-session-recorder, use-resizable-panels, traffic-inspector-page,
  timing-i18n, stats-tab, session-recorder-bar, same-context-filter,
  historic-session-banner, conversation-tab, conversation-tab-separators,
  cli-tools-no-mitm-tab, agent-bridge-server-card-a11y) were authored against
  node:test but live under tests/unit/ui/*.test.tsx, which vitest.config.ts
  collects but test:unit's glob (only *.test.ts) never does — orphaned. Fixed
  by switching their describe/it/beforeEach imports to "vitest".
- jsdom does not implement window.matchMedia, and several dashboard
  components read it via useTheme() (directly, or transitively through
  ProviderIcon). Added tests/_setup/vitestUiPolyfills.ts (wired into
  vitest.config.ts) with a minimal MediaQueryList polyfill — fixed
  providerCascadeNode, ProviderIcon-icon-url, CliAgentsPage, playground-studio,
  comboLiveStudio, memories-tab, home-topology-hidden, ProxyRegistryManager-tdz.
- playground-build-tab.test.tsx (9 tests) and compressionHub*.test.tsx (2
  tests) asserted against pre-redesign UI: BuildTab now sits behind a 3-step
  BuildWizard (mode picker -> configure -> run), and CompressionHub is a
  Phase-2 thin overview without the old master toggle/mode selector/pipeline
  list. Rewrote the build-tab test to drive the wizard, and removed the two
  compressionHub.test.tsx assertions already superseded by
  compressionHub-active-selector.test.tsx. compressionHub-context-editing.test.tsx
  asserted stale Portuguese copy against a component that deliberately uses
  literal English strings (documented hydration workaround) — aligned to the
  real text.
- search-tools-compare-tab.test.tsx: the D22 4-provider cap documented in
  docs/frameworks/SEARCH_TOOLS_STUDIO.md was never implemented in CompareTab —
  fixed the component (disable extra toggles + cap selectAll + warning
  message) since the test was correct and the component was the bug. Also
  fixed an assertion looking for a <table> that never existed (the results
  panel is a div-based side-by-side layout).
- CliAgentsPage.test.tsx: the agent-tool catalog grew from 6 to 8 (omp, letta
  added) since the test was written — updated the fixture and expected count.
- memories-tab.test.tsx: a call-order-dependent fetch mock
  (mockResolvedValueOnce + fallback) broke once MemoriesTab started firing an
  immediate health check that raced its 300ms-debounced list fetch — switched
  to a URL-keyed mock like the rest of the file.
- home-topology-hidden-4596.test.tsx: useLiveDashboard now runs an async
  handshake fetch before opening the WebSocket — stubbed fetch and awaited it.
- same-context-filter.test.tsx: the filter branch moved from
  useTrafficStream.applyFilter into the extracted, reusable
  matchesTrafficFilter() helper — updated the source-grep target.
- tests/unit/ui/provider-plan-config.test.tsx deleted: it tested
  ProviderPlanConfigClient, which tests/unit/quota-plans-route-retired.test.ts
  proves was deliberately retired (Plans screen removed).

Result: test:vitest:ui 158/158 files, 870/870 tests passing (was 30 failed /
159, 70 failed / 743). test:vitest (MCP/autoCombo) still green at 28/28,
253/253. Not promoted to blocking in this PR per the task — the owner
promotes after reviewing the green suite.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-14 16:24:19 -03:00
committed by GitHub
parent 9e8aeab7c6
commit a5cad5ab2a
30 changed files with 229 additions and 233 deletions

View File

@@ -0,0 +1 @@
- chore(tests): fix all 70 failing `test:vitest:ui` tests across 30 files (was advisory/parked) — root causes were 15 node:test-authored `.tsx` files never collected by vitest, a missing `window.matchMedia` jsdom polyfill, stale assertions against a redesigned BuildTab wizard / CompressionHub Phase-2 UI, and one obsolete test for a retired Plans screen; suite is now 158/158 files, 870/870 tests green (promotion to blocking is a follow-up)

View File

@@ -5,6 +5,8 @@ import { useTranslations } from "next-intl";
import Link from "next/link";
import type { SearchProviderCatalogItem } from "@/shared/schemas/searchTools";
const MAX_COMPARE_PROVIDERS = 4; // D22: cap at 4 providers running in parallel
export interface CompareResult {
provider: string;
latency: number;
@@ -69,12 +71,15 @@ export default function CompareTab({ providers, onMetrics }: CompareTabProps) {
const toggleProvider = useCallback((id: string) => {
setSelectedProviderIds((prev) => {
if (prev.includes(id)) return prev.filter((p) => p !== id);
if (prev.length >= MAX_COMPARE_PROVIDERS) return prev;
return [...prev, id];
});
}, []);
const selectAll = useCallback(() => {
setSelectedProviderIds(activeSearchProviders.map((p) => p.id));
setSelectedProviderIds(
activeSearchProviders.slice(0, MAX_COMPARE_PROVIDERS).map((p) => p.id)
);
}, [activeSearchProviders]);
const clearAll = useCallback(() => {
@@ -230,7 +235,7 @@ export default function CompareTab({ providers, onMetrics }: CompareTabProps) {
</button>
</div>
{/* Provider picker — no cap */}
{/* Provider picker — capped at MAX_COMPARE_PROVIDERS (D22) */}
<div>
<div className="flex items-center justify-between mb-2">
<p className="text-[10px] text-text-muted">
@@ -253,9 +258,15 @@ export default function CompareTab({ providers, onMetrics }: CompareTabProps) {
</button>
</div>
</div>
{selectedProviderIds.length >= MAX_COMPARE_PROVIDERS && (
<p className="text-warning text-[10px] mb-2">
Maximum of {MAX_COMPARE_PROVIDERS} providers can be compared at once.
</p>
)}
<div className="flex flex-wrap gap-2">
{activeSearchProviders.map((p) => {
const selected = selectedProviderIds.includes(p.id);
const atCap = !selected && selectedProviderIds.length >= MAX_COMPARE_PROVIDERS;
return (
<button
key={p.id}
@@ -264,8 +275,10 @@ export default function CompareTab({ providers, onMetrics }: CompareTabProps) {
selected
? "bg-primary/15 text-primary border-primary/30"
: "text-text-muted border-border hover:text-text-main hover:border-primary/30",
atCap ? "opacity-40 cursor-not-allowed" : "",
].join(" ")}
onClick={() => toggleProvider(p.id)}
disabled={atCap}
data-testid={`provider-toggle-${p.id}`}
aria-pressed={selected}
>

View File

@@ -0,0 +1,23 @@
// jsdom (unlike real browsers) does not implement `window.matchMedia`. Several
// dashboard components read the OS color-scheme preference via
// `window.matchMedia("(prefers-color-scheme: dark)")` (see
// `src/shared/hooks/useTheme.ts`), so any test that mounts a component using
// that hook (directly or transitively, e.g. via `ProviderIcon`) crashes with
// `TypeError: window.matchMedia is not a function` unless this polyfill runs
// first. Keep this minimal — it only needs to satisfy the subset of the
// MediaQueryList API this codebase actually calls.
if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
window.matchMedia = (query: string): MediaQueryList => {
const mql = {
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
};
return mql as unknown as MediaQueryList;
};
}

View File

@@ -54,12 +54,18 @@ const { default: CliAgentsPageClient } = await import(
// ── Fixtures ──────────────────────────────────────────────────────────────────
/** 6 agent tool ids from the catalog (§3.2 of plan-14) */
/**
* Agent tool ids from the catalog (§3.2 of plan-14, category: "agent" in
* src/shared/constants/cliTools.ts). "omp" and "letta" were added to the
* catalog after plan-14 shipped, bringing the count from 6 to 8.
*/
const AGENT_IDS = [
"openclaw",
"hermes-agent",
"goose",
"interpreter",
"omp",
"letta",
"warp",
"agent-deck",
] as const;
@@ -144,9 +150,9 @@ describe("CliAgentsPageClient", () => {
expect(container.textContent).toContain("pageTitle");
}, 15000);
it("2. renders exactly 6 agent tool cards", async () => {
it("2. renders exactly 8 agent tool cards", async () => {
const container = await renderPage();
expect(countAgentCards(container)).toBe(6);
expect(countAgentCards(container)).toBe(8);
}, 15000);
it("3. search filter — 'hermes' shows 1 card (hermes-agent)", async () => {

View File

@@ -2,7 +2,7 @@
* a11y tests for AgentBridgeServerCard — each action button must have aria-label.
* Uses source-text inspection (no JSDOM render needed) for the structural assertion.
*/
import { describe, it } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

View File

@@ -4,7 +4,7 @@
*
* Uses source-text inspection — no JSDOM render needed.
*/
import { describe, it } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

View File

@@ -139,8 +139,11 @@ describe("CompressionHub — Context Editing", () => {
});
await flush();
// CompressionHub deliberately does NOT use useTranslations (see the
// hydration note at the top of CompressionHub.tsx) — its strings are
// literal English text, exactly like EngineConfigPage.
const text = container.textContent ?? "";
expect(text).toContain("Compressão delegada ao provedor");
expect(text).toContain("Provider-delegated compression");
expect(text).toContain("Context Editing (Claude)");
});
@@ -156,8 +159,8 @@ describe("CompressionHub — Context Editing", () => {
await flush();
const text = container.textContent ?? "";
expect(text).toContain("apenas para Claude");
expect(text).toContain("não reescrevemos a mensagem");
expect(text).toContain("available for Claude (Anthropic) only");
expect(text).toContain("we do not rewrite the message");
});
it("PUTs contextEditing: { enabled: true } when the toggle is flipped on", async () => {

View File

@@ -111,28 +111,13 @@ async function flush() {
// ── Tests ─────────────────────────────────────────────────────────────────
describe("CompressionHub", () => {
it("renders the master switch, mode selector, and the layered pipeline", async () => {
setupFetchMock({ enabled: true, mode: "stacked", pipeline: [{ engine: "rtk" }] });
const { default: CompressionHub } =
await import("../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<CompressionHub />);
});
await flush();
const text = container.textContent ?? "";
expect(text).toContain("Compression Hub");
expect(text).toContain("Token Saver");
expect(text).toContain("Stacked");
// Active pipeline engine (from the default combo) renders
expect(text).toContain("RTK");
// Inactive engines from the catalog render too
expect(text).toContain("Caveman");
// Active-pipeline callout shows when enabled && stacked
expect(text).toContain("Layer pipeline is active");
});
// NOTE: the master Token Saver toggle, mode selector, and layered-pipeline
// preview this describe block used to assert on were removed by the Phase 2
// Hub redesign (see the "Phase 2" comment at the top of CompressionHub.tsx —
// the Hub is now a thin overview with just an active-profile selector + the
// Context Editing toggle). That redesign, including the explicit assertion
// that the master toggle/mode selector/reorder buttons no longer render, is
// covered by compressionHub-active-selector.test.tsx.
it("INVARIANT #1: no per-layer control issues a PUT/POST to /api/context/combos/default", { timeout: 20000 }, async () => {
const comboWrites: { method: string }[] = [];
@@ -186,22 +171,6 @@ describe("CompressionHub", () => {
expect(comboWrites).toHaveLength(0);
});
it("shows the activation warning when Token Saver is off", async () => {
setupFetchMock({ enabled: false, mode: "off", pipeline: [] });
const { default: CompressionHub } =
await import("../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<CompressionHub />);
});
await flush();
const text = container.textContent ?? "";
expect(text).toContain("Enable Token Saver");
expect(text).toContain("only run in Stacked mode");
});
});
describe("CompressionCombosPageClient", () => {

View File

@@ -3,7 +3,7 @@
* Validates the rendering logic: both sections appear when both request/response have turns;
* only CONTEXT HISTORY appears when response is empty.
*/
import { describe, it } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
import { normalizeConversation } from "../../../src/mitm/inspector/conversationNormalizer.ts";
import type { InterceptedRequest } from "../../../src/mitm/inspector/types.ts";

View File

@@ -1,7 +1,7 @@
/**
* Tests for ConversationTab — normalizeConversation + chat bubble rendering logic
*/
import { describe, it } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
import { normalizeConversation } from "../../../src/mitm/inspector/conversationNormalizer.ts";
import type { InterceptedRequest } from "../../../src/mitm/inspector/types.ts";

View File

@@ -1,7 +1,7 @@
/**
* Tests for HistoricSessionBanner — render with sessionName/null + backToLive callback
*/
import { describe, it } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
// Pure logic tests — no DOM needed (no next-intl in node:test runner)

View File

@@ -19,6 +19,14 @@ describe("home topology hidden networking (#4596)", () => {
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
websocketMock.mockClear();
// useLiveDashboard runs a runtime handshake (GET /api/v1/ws?handshake=1) to
// discover the public WS URL before it opens the socket, so it never
// connects to the hardcoded default and then flaps to the public URL. Stub
// it to fail fast instead of letting the real global fetch hit the network.
vi.stubGlobal(
"fetch",
vi.fn(() => Promise.reject(new Error("network unavailable in test")))
);
vi.stubGlobal(
"WebSocket",
class WebSocketMock {
@@ -59,9 +67,14 @@ describe("home topology hidden networking (#4596)", () => {
expect(websocketMock).not.toHaveBeenCalled();
});
it("opens a WebSocket when the topology section is enabled", () => {
act(() => {
it("opens a WebSocket when the topology section is enabled", async () => {
await act(async () => {
root!.render(<LiveRequestsHarness enabled={true} />);
// Let the handshake fetch's rejection propagate through .catch()/.finally()
// so `wsUrlResolved` flips to true and the connect effect re-runs.
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
});
expect(websocketMock).toHaveBeenCalledTimes(1);
});

View File

@@ -292,18 +292,27 @@ describe("MemoriesTab", () => {
});
it("calls DELETE when delete confirmed", async () => {
const mockFetch = vi.fn();
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: MOCK_MEMORIES,
total: 2,
totalPages: 1,
stats: { total: 2, tokensUsed: 0, hitRate: 0, cacheStats: { hits: 0, misses: 0 } },
}),
// MemoriesTab fires two independent fetches on mount: an immediate health
// check (/api/memory/health) and a 300ms-debounced memories list fetch
// (/api/memory?...). A call-order-dependent mock (mockResolvedValueOnce +
// fallback) is fragile here because the health check resolves first and
// would consume the "once" response meant for the list. Key off the URL
// instead, like the rest of this file's fetch mocks do.
const mockFetch = vi.fn((url: string) => {
if (typeof url === "string" && url.startsWith("/api/memory?")) {
return Promise.resolve({
ok: true,
json: async () => ({
data: MOCK_MEMORIES,
total: 2,
totalPages: 1,
stats: { total: 2, tokensUsed: 0, hitRate: 0, cacheStats: { hits: 0, misses: 0 } },
}),
});
}
return Promise.resolve({ ok: true, json: async () => ({}) });
});
mockFetch.mockResolvedValue({ ok: true, json: async () => ({}) });
globalThis.fetch = mockFetch;
globalThis.fetch = mockFetch as unknown as typeof fetch;
const { default: MemoriesTab } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab"
);

View File

@@ -64,6 +64,52 @@ function renderBuildTab(config = BASE_CONFIG): HTMLDivElement {
return el;
}
// ── BuildWizard navigation helpers ──────────────────────────────────────────
//
// BuildTab now renders behind a 3-step wizard (BuildWizard.tsx):
// step 1 — pick a mode (Tools / JSON / Both)
// step 2 — configure tools and/or the JSON schema, depending on the mode
// step 3 — run + toolbar badges + prompt textarea
//
// next-intl is mocked as a key pass-through above, so every translated label
// renders as its raw i18n key (e.g. "nextButton", "modeToolsTitle").
type BuildMode = "tools" | "json" | "both";
function clickNext(el: HTMLDivElement): void {
const nextBtn = Array.from(el.querySelectorAll("button")).find((b) =>
b.textContent?.includes("nextButton"),
) as HTMLButtonElement;
act(() => {
nextBtn.click();
});
}
function selectMode(el: HTMLDivElement, mode: BuildMode): void {
// Step 1's default mode is already "tools" — only click a mode card when a
// different mode is required.
if (mode === "tools") return;
const key = mode === "json" ? "modeJsonTitle" : "modeBothTitle";
const card = Array.from(el.querySelectorAll("button")).find((b) =>
b.textContent?.includes(key),
) as HTMLButtonElement;
act(() => {
card.click();
});
}
/** Drive the wizard from step 1 to step 2, selecting `mode` along the way. */
function goToStep2(el: HTMLDivElement, mode: BuildMode = "tools"): void {
selectMode(el, mode);
clickNext(el);
}
/** Drive the wizard from step 1 all the way to step 3 (run screen). */
function goToStep3(el: HTMLDivElement, mode: BuildMode = "tools"): void {
goToStep2(el, mode);
clickNext(el);
}
afterEach(() => {
for (const { root, el } of containers) {
act(() => root.unmount());
@@ -76,28 +122,34 @@ afterEach(() => {
describe("BuildTab", () => {
it("renders Run button", () => {
const el = renderBuildTab();
const runBtn = el.querySelector("[class*='bg-primary']");
expect(runBtn?.textContent).toContain("runLabel");
goToStep3(el);
const runBtn = Array.from(el.querySelectorAll("button")).find((b) =>
b.textContent?.includes("runButton"),
);
expect(runBtn).not.toBeUndefined();
});
it("renders Function calling section", () => {
const el = renderBuildTab();
expect(el.textContent).toContain("Function calling");
goToStep2(el, "tools");
expect(el.textContent).toContain("toolsLabel");
expect(el.textContent).toContain("Add tool");
});
it("renders Structured output section", () => {
const el = renderBuildTab();
expect(el.textContent).toContain("Structured output");
goToStep2(el, "json");
expect(el.textContent).toContain("structuredOutputLabel");
expect(el.textContent).toContain("JSON mode");
});
it("adds a tool and shows it in function calling UI", async () => {
const el = renderBuildTab();
goToStep2(el, "tools");
// Find add tool form inputs in the right panel
// Find add tool form inputs in the tools panel
const allInputs = el.querySelectorAll("input[type='text']") as NodeListOf<HTMLInputElement>;
// The first or second input should be the function name
// The first input is the function name
const nameInput = allInputs[0];
act(() => setInputValue(nameInput, "search_web"));
@@ -115,16 +167,15 @@ describe("BuildTab", () => {
it("shows validation error for invalid JSON in tool params", async () => {
const el = renderBuildTab();
goToStep2(el, "tools");
const allInputs = el.querySelectorAll("input[type='text']") as NodeListOf<HTMLInputElement>;
act(() => setInputValue(allInputs[0], "bad_tool"));
// The parameters textarea is in the Add tool form section — it has default valid JSON.
// We need to find the textarea labeled "JSON schema for parameters" in the add form.
const paramsTextareas = Array.from(el.querySelectorAll("textarea")).filter(
(t) => t.getAttribute("aria-label") === "JSON schema for parameters",
);
// The last one is in the Add tool form (the first may be the message prompt textarea)
const paramsTextarea = paramsTextareas[paramsTextareas.length - 1] as HTMLTextAreaElement;
act(() => setInputValue(paramsTextarea, "NOT JSON {{{"));
@@ -140,6 +191,7 @@ describe("BuildTab", () => {
it("enables JSON mode toggle and shows schema editor", async () => {
const el = renderBuildTab();
goToStep2(el, "json");
const toggle = el.querySelector("[role='switch']") as HTMLButtonElement;
expect(toggle).not.toBeNull();
@@ -153,6 +205,7 @@ describe("BuildTab", () => {
it("shows tool badge in toolbar when tools are added", async () => {
const el = renderBuildTab();
goToStep2(el, "tools");
const allInputs = el.querySelectorAll("input[type='text']") as NodeListOf<HTMLInputElement>;
act(() => setInputValue(allInputs[0], "my_tool"));
@@ -163,7 +216,9 @@ describe("BuildTab", () => {
) as HTMLButtonElement;
await act(async () => { addToolBtn.click(); });
// Badge "1 tool" should appear in toolbar
clickNext(el); // step 2 -> step 3
// Badge "1 tool" should appear in the step-3 toolbar
expect(el.textContent).toContain("1 tool");
});
@@ -183,6 +238,7 @@ describe("BuildTab", () => {
);
const el = renderBuildTab();
goToStep2(el, "tools");
// Add a tool
const allInputs = el.querySelectorAll("input[type='text']") as NodeListOf<HTMLInputElement>;
@@ -193,14 +249,16 @@ describe("BuildTab", () => {
) as HTMLButtonElement;
await act(async () => { addToolBtn.click(); });
// Type a prompt
const promptTextarea = el.querySelector("textarea[placeholder*='message']") as HTMLTextAreaElement;
clickNext(el); // step 2 -> step 3
// Type a prompt (the only textarea left on step 3 is the prompt input)
const promptTextarea = el.querySelector("textarea") as HTMLTextAreaElement;
act(() => setInputValue(promptTextarea, "Run this tool"));
// Click Run (label is "runLabel" via mocked t())
// Click Run (label is "runButton" via mocked t())
const runBtns = el.querySelectorAll("button");
const runBtn = Array.from(runBtns).find(
(b) => b.textContent?.includes("runLabel") && !b.textContent?.includes("clearAll"),
(b) => b.textContent?.includes("runButton"),
) as HTMLButtonElement;
await act(async () => { runBtn.click(); });
await act(async () => {
@@ -218,8 +276,12 @@ describe("BuildTab", () => {
it("shows JSON mode badge in toolbar when JSON mode is enabled", async () => {
const el = renderBuildTab();
goToStep2(el, "json");
const toggle = el.querySelector("[role='switch']") as HTMLButtonElement;
await act(async () => { toggle.click(); });
clickNext(el); // step 2 -> step 3
expect(el.textContent).toContain("JSON mode");
});
});

View File

@@ -92,9 +92,9 @@ function renderCompareTab(config = BASE_CONFIG): HTMLDivElement {
return el;
}
function setInputValue(el: HTMLInputElement, value: string) {
function setInputValue(el: HTMLInputElement | HTMLTextAreaElement, value: string) {
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
el instanceof HTMLTextAreaElement ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype,
"value",
)?.set;
nativeSetter?.call(el, value);
@@ -204,6 +204,10 @@ describe("CompareTab", () => {
act(() => setInputValue(input, "model-2"));
await act(async () => { addBtn.click(); });
// Run all is disabled until a prompt is entered
const promptTextarea = el.querySelector("[aria-label='User prompt']") as HTMLTextAreaElement;
act(() => setInputValue(promptTextarea, "Compare this"));
const runBtn = el.querySelector("[aria-label='Run all columns']") as HTMLButtonElement;
await act(async () => { runBtn.click(); });
@@ -219,6 +223,11 @@ describe("CompareTab", () => {
);
const el = renderCompareTab();
// Run all is disabled until a prompt is entered
const promptTextarea = el.querySelector("[aria-label='User prompt']") as HTMLTextAreaElement;
act(() => setInputValue(promptTextarea, "Compare this"));
const runBtn = el.querySelector("[aria-label='Run all columns']") as HTMLButtonElement;
act(() => { runBtn.click(); });

View File

@@ -68,6 +68,10 @@ vi.mock("@/shared/components/MonacoEditor", () => ({
vi.mock("@/shared/constants/providers", () => ({
ALIAS_TO_ID: {},
AI_PROVIDERS: {},
OPENAI_COMPATIBLE_PREFIX: "openai-compatible-",
ANTHROPIC_COMPATIBLE_PREFIX: "anthropic-compatible-",
CLAUDE_CODE_COMPATIBLE_PREFIX: "anthropic-compatible-cc-",
}));
vi.mock("@/shared/utils/maskEmail", () => ({

View File

@@ -1,134 +0,0 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("@/shared/components", () => ({
Button: ({
children,
onClick,
disabled,
}: {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}) => (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
),
}));
vi.mock("@/shared/components/ProviderIcon", () => ({
default: () => <span />,
}));
vi.mock("@/lib/quota/planRegistry", () => ({
knownProviders: () => ["openai", "anthropic"],
getKnownPlan: (prov: string) => {
if (prov === "openai") {
return { dimensions: [{ unit: "tokens", window: "daily", limit: 100000 }] };
}
return null;
},
}));
const MOCK_CONNECTIONS = [
{ id: "conn_1", provider: "openai", name: "GPT Account" },
{ id: "conn_2", provider: "anthropic", email: "user@example.com" },
];
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);
const { default: ProviderPlanConfigClient } = await import(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/plans/ProviderPlanConfigClient"
);
let container: HTMLDivElement | null = null;
let root: ReturnType<typeof createRoot> | null = null;
async function renderPage() {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
container = document.createElement("div");
document.body.appendChild(container);
await act(async () => {
root = createRoot(container!);
root.render(<ProviderPlanConfigClient />);
});
// Wait for initial fetch effect to resolve
await act(async () => {
await new Promise((r) => setTimeout(r, 30));
});
}
describe("ProviderPlanConfigClient", { timeout: 15000 }, () => {
beforeEach(() => {
mockFetch.mockImplementation((url: string) => {
if (String(url).includes("/api/providers/client")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ connections: MOCK_CONNECTIONS }),
} as unknown as Response);
}
if (String(url).includes("/api/quota/plans")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve([]),
} as unknown as Response);
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve({}),
} as unknown as Response);
});
});
afterEach(() => {
if (root && container) act(() => root!.unmount());
container?.remove();
container = null;
root = null;
vi.clearAllMocks();
});
it("renders the page title", async () => {
await renderPage();
expect(document.body.innerHTML).toContain("title");
});
it("renders catalog section with known providers", async () => {
await renderPage();
// catalogTitle key should appear
expect(document.body.innerHTML).toContain("catalogTitle");
expect(document.body.innerHTML).toContain("openai");
});
it("renders connection selector with options", async () => {
await renderPage();
const select = document.querySelector("select") as HTMLSelectElement;
expect(select).not.toBeNull();
expect(select.options.length).toBeGreaterThan(1);
});
it("shows right-panel placeholder when no connection selected", async () => {
await renderPage();
expect(document.body.innerHTML).toContain("unknownProviderNotice");
});
it("renders save button after selecting a connection", async () => {
await renderPage();
const select = document.querySelector("select") as HTMLSelectElement;
await act(async () => {
select.value = "conn_1";
select.dispatchEvent(new Event("change", { bubbles: true }));
});
expect(document.body.innerHTML).toContain("saveOverrideButton");
});
});

View File

@@ -6,7 +6,7 @@
* - RequestRow exports an onSameContext prop
* - useTrafficFilters.setSameContext is referenced from TrafficInspectorPageClient
*/
import { describe, it } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
@@ -17,21 +17,35 @@ const ROOT = path.resolve(
__dirname,
"../../../src/app/(dashboard)/dashboard/tools/traffic-inspector"
);
const SRC_ROOT = path.resolve(__dirname, "../../../src");
function read(rel: string): string {
return fs.readFileSync(path.join(ROOT, rel), "utf8");
}
function readSrc(rel: string): string {
return fs.readFileSync(path.join(SRC_ROOT, rel), "utf8");
}
describe("R5-4 same-context filter end-to-end", () => {
it("useTrafficStream.applyFilter has sameContextKey branch", () => {
const src = read("hooks/useTrafficStream.ts");
// The comparison itself now lives in the extracted, independently-testable
// matchesTrafficFilter() helper (src/lib/inspector/matchesTrafficFilter.ts) —
// useTrafficStream.applyFilter just delegates to it.
const hookSrc = read("hooks/useTrafficStream.ts");
assert.ok(
src.includes("sameContextKey") && src.includes("contextKey"),
"applyFilter should branch on sameContextKey / contextKey"
hookSrc.includes("matchesTrafficFilter"),
"applyFilter should delegate to matchesTrafficFilter"
);
const matcherSrc = readSrc("lib/inspector/matchesTrafficFilter.ts");
assert.ok(
matcherSrc.includes("sameContextKey") && matcherSrc.includes("contextKey"),
"matchesTrafficFilter should branch on sameContextKey / contextKey"
);
// Must actually exclude requests where contextKey differs
assert.ok(
src.includes("req.contextKey !== f.sameContextKey"),
matcherSrc.includes("req.contextKey !== f.sameContextKey"),
"should exclude when contextKey !== sameContextKey"
);
});

View File

@@ -270,12 +270,13 @@ describe("CompareTab", () => {
await new Promise((r) => setTimeout(r, 150));
});
// Check that the table exists and contains overlap info
const table = el.querySelector("table");
if (table) {
// The results panel renders as a div-based side-by-side layout (not a <table>) —
// the overlap summary footer lives inside [data-testid='compare-results'].
const resultsPanel = el.querySelector("[data-testid='compare-results']");
if (resultsPanel) {
// URL overlap row should contain a fraction like "1/2"
const tableText = table.textContent ?? "";
expect(tableText).toMatch(/URL overlap|\d+\/\d+/);
const panelText = resultsPanel.textContent ?? "";
expect(panelText).toMatch(/in common|\d+\/\d+/);
} else {
// Loading state is still active — acceptable
expect(el.querySelector("[data-testid='compare-loading']")).toBeTruthy();

View File

@@ -112,7 +112,9 @@ describe("ScrapeTab", () => {
});
const errorEl = el.querySelector("[data-testid='url-error']");
expect(errorEl).toBeTruthy();
expect(errorEl?.textContent).toContain("URL");
// next-intl is mocked as a key pass-through above (per repo convention), so the
// rendered text is the raw i18n key, not the translated "URL is required" copy.
expect(errorEl?.textContent).toContain("scrapeUrlRequired");
});
it("shows error for invalid URL", () => {

View File

@@ -1,7 +1,7 @@
/**
* Tests for SessionRecorderBar — start/stop flow + timer logic
*/
import { describe, it } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
function formatElapsed(s: number): string {

View File

@@ -2,7 +2,7 @@
* Asserts that StatsTab lazy-loads StatsCharts via next/dynamic (ssr: false)
* and does NOT statically import anything from "recharts".
*/
import { describe, it } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

View File

@@ -5,7 +5,7 @@
* Round-3 F-I18N translated ConversationTab/StatsTab/StatsCharts but missed
* TimingTab (5 labels) and TimingWaterfall (2 labels). Round-4 closed the gap.
*/
import { describe, it } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

View File

@@ -1,7 +1,7 @@
/**
* Smoke tests for Traffic Inspector page structure and constants
*/
import { describe, it } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
describe("Traffic Inspector page smoke tests", () => {

View File

@@ -1,7 +1,7 @@
/**
* Tests for useResizablePanels — drag changes width, collapse to 48px, localStorage persistence
*/
import { describe, it } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
const MIN_WIDTH = 280;

View File

@@ -4,7 +4,7 @@
* Verifies that during recording, new traffic WS events trigger
* POST to /api/tools/traffic-inspector/sessions/{id}/requests.
*/
import { describe, it, before, after } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";

View File

@@ -6,7 +6,7 @@
* This matches how use-traffic-stream.test.tsx tests hook logic (pure logic,
* no React renderer needed).
*/
import { describe, it, beforeEach } from "node:test";
import { describe, it, beforeEach } from "vitest";
import assert from "node:assert/strict";
// ---------------------------------------------------------------------------

View File

@@ -1,7 +1,7 @@
/**
* Tests for useTrafficStream — WebSocket snapshot/new/update/clear + reconnect backoff
*/
import { describe, it, before, after, mock } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";

View File

@@ -1,7 +1,7 @@
/**
* Tests for useVirtualList — virtualizes 1000+ items without rendering all
*/
import { describe, it } from "node:test";
import { describe, it } from "vitest";
import assert from "node:assert/strict";
const ESTIMATED_ROW_HEIGHT = 48;

View File

@@ -6,6 +6,7 @@ export default defineConfig({
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./tests/_setup/vitestUiPolyfills.ts"],
pool: "threads",
maxWorkers: 20,
fileParallelism: true,