mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
committed by
GitHub
parent
9e8aeab7c6
commit
a5cad5ab2a
1
changelog.d/maintenance/vitest-ui-suite-green.md
Normal file
1
changelog.d/maintenance/vitest-ui-suite-green.md
Normal 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)
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
23
tests/_setup/vitestUiPolyfills.ts
Normal file
23
tests/_setup/vitestUiPolyfills.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(); });
|
||||
|
||||
@@ -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", () => ({
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,6 +6,7 @@ export default defineConfig({
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
setupFiles: ["./tests/_setup/vitestUiPolyfills.ts"],
|
||||
pool: "threads",
|
||||
maxWorkers: 20,
|
||||
fileParallelism: true,
|
||||
|
||||
Reference in New Issue
Block a user