fix(test): revive orphaned vitest tests and fix CI routing (#8718)

This commit is contained in:
Mohit Rawat
2026-08-11 12:54:54 +05:30
committed by GitHub
parent 016c3fdb77
commit 324e28f438
11 changed files with 383 additions and 519 deletions

View File

@@ -197,7 +197,7 @@ describe("TierResolver", () => {
{ provider: "openai", model: "gpt-4o" },
{ provider: "openai", model: "gpt-4o" },
]);
// Observable effect of the cache: the duplicate resolves to the same tier and only
// Observable effect of the cache: the duplicate resolves to the same tier and only
// ONE entry is memoized (getTierStats counts cache entries, not classify calls).
expect(results).toHaveLength(2);
expect(results[0].tier).toBe(results[1].tier);

View File

@@ -26,7 +26,7 @@ function makeContainer(): HTMLElement {
return container;
}
describe("AutoComboCatalog", () => {
describe("AutoComboCatalog", { timeout: 15_000 }, () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }

View File

@@ -8,7 +8,12 @@ import type { AgentSkill, SkillCoverage } from "../../src/lib/agentSkills/types"
// ── i18n stub ────────────────────────────────────────────────────────────────
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
useTranslations: () => {
const t = (key: string) => key;
t.has = () => false;
return t;
},
}));
// ── next/link stub ───────────────────────────────────────────────────────────
@@ -30,7 +35,10 @@ vi.mock("next/link", () => ({
// ── next/dynamic stub — renders placeholder immediately ───────────────────────
vi.mock("next/dynamic", () => ({
default: (loader: () => Promise<{ default: React.ComponentType<{ children: string }> }>, _opts?: unknown) => {
default: (
loader: () => Promise<{ default: React.ComponentType<{ children: string }> }>,
_opts?: unknown
) => {
// Return a synchronous stub that renders children as plain text.
return function DynamicStub({ children }: { children: string }) {
return <div data-testid="react-markdown">{children}</div>;
@@ -49,7 +57,8 @@ function makeSkill(overrides: Partial<AgentSkill> = {}): AgentSkill {
area: "providers",
icon: "hub",
endpoints: ["POST /api/providers", "GET /api/providers"],
rawUrl: "https://raw.githubusercontent.com/diegosouzapw/OmniRoute/refs/heads/main/skills/omni-providers/SKILL.md",
rawUrl:
"https://raw.githubusercontent.com/diegosouzapw/OmniRoute/refs/heads/main/skills/omni-providers/SKILL.md",
githubUrl: "https://github.com/diegosouzapw/OmniRoute/blob/main/skills/omni-providers/SKILL.md",
...overrides,
};
@@ -63,7 +72,7 @@ function make42Skills(): AgentSkill[] {
id: `omni-skill-${i}`,
name: `API Skill ${i}`,
category: "api",
}),
})
);
}
for (let i = 0; i < 20; i++) {
@@ -74,7 +83,7 @@ function make42Skills(): AgentSkill[] {
category: "cli",
endpoints: undefined,
cliCommands: [`skill${i} run`, `skill${i} status`],
}),
})
);
}
return skills;
@@ -96,7 +105,11 @@ const PARTIAL_COVERAGE: SkillCoverage = {
// ── Fetch mock factory ───────────────────────────────────────────────────────
function mockFetch(skills: AgentSkill[], coverage: SkillCoverage, rawMarkdown = "# Test Skill\nContent here.") {
function mockFetch(
skills: AgentSkill[],
coverage: SkillCoverage,
rawMarkdown = "# Test Skill\nContent here."
) {
return vi.fn(async (url: string | Request) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr === "/api/agent-skills") {
@@ -128,7 +141,9 @@ function makeContainer(): HTMLElement {
}
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
// Mock clipboard
Object.defineProperty(navigator, "clipboard", {
value: { writeText: vi.fn().mockResolvedValue(undefined) },
@@ -165,9 +180,8 @@ describe("AgentSkillsPageClient", () => {
const skills = make42Skills();
vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const { AgentSkillsPageClient } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient");
const container = makeContainer();
root = createRoot(container);
await act(async () => {
@@ -181,9 +195,8 @@ describe("AgentSkillsPageClient", () => {
it("renders SkillsConceptCard variant=agent at the top", async () => {
vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const { AgentSkillsPageClient } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient");
const container = makeContainer();
root = createRoot(container);
await act(async () => {
@@ -198,16 +211,17 @@ describe("AgentSkillsPageClient", () => {
const skills = make42Skills();
vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const { AgentSkillsPageClient } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient");
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const filterApiBtn = container.querySelector("[data-testid='filter-api']") as HTMLButtonElement | null;
const filterApiBtn = container.querySelector(
"[data-testid='filter-api']"
) as HTMLButtonElement | null;
expect(filterApiBtn).not.toBeNull();
await act(async () => {
@@ -222,16 +236,17 @@ describe("AgentSkillsPageClient", () => {
const skills = make42Skills();
vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const { AgentSkillsPageClient } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient");
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const filterCliBtn = container.querySelector("[data-testid='filter-cli']") as HTMLButtonElement | null;
const filterCliBtn = container.querySelector(
"[data-testid='filter-cli']"
) as HTMLButtonElement | null;
await act(async () => {
filterCliBtn?.click();
});
@@ -246,16 +261,17 @@ describe("AgentSkillsPageClient", () => {
const fetchMock = mockFetch(skills, FULL_COVERAGE, "# omni-skill-0 doc");
vi.stubGlobal("fetch", fetchMock);
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const { AgentSkillsPageClient } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient");
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const firstCard = container.querySelector("[data-testid='skill-card-omni-skill-0']") as HTMLElement | null;
const firstCard = container.querySelector(
"[data-testid='skill-card-omni-skill-0']"
) as HTMLElement | null;
expect(firstCard).not.toBeNull();
await act(async () => {
@@ -264,7 +280,7 @@ describe("AgentSkillsPageClient", () => {
// Before debounce fires — raw fetch should NOT have been made yet
const rawFetchCallsBefore = (fetchMock as ReturnType<typeof vi.fn>).mock.calls.filter(
([url]: [string]) => typeof url === "string" && url.includes("/raw"),
([url]: [string]) => typeof url === "string" && url.includes("/raw")
);
expect(rawFetchCallsBefore.length).toBe(0);
@@ -275,7 +291,7 @@ describe("AgentSkillsPageClient", () => {
// Now the raw fetch should have been triggered
const rawFetchCallsAfter = (fetchMock as ReturnType<typeof vi.fn>).mock.calls.filter(
([url]: [string]) => typeof url === "string" && url.includes("/raw"),
([url]: [string]) => typeof url === "string" && url.includes("/raw")
);
expect(rawFetchCallsAfter.length).toBeGreaterThan(0);
@@ -285,9 +301,8 @@ describe("AgentSkillsPageClient", () => {
it("preview pane shows empty state when no card is selected", async () => {
vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const { AgentSkillsPageClient } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient");
const container = makeContainer();
root = createRoot(container);
await act(async () => {
@@ -301,9 +316,8 @@ describe("AgentSkillsPageClient", () => {
it("CoverageBar is rendered with 100% = green bars when coverage is full", async () => {
vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const { AgentSkillsPageClient } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient");
const container = makeContainer();
root = createRoot(container);
await act(async () => {
@@ -328,9 +342,8 @@ describe("AgentSkillsPageClient", () => {
it("generate button is hidden when coverage is 100%", async () => {
vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const { AgentSkillsPageClient } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient");
const container = makeContainer();
root = createRoot(container);
await act(async () => {
@@ -344,9 +357,8 @@ describe("AgentSkillsPageClient", () => {
it("generate button is visible when coverage is partial", async () => {
vi.stubGlobal("fetch", mockFetch(make42Skills(), PARTIAL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const { AgentSkillsPageClient } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient");
const container = makeContainer();
root = createRoot(container);
await act(async () => {
@@ -361,16 +373,17 @@ describe("AgentSkillsPageClient", () => {
const skills = make42Skills();
vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const { AgentSkillsPageClient } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient");
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const searchInput = container.querySelector("[data-testid='search-input']") as HTMLInputElement | null;
const searchInput = container.querySelector(
"[data-testid='search-input']"
) as HTMLInputElement | null;
expect(searchInput).not.toBeNull();
await act(async () => {
@@ -380,7 +393,7 @@ describe("AgentSkillsPageClient", () => {
// React uses onChange
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value",
"value"
)?.set;
nativeInputValueSetter?.call(searchInput, "API Skill 0");
searchInput.dispatchEvent(new Event("change", { bubbles: true }));
@@ -396,9 +409,8 @@ describe("AgentSkillsPageClient", () => {
it("MCP and A2A links bar is present", async () => {
vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const { AgentSkillsPageClient } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient");
const container = makeContainer();
root = createRoot(container);
await act(async () => {
@@ -414,9 +426,8 @@ describe("AgentSkillsPageClient", () => {
describe("CoverageBar", () => {
it("renders two progressbars with correct aria attributes", async () => {
const { CoverageBar } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar"
);
const { CoverageBar } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar");
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
@@ -438,9 +449,8 @@ describe("CoverageBar", () => {
});
it("applies red color class when coverage is below 75%", async () => {
const { CoverageBar } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar"
);
const { CoverageBar } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar");
const lowCoverage: SkillCoverage = {
api: { have: 5, total: 22 },
cli: { have: 0, total: 20 },
@@ -462,9 +472,8 @@ describe("CoverageBar", () => {
});
it("applies amber color class when coverage is between 75% and 100%", async () => {
const { CoverageBar } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar"
);
const { CoverageBar } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar");
const partialCoverage: SkillCoverage = {
api: { have: 18, total: 22 }, // ~81.8% = amber
cli: { have: 15, total: 20 }, // 75% = amber
@@ -490,9 +499,8 @@ describe("CoverageBar", () => {
describe("SkillCard", () => {
it("renders skill name and description", async () => {
const { SkillCard } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"
);
const { SkillCard } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard");
const skill = makeSkill({ name: "Providers", description: "Manage connections" });
const container = makeContainer();
const localRoot = createRoot(container);
@@ -507,9 +515,8 @@ describe("SkillCard", () => {
});
it("has role=button and aria-pressed=false when not selected", async () => {
const { SkillCard } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"
);
const { SkillCard } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard");
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
@@ -524,9 +531,8 @@ describe("SkillCard", () => {
});
it("has aria-pressed=true when selected", async () => {
const { SkillCard } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"
);
const { SkillCard } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard");
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
@@ -540,9 +546,8 @@ describe("SkillCard", () => {
});
it("calls onClick when clicked", async () => {
const { SkillCard } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"
);
const { SkillCard } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard");
const handleClick = vi.fn();
const container = makeContainer();
const localRoot = createRoot(container);
@@ -558,9 +563,8 @@ describe("SkillCard", () => {
});
it("shows first 2 endpoints as chips for API skill", async () => {
const { SkillCard } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"
);
const { SkillCard } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard");
const skill = makeSkill({
endpoints: ["POST /api/providers", "GET /api/providers", "DELETE /api/providers/:id"],
});
@@ -583,15 +587,12 @@ describe("SkillCard", () => {
describe("SkillPreviewPane", () => {
it("renders empty state when skillId is null", async () => {
const { SkillPreviewPane } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane"
);
const { SkillPreviewPane } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane");
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(
<SkillPreviewPane skillId={null} markdown={null} loading={false} />,
);
localRoot.render(<SkillPreviewPane skillId={null} markdown={null} loading={false} />);
});
const empty = container.querySelector("[data-testid='skill-preview-empty']");
@@ -602,9 +603,8 @@ describe("SkillPreviewPane", () => {
});
it("renders markdown when skillId and markdown are provided", async () => {
const { SkillPreviewPane } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane"
);
const { SkillPreviewPane } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane");
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
@@ -613,7 +613,7 @@ describe("SkillPreviewPane", () => {
skillId="omni-providers"
markdown="# Providers\nContent here."
loading={false}
/>,
/>
);
});
@@ -625,15 +625,12 @@ describe("SkillPreviewPane", () => {
});
it("shows error state when skillId provided but markdown is empty string", async () => {
const { SkillPreviewPane } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane"
);
const { SkillPreviewPane } =
await import("../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane");
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(
<SkillPreviewPane skillId="omni-providers" markdown="" loading={false} />,
);
localRoot.render(<SkillPreviewPane skillId="omni-providers" markdown="" loading={false} />);
});
// markdown is "" (falsy) — should show error state

View File

@@ -20,14 +20,23 @@ vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const storage = new Map<string, string>();
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: {
clear: () => storage.clear(),
getItem: (key: string) => storage.get(key) ?? null,
removeItem: (key: string) => storage.delete(key),
setItem: (key: string, value: string) => storage.set(key, value),
},
});
// ── Import components after mocks ─────────────────────────────────────────────
const { default: BatchConceptCard } = await import(
"../../../../src/app/(dashboard)/dashboard/batch/components/BatchConceptCard"
);
const { default: FilesConceptCard } = await import(
"../../../../src/app/(dashboard)/dashboard/batch/components/FilesConceptCard"
);
const { default: BatchConceptCard } =
await import("../../../../src/app/(dashboard)/dashboard/batch/components/BatchConceptCard");
const { default: FilesConceptCard } =
await import("../../../../src/app/(dashboard)/dashboard/batch/components/FilesConceptCard");
// ── Helpers ───────────────────────────────────────────────────────────────────

View File

@@ -22,7 +22,12 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// ── Mocks ─────────────────────────────────────────────────────────────────────
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
useTranslations: () => {
const t = (key: string) => key;
t.has = () => false;
return t;
},
}));
vi.mock("next/link", () => ({
@@ -33,17 +38,20 @@ vi.mock("next/link", () => ({
// Mock retryFailed (used by BatchRowActions → useBatchActions)
vi.mock("@/lib/batches/retryFailed", () => ({
buildRetryPlan: vi.fn(() => ({ retriableLines: 0, newJsonl: "", failedCustomIds: [], skippedLines: 0 })),
buildRetryPlan: vi.fn(() => ({
retriableLines: 0,
newJsonl: "",
failedCustomIds: [],
skippedLines: 0,
})),
}));
// ── Import components after mocks ─────────────────────────────────────────────
const { default: BatchListTab } = await import(
"../../../../src/app/(dashboard)/dashboard/batch/BatchListTab"
);
const { default: FilesListTab } = await import(
"../../../../src/app/(dashboard)/dashboard/batch/FilesListTab"
);
const { default: BatchListTab } =
await import("../../../../src/app/(dashboard)/dashboard/batch/BatchListTab");
const { default: FilesListTab } =
await import("../../../../src/app/(dashboard)/dashboard/batch/FilesListTab");
// ── Helpers ───────────────────────────────────────────────────────────────────
@@ -56,21 +64,23 @@ function makeDiv() {
}
// Batch record factory
function makeBatch(overrides: Partial<{
id: string;
status: string;
endpoint: string;
model: string;
requestCountsTotal: number;
requestCountsCompleted: number;
requestCountsFailed: number;
outputFileId: string | null;
errorFileId: string | null;
expiresAt: number | null;
inputFileId: string;
completionWindow: string;
createdAt: number;
}> = {}) {
function makeBatch(
overrides: Partial<{
id: string;
status: string;
endpoint: string;
model: string;
requestCountsTotal: number;
requestCountsCompleted: number;
requestCountsFailed: number;
outputFileId: string | null;
errorFileId: string | null;
expiresAt: number | null;
inputFileId: string;
completionWindow: string;
createdAt: number;
}> = {}
) {
return {
id: overrides.id ?? "batch-001",
endpoint: overrides.endpoint ?? "/v1/chat/completions",
@@ -99,14 +109,16 @@ function makeBatch(overrides: Partial<{
}
// File record factory
function makeFile(overrides: Partial<{
id: string;
filename: string;
bytes: number;
purpose: string;
createdAt: number;
expiresAt: number | null;
}> = {}) {
function makeFile(
overrides: Partial<{
id: string;
filename: string;
bytes: number;
purpose: string;
createdAt: number;
expiresAt: number | null;
}> = {}
) {
return {
id: overrides.id ?? "file-001",
filename: overrides.filename ?? "batch-input.jsonl",
@@ -131,7 +143,10 @@ function render(jsx: React.ReactElement) {
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => ({}), text: async () => "" }));
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({ ok: true, json: async () => ({}), text: async () => "" })
);
vi.stubGlobal("confirm", vi.fn().mockReturnValue(false)); // don't confirm dialogs
});
@@ -163,9 +178,7 @@ describe("BatchListTab — rendering", () => {
it("2. 'Remove completed' button appears when there are completed batches", () => {
const batches = [makeBatch({ id: "batch-completed", status: "completed" })];
const el = render(
<BatchListTab batches={batches} files={[]} loading={false} />
);
const el = render(<BatchListTab batches={batches} files={[]} loading={false} />);
// button text contains "Remove completed"
const btn = Array.from(el.querySelectorAll("button")).find((b) =>
b.textContent?.includes("batchListRemoveCompleted")
@@ -205,9 +218,7 @@ describe("BatchListTab — rendering", () => {
makeBatch({ id: "batch-completed", status: "completed" }),
makeBatch({ id: "batch-in-progress", status: "in_progress" }),
];
const el = render(
<BatchListTab batches={batches} files={[]} loading={false} />
);
const el = render(<BatchListTab batches={batches} files={[]} loading={false} />);
// Both visible initially
expect(el.textContent).toContain("batch-completed");
@@ -231,9 +242,7 @@ describe("BatchListTab — rendering", () => {
makeBatch({ id: "batch-unique-aaa", status: "completed" }),
makeBatch({ id: "batch-unique-bbb", status: "completed" }),
];
const el = render(
<BatchListTab batches={batches} files={[]} loading={false} />
);
const el = render(<BatchListTab batches={batches} files={[]} loading={false} />);
// Search input exists and accepts text
const input = el.querySelector("input[type='text']") as HTMLInputElement;
@@ -245,27 +254,21 @@ describe("BatchListTab — rendering", () => {
});
it("6. shows loading spinner when loading=true and no batches", () => {
const el = render(
<BatchListTab batches={[]} files={[]} loading={true} />
);
const el = render(<BatchListTab batches={[]} files={[]} loading={true} />);
// Loading state renders a spinner (animate-spin class)
const spinner = el.querySelector(".animate-spin");
expect(spinner).not.toBeNull();
});
it("7. shows empty state when no batches and not loading", () => {
const el = render(
<BatchListTab batches={[]} files={[]} loading={false} />
);
const el = render(<BatchListTab batches={[]} files={[]} loading={false} />);
// Should show some empty-state indicator (no spinner)
expect(el.querySelector(".animate-spin")).toBeNull();
});
it("8. sanitization: rendered content contains no stack traces or file paths", () => {
const batches = [makeBatch({ id: "batch-safe", status: "completed" })];
const el = render(
<BatchListTab batches={batches} files={[]} loading={false} />
);
const el = render(<BatchListTab batches={batches} files={[]} loading={false} />);
const text = el.textContent ?? "";
expect(text).not.toMatch(/\/home\//);
expect(text).not.toMatch(/at \//);
@@ -283,9 +286,7 @@ describe("BatchListTab — rendering", () => {
requestCountsFailed: 10,
}),
];
const el = render(
<BatchListTab batches={batches} files={[]} loading={false} />
);
const el = render(<BatchListTab batches={batches} files={[]} loading={false} />);
// The i18n key is rendered literally in tests (mock useTranslations returns key)
expect(el.textContent).toContain("batchListProgressPartial");
});
@@ -300,21 +301,21 @@ describe("BatchListTab — rendering", () => {
requestCountsFailed: 0,
}),
];
const el = render(
<BatchListTab batches={batches} files={[]} loading={false} />
);
const el = render(<BatchListTab batches={batches} files={[]} loading={false} />);
expect(el.textContent).toContain("-50%");
});
it("18. Provider column derives provider from model id (A-1)", () => {
const batches = [
makeBatch({ id: "batch-openai", model: "gpt-4o", status: "completed" }),
makeBatch({ id: "batch-anthropic", model: "claude-3-5-sonnet-20241022", status: "completed" }),
makeBatch({
id: "batch-anthropic",
model: "claude-3-5-sonnet-20241022",
status: "completed",
}),
makeBatch({ id: "batch-gemini", model: "gemini-1.5-flash", status: "completed" }),
];
const el = render(
<BatchListTab batches={batches} files={[]} loading={false} />
);
const el = render(<BatchListTab batches={batches} files={[]} loading={false} />);
const text = el.textContent ?? "";
expect(text).toContain("OpenAI");
expect(text).toContain("Anthropic");
@@ -327,9 +328,7 @@ describe("BatchListTab — rendering", () => {
makeBatch({ id: "b-o1", model: "o1-preview", status: "completed" }),
makeBatch({ id: "b-o3", model: "o3-mini", status: "completed" }),
];
const el = render(
<BatchListTab batches={batches} files={[]} loading={false} />
);
const el = render(<BatchListTab batches={batches} files={[]} loading={false} />);
const text = el.textContent ?? "";
// All three are OpenAI families — the column should print "OpenAI" three times.
const occurrences = (text.match(/OpenAI/g) ?? []).length;
@@ -338,13 +337,15 @@ describe("BatchListTab — rendering", () => {
it("20. Provider derivation routes unknown / null model through i18n keys (R2)", () => {
const batches = [
makeBatch({ id: "b-unknown-model", model: "weird-model-name-not-recognized", status: "completed" }),
makeBatch({
id: "b-unknown-model",
model: "weird-model-name-not-recognized",
status: "completed",
}),
// makeBatch's default model is gpt-4o; explicitly null-ish models below
makeBatch({ id: "b-null-model", model: "", status: "completed" }),
];
const el = render(
<BatchListTab batches={batches} files={[]} loading={false} />
);
const el = render(<BatchListTab batches={batches} files={[]} loading={false} />);
const text = el.textContent ?? "";
// i18n mock returns the key literal — proves we route through t() and
// didn't leave hardcoded "Other" / "—" English strings in the cell.
@@ -362,9 +363,7 @@ describe("FilesListTab — rendering", () => {
makeFile({ id: "file-bbb", filename: "output-bbb.jsonl", purpose: "batch-output" }),
makeFile({ id: "file-ccc", filename: "fine-tune-ccc.jsonl", purpose: "fine-tune" }),
];
const el = render(
<FilesListTab files={files} loading={false} onRefresh={vi.fn()} />
);
const el = render(<FilesListTab files={files} loading={false} onRefresh={vi.fn()} />);
expect(el.textContent).toContain("input-aaa.jsonl");
expect(el.textContent).toContain("output-bbb.jsonl");
expect(el.textContent).toContain("fine-tune-ccc.jsonl");
@@ -375,9 +374,7 @@ describe("FilesListTab — rendering", () => {
makeFile({ id: "file-batch", filename: "batch-input.jsonl", purpose: "batch" }),
makeFile({ id: "file-fine", filename: "fine-tune.jsonl", purpose: "fine-tune" }),
];
const el = render(
<FilesListTab files={files} loading={false} />
);
const el = render(<FilesListTab files={files} loading={false} />);
expect(el.textContent).toContain("batch-input.jsonl");
expect(el.textContent).toContain("fine-tune.jsonl");
@@ -399,9 +396,7 @@ describe("FilesListTab — rendering", () => {
makeFile({ id: "file-alpha", filename: "alpha-batch.jsonl", purpose: "batch" }),
makeFile({ id: "file-beta", filename: "beta-batch.jsonl", purpose: "batch" }),
];
const el = render(
<FilesListTab files={files} loading={false} />
);
const el = render(<FilesListTab files={files} loading={false} />);
// Search input exists
const input = el.querySelector("input[type='text']") as HTMLInputElement;
@@ -413,9 +408,7 @@ describe("FilesListTab — rendering", () => {
});
it("12. 'used by' column header is visible (i18n key)", () => {
const el = render(
<FilesListTab files={[makeFile()]} loading={false} />
);
const el = render(<FilesListTab files={[makeFile()]} loading={false} />);
// filesListUsedByColumn key is rendered in the header
expect(el.textContent).toContain("filesListUsedByColumn");
});
@@ -433,9 +426,7 @@ describe("FilesListTab — rendering", () => {
model: "gpt-4o",
},
];
const el = render(
<FilesListTab files={[file]} loading={false} batches={batches} />
);
const el = render(<FilesListTab files={[file]} loading={false} batches={batches} />);
// Truncated id prefix (12 chars) appears inline
expect(el.textContent).toContain("batch-using-");
// Role label is rendered next to the id (G-AUD1 — plan §4 "b1 (input)")
@@ -447,9 +438,7 @@ describe("FilesListTab — rendering", () => {
});
it("14. loading state shows spinner", () => {
const el = render(
<FilesListTab files={[]} loading={true} />
);
const el = render(<FilesListTab files={[]} loading={true} />);
const spinner = el.querySelector(".animate-spin");
expect(spinner).not.toBeNull();
});

View File

@@ -4,11 +4,10 @@
* split, file structure, source patterns, and exported symbols without DOM rendering.
*
* Run:
* node --import tsx/esm --test tests/unit/omni-skills-page.test.tsx
* vitest run tests/unit/omni-skills-page.test.tsx
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { describe, expect, it } from "vitest";
import { existsSync, readFileSync } from "node:fs";
import { resolve, join } from "node:path";
@@ -20,11 +19,13 @@ const base = resolve(join(cwd, "src/app/(dashboard)/dashboard/omni-skills"));
describe("File structure — omni-skills directory", () => {
it("old /dashboard/skills directory does not exist", () => {
const oldPath = resolve(join(cwd, "src/app/(dashboard)/dashboard/skills"));
assert.ok(!existsSync(oldPath), `Old skills/ directory must be absent (found at ${oldPath})`);
expect(existsSync(oldPath), `Old skills/ directory must be absent (found at ${oldPath})`).toBe(
false
);
});
it("new /dashboard/omni-skills directory exists", () => {
assert.ok(existsSync(base), `omni-skills/ directory must exist at ${base}`);
expect(existsSync(base), `omni-skills/ directory must exist at ${base}`).toBe(true);
});
const expectedFiles = [
@@ -40,9 +41,8 @@ describe("File structure — omni-skills directory", () => {
for (const file of expectedFiles) {
it(`file exists: omni-skills/${file}`, () => {
assert.ok(
existsSync(resolve(join(base, file))),
`Expected omni-skills/${file} to exist`
expect(existsSync(resolve(join(base, file))), `Expected omni-skills/${file} to exist`).toBe(
true
);
});
}
@@ -54,21 +54,24 @@ describe("page.tsx — server component", () => {
const src = readFileSync(resolve(join(base, "page.tsx")), "utf-8");
it("is a server component (no 'use client' directive)", () => {
assert.ok(
expect(
!src.includes('"use client"') && !src.includes("'use client'"),
"page.tsx must not have 'use client'"
);
).toBe(true);
});
it("imports and renders OmniSkillsPageClient", () => {
assert.ok(src.includes("OmniSkillsPageClient"), "page.tsx must reference OmniSkillsPageClient");
expect(
src.includes("OmniSkillsPageClient"),
"page.tsx must reference OmniSkillsPageClient"
).toBe(true);
});
it("has a default export named Page", () => {
assert.ok(
expect(
src.includes("export default function Page"),
"page.tsx must have 'export default function Page'"
);
).toBe(true);
});
});
@@ -78,48 +81,50 @@ describe("OmniSkillsPageClient.tsx", () => {
const src = readFileSync(resolve(join(base, "OmniSkillsPageClient.tsx")), "utf-8");
it("starts with 'use client'", () => {
assert.ok(src.startsWith('"use client"'), "OmniSkillsPageClient must start with 'use client'");
expect(
src.startsWith('"use client"'),
"OmniSkillsPageClient must start with 'use client'"
).toBe(true);
});
it("has all 4 tab IDs", () => {
for (const tabId of ["skills", "executions", "sandbox", "marketplace"]) {
assert.ok(
expect(
src.includes(`id: "${tabId}"`),
`OmniSkillsPageClient must have tab id="${tabId}"`
);
).toBe(true);
}
});
it("renders SkillsConceptCard with variant='omni'", () => {
assert.ok(
expect(
src.includes('variant="omni"'),
"OmniSkillsPageClient must render <SkillsConceptCard variant=\"omni\" />"
);
'OmniSkillsPageClient must render <SkillsConceptCard variant="omni" />'
).toBe(true);
});
it("imports SkillsConceptCard from shared components", () => {
assert.ok(
expect(
src.includes("SkillsConceptCard"),
"OmniSkillsPageClient must import SkillsConceptCard"
);
).toBe(true);
});
it("has selectedSkillId state", () => {
assert.ok(
expect(
src.includes("selectedSkillId"),
"OmniSkillsPageClient must maintain selectedSkillId state"
);
).toBe(true);
});
it("wires OmniSkillsList with inspector props", () => {
assert.ok(
src.includes("OmniSkillsList"),
"OmniSkillsPageClient must render OmniSkillsList"
expect(src.includes("OmniSkillsList"), "OmniSkillsPageClient must render OmniSkillsList").toBe(
true
);
assert.ok(
expect(
src.includes("onSelectSkill"),
"OmniSkillsPageClient must pass onSelectSkill to OmniSkillsList"
);
).toBe(true);
});
it("renders all 4 tab components", () => {
@@ -129,12 +134,12 @@ describe("OmniSkillsPageClient.tsx", () => {
"OmniSandboxTab",
"OmniMarketplaceTab",
]) {
assert.ok(src.includes(component), `OmniSkillsPageClient must render <${component}>`);
expect(src.includes(component), `OmniSkillsPageClient must render <${component}>`).toBe(true);
}
});
it("has install modal with hardcoded 'X' close button (preserved behavior)", () => {
assert.ok(src.includes("showInstallModal"), "must preserve showInstallModal state");
expect(src.includes("showInstallModal"), "must preserve showInstallModal state").toBe(true);
});
});
@@ -144,31 +149,31 @@ describe("OmniSkillCard.tsx", () => {
const src = readFileSync(resolve(join(base, "components/OmniSkillCard.tsx")), "utf-8");
it("starts with 'use client'", () => {
assert.ok(src.startsWith('"use client"'), "must be a client component");
expect(src.startsWith('"use client"'), "must be a client component").toBe(true);
});
it("accepts skill, selected, onClick props", () => {
assert.ok(src.includes("OmniSkillCardProps"), "must define OmniSkillCardProps");
assert.ok(src.includes("selected:"), "must have selected prop");
assert.ok(src.includes("onClick:"), "must have onClick prop");
expect(src.includes("OmniSkillCardProps"), "must define OmniSkillCardProps").toBe(true);
expect(src.includes("selected:"), "must have selected prop").toBe(true);
expect(src.includes("onClick:"), "must have onClick prop").toBe(true);
});
it("has role='button' for accessibility", () => {
assert.ok(src.includes('role="button"'), "must have role='button' for accessibility");
expect(src.includes('role="button"'), "must have role='button' for accessibility").toBe(true);
});
it("exports OmniSkillCard", () => {
assert.ok(
expect(
src.includes("export function OmniSkillCard") || src.includes("export { OmniSkillCard }"),
"must export OmniSkillCard"
);
).toBe(true);
});
it("exports OmniSkill interface", () => {
assert.ok(
expect(
src.includes("export interface OmniSkill"),
"must export OmniSkill interface for other components"
);
).toBe(true);
});
});
@@ -178,44 +183,42 @@ describe("SkillInspectorPane.tsx", () => {
const src = readFileSync(resolve(join(base, "components/SkillInspectorPane.tsx")), "utf-8");
it("starts with 'use client'", () => {
assert.ok(src.startsWith('"use client"'), "must be a client component");
expect(src.startsWith('"use client"'), "must be a client component").toBe(true);
});
it("has all 4 sub-tab IDs", () => {
for (const tabId of ["schema", "handler", "executions", "sandbox"]) {
assert.ok(
src.includes(`"${tabId}"`),
`SkillInspectorPane must include sub-tab "${tabId}"`
expect(src.includes(`"${tabId}"`), `SkillInspectorPane must include sub-tab "${tabId}"`).toBe(
true
);
}
});
it("has empty state text when no skill selected", () => {
assert.ok(
src.includes("Selecione uma skill"),
"must have empty state message"
);
expect(
src.includes("selectSkillToInspect"),
"must have empty state message (i18n key selectSkillToInspect)"
).toBe(true);
});
it("fetches /api/skills/[id] for skill detail", () => {
assert.ok(
expect(
src.includes("/api/skills/${selectedSkillId}") ||
src.includes("`/api/skills/${selectedSkillId}`"),
"must fetch /api/skills/${selectedSkillId} for detail"
);
).toBe(true);
});
it("fetches /api/skills/executions for the executions tab", () => {
assert.ok(
src.includes("api/skills/executions?skillId=") ||
src.includes("api/skills/executions"),
expect(
src.includes("api/skills/executions?skillId=") || src.includes("api/skills/executions"),
"must fetch executions for the selected skill"
);
).toBe(true);
});
it("has ON / AUTO / OFF / Uninstall buttons", () => {
assert.ok(src.includes("onSetMode"), "must call onSetMode for mode buttons");
assert.ok(src.includes("onUninstall"), "must call onUninstall");
expect(src.includes("onSetMode"), "must call onSetMode for mode buttons").toBe(true);
expect(src.includes("onUninstall"), "must call onUninstall").toBe(true);
});
});
@@ -225,19 +228,21 @@ describe("OmniSkillsList.tsx", () => {
const src = readFileSync(resolve(join(base, "components/OmniSkillsList.tsx")), "utf-8");
it("uses grid-cols-12 split layout", () => {
assert.ok(src.includes("grid-cols-12"), "must use 12-column grid for split layout");
expect(src.includes("grid-cols-12"), "must use 12-column grid for split layout").toBe(true);
});
it("renders OmniSkillCard for each skill", () => {
assert.ok(src.includes("OmniSkillCard"), "must render OmniSkillCard per skill");
expect(src.includes("OmniSkillCard"), "must render OmniSkillCard per skill").toBe(true);
});
it("renders SkillInspectorPane on the right", () => {
assert.ok(src.includes("SkillInspectorPane"), "must include SkillInspectorPane in right col");
expect(src.includes("SkillInspectorPane"), "must include SkillInspectorPane in right col").toBe(
true
);
});
it("has onSelectSkill prop to control inspector state", () => {
assert.ok(src.includes("onSelectSkill"), "must accept onSelectSkill prop");
expect(src.includes("onSelectSkill"), "must accept onSelectSkill prop").toBe(true);
});
});
@@ -247,18 +252,18 @@ describe("OmniExecutionsTab.tsx", () => {
const src = readFileSync(resolve(join(base, "components/OmniExecutionsTab.tsx")), "utf-8");
it("starts with 'use client'", () => {
assert.ok(src.startsWith('"use client"'), "must be a client component");
expect(src.startsWith('"use client"'), "must be a client component").toBe(true);
});
it("renders a table with skill/status/duration/time columns", () => {
assert.ok(src.includes("{t(\"skill\")}"), "must have skill column");
assert.ok(src.includes("{t(\"status\")}"), "must have status column");
assert.ok(src.includes("{t(\"duration\")}"), "must have duration column");
expect(src.includes('{t("skill")}'), "must have skill column").toBe(true);
expect(src.includes('{t("status")}'), "must have status column").toBe(true);
expect(src.includes('{t("duration")}'), "must have duration column").toBe(true);
});
it("has pagination buttons", () => {
assert.ok(src.includes("onPagePrev"), "must accept onPagePrev handler");
assert.ok(src.includes("onPageNext"), "must accept onPageNext handler");
expect(src.includes("onPagePrev"), "must accept onPagePrev handler").toBe(true);
expect(src.includes("onPageNext"), "must accept onPageNext handler").toBe(true);
});
});
@@ -268,13 +273,13 @@ describe("OmniSandboxTab.tsx", () => {
const src = readFileSync(resolve(join(base, "components/OmniSandboxTab.tsx")), "utf-8");
it("starts with 'use client'", () => {
assert.ok(src.startsWith('"use client"'), "must be a client component");
expect(src.startsWith('"use client"'), "must be a client component").toBe(true);
});
it("shows sandbox config values", () => {
assert.ok(src.includes("100ms"), "must show 100ms CPU limit");
assert.ok(src.includes("256MB"), "must show 256MB memory limit");
assert.ok(src.includes("30s"), "must show 30s timeout");
expect(src.includes("100ms"), "must show 100ms CPU limit").toBe(true);
expect(src.includes("256MB"), "must show 256MB memory limit").toBe(true);
expect(src.includes("30s"), "must show 30s timeout").toBe(true);
});
});
@@ -284,42 +289,41 @@ describe("OmniMarketplaceTab.tsx", () => {
const src = readFileSync(resolve(join(base, "components/OmniMarketplaceTab.tsx")), "utf-8");
it("starts with 'use client'", () => {
assert.ok(src.startsWith('"use client"'), "must be a client component");
expect(src.startsWith('"use client"'), "must be a client component").toBe(true);
});
it("has marketplace search logic", () => {
assert.ok(
expect(
src.includes("/api/skills/marketplace"),
"must call /api/skills/marketplace endpoint"
);
).toBe(true);
});
it("has skills.sh search logic", () => {
assert.ok(src.includes("/api/skills/skillssh"), "must call /api/skills/skillssh endpoint");
expect(src.includes("/api/skills/skillssh"), "must call /api/skills/skillssh endpoint").toBe(
true
);
});
it("accepts skillsProvider and onRefreshSkills props", () => {
assert.ok(src.includes("skillsProvider"), "must accept skillsProvider prop");
assert.ok(src.includes("onRefreshSkills"), "must accept onRefreshSkills prop");
expect(src.includes("skillsProvider"), "must accept skillsProvider prop").toBe(true);
expect(src.includes("onRefreshSkills"), "must accept onRefreshSkills prop").toBe(true);
});
});
// ─── E2E test update ──────────────────────────────────────────────────────────
describe("E2E spec path", () => {
const src = readFileSync(
resolve(join(cwd, "tests/e2e/skills-marketplace.spec.ts")),
"utf-8"
);
const src = readFileSync(resolve(join(cwd, "tests/e2e/skills-marketplace.spec.ts")), "utf-8");
it("uses /dashboard/omni-skills (not /dashboard/skills)", () => {
assert.ok(
expect(
src.includes("/dashboard/omni-skills"),
"E2E spec must navigate to /dashboard/omni-skills"
);
assert.ok(
).toBe(true);
expect(
!src.includes('"/dashboard/skills"') && !src.includes("'/dashboard/skills'"),
"E2E spec must not have the old /dashboard/skills path in gotoDashboardRoute call"
);
).toBe(true);
});
});

View File

@@ -1,116 +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";
const cleanupCallbacks: Array<() => void> = [];
function createTestStorage(): Storage {
const entries = new Map<string, string>();
return {
get length() {
return entries.size;
},
clear: () => entries.clear(),
getItem: (key) => entries.get(key) ?? null,
key: (index) => Array.from(entries.keys())[index] ?? null,
removeItem: (key) => {
entries.delete(key);
},
setItem: (key, value) => {
entries.set(key, value);
},
};
}
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => {
container.remove();
});
return container;
}
describe("AutoRoutingBanner", () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.stubGlobal("localStorage", createTestStorage());
localStorage.clear();
});
afterEach(() => {
while (cleanupCallbacks.length > 0) {
cleanupCallbacks.pop()?.();
}
document.body.innerHTML = "";
localStorage.clear();
vi.unstubAllGlobals();
});
it("renders banner on first mount", async () => {
const { default: AutoRoutingBanner } = await import("@/shared/components/AutoRoutingBanner");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<AutoRoutingBanner />);
});
expect(container.querySelector('[role="banner"]')).toBeTruthy();
expect(container.textContent).toContain("Auto-Routing Active");
});
it("includes link to Combos page", async () => {
const { default: AutoRoutingBanner } = await import("@/shared/components/AutoRoutingBanner");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<AutoRoutingBanner />);
});
const link = container.querySelector('a[href="/dashboard/combos"]');
expect(link).toBeTruthy();
});
it("can be dismissed by clicking close button", async () => {
const { default: AutoRoutingBanner } = await import("@/shared/components/AutoRoutingBanner");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<AutoRoutingBanner />);
});
expect(container.querySelector('[role="banner"]')).toBeTruthy();
const closeButton = container.querySelector('button[aria-label="Dismiss auto-routing banner"]');
expect(closeButton).toBeTruthy();
await act(async () => {
closeButton?.click();
});
expect(container.querySelector('[role="banner"]')).toBeFalsy();
});
it("persists dismissal to localStorage", async () => {
const { default: AutoRoutingBanner } = await import("@/shared/components/AutoRoutingBanner");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<AutoRoutingBanner />);
});
const closeButton = container.querySelector('button[aria-label="Dismiss auto-routing banner"]');
await act(async () => {
closeButton?.click();
});
expect(localStorage.getItem("auto-routing-banner-dismissed")).toBe("true");
});
it("remains hidden after dismissal on remount", async () => {
localStorage.setItem("auto-routing-banner-dismissed", "true");
const { default: AutoRoutingBanner } = await import("@/shared/components/AutoRoutingBanner");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<AutoRoutingBanner />);
});
expect(container.querySelector('[role="banner"]')).toBeFalsy();
});
});

View File

@@ -21,7 +21,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// ---------------------------------------------------------------------------
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useTranslations: () => {
const t = (key: string) => key;
t.has = () => true;
return t;
},
useLocale: () => "en",
}));
// Stub shared components
@@ -91,11 +96,10 @@ async function renderComponent(
forceOpen?: boolean;
inputContent?: string;
onOpenChange?: (open: boolean) => void;
} = {},
} = {}
) {
const { default: CompressionPreviewAccordion } = await import(
"@/app/(dashboard)/dashboard/translator/components/advanced/CompressionPreviewAccordion"
);
const { default: CompressionPreviewAccordion } =
await import("@/app/(dashboard)/dashboard/translator/components/advanced/CompressionPreviewAccordion");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
@@ -106,9 +110,7 @@ async function renderComponent(
/** Click the accordion toggle button to open/close it. */
async function clickToggle(container: HTMLElement) {
const btn = container.querySelector(
"button[aria-expanded]",
) as HTMLButtonElement | null;
const btn = container.querySelector("button[aria-expanded]") as HTMLButtonElement | null;
expect(btn).toBeTruthy();
await act(async () => {
btn?.click();
@@ -137,9 +139,8 @@ afterEach(() => {
describe("CompressionPreviewAccordion — export", () => {
it("exports a default function component", async () => {
const mod = await import(
"@/app/(dashboard)/dashboard/translator/components/advanced/CompressionPreviewAccordion"
);
const mod =
await import("@/app/(dashboard)/dashboard/translator/components/advanced/CompressionPreviewAccordion");
expect(typeof mod.default).toBe("function");
});
});
@@ -345,7 +346,7 @@ describe("CompressionPreviewAccordion — Preview fetch", () => {
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({ "Content-Type": "application/json" }),
}),
})
);
// Verify body has correct shape
@@ -534,7 +535,7 @@ describe("CompressionPreviewAccordion — error path (Hard Rule #12)", () => {
it("error message does NOT contain stack-trace lines (at /path/...)", async () => {
const stackError = new Error(
"Something went wrong\n at /home/user/app/src/file.ts:42:13\n at Object.<anonymous> /home/user/app/tests/test.ts:10:5",
"Something went wrong\n at /home/user/app/src/file.ts:42:13\n at Object.<anonymous> /home/user/app/tests/test.ts:10:5"
);
const fetchMock = vi.fn().mockRejectedValue(stackError);
vi.stubGlobal("fetch", fetchMock);
@@ -578,7 +579,7 @@ describe("CompressionPreviewAccordion — error path (Hard Rule #12)", () => {
const errorEl = container.querySelector("[role='alert']");
expect(errorEl).toBeTruthy();
expect(errorEl?.textContent).toContain("Preview failed");
expect(errorEl?.textContent).toContain("compressionPreviewFailed");
vi.unstubAllGlobals();
});

View File

@@ -6,18 +6,21 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Minimal i18n stub — returns the key so tests can assert on fallback rendering
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
useTranslations: () => {
const t = (key: string) => key;
t.has = () => false;
return t;
},
}));
// Minimal shared component stubs — Card wraps children, Tooltip passes through
vi.mock("@/shared/components", () => ({
Card: ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => <div data-testid="card" className={className}>{children}</div>,
Card: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div data-testid="card" className={className}>
{children}
</div>
),
}));
vi.mock("@/shared/components/Tooltip", () => ({
@@ -50,16 +53,14 @@ describe("TranslatorConceptCard", () => {
});
it("exports a default function component", { timeout: 30000 }, async () => {
const mod = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard"
);
const mod =
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
expect(typeof mod.default).toBe("function");
});
it("renders the card with info icon and headline", async () => {
const { default: TranslatorConceptCard } = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard"
);
const { default: TranslatorConceptCard } =
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
@@ -74,9 +75,8 @@ describe("TranslatorConceptCard", () => {
});
it("renders the flow diagram with 4 FlowNode elements (app, source, hub, target)", async () => {
const { default: TranslatorConceptCard } = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard"
);
const { default: TranslatorConceptCard } =
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
@@ -92,9 +92,8 @@ describe("TranslatorConceptCard", () => {
});
it("renders the diagram with arrow_forward separators", async () => {
const { default: TranslatorConceptCard } = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard"
);
const { default: TranslatorConceptCard } =
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
@@ -107,16 +106,15 @@ describe("TranslatorConceptCard", () => {
});
it("toggle button starts collapsed (aria-expanded=false)", async () => {
const { default: TranslatorConceptCard } = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard"
);
const { default: TranslatorConceptCard } =
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<TranslatorConceptCard />);
});
const toggleBtn = container.querySelector(
"button[aria-controls='translator-concept-how-it-works']",
"button[aria-controls='translator-concept-how-it-works']"
);
expect(toggleBtn).toBeTruthy();
expect(toggleBtn?.getAttribute("aria-expanded")).toBe("false");
@@ -125,16 +123,15 @@ describe("TranslatorConceptCard", () => {
});
it("toggle expands 'Como funciona' section and sets aria-expanded=true", async () => {
const { default: TranslatorConceptCard } = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard"
);
const { default: TranslatorConceptCard } =
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<TranslatorConceptCard />);
});
const toggleBtn = container.querySelector(
"button[aria-controls='translator-concept-how-it-works']",
"button[aria-controls='translator-concept-how-it-works']"
) as HTMLButtonElement | null;
expect(toggleBtn).toBeTruthy();
@@ -149,16 +146,15 @@ describe("TranslatorConceptCard", () => {
});
it("toggle collapses section on second click and restores aria-expanded=false", async () => {
const { default: TranslatorConceptCard } = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard"
);
const { default: TranslatorConceptCard } =
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<TranslatorConceptCard />);
});
const toggleBtn = container.querySelector(
"button[aria-controls='translator-concept-how-it-works']",
"button[aria-controls='translator-concept-how-it-works']"
) as HTMLButtonElement | null;
// Expand
@@ -176,9 +172,8 @@ describe("TranslatorConceptCard", () => {
});
it("toggle button icon changes between expand_more and expand_less", async () => {
const { default: TranslatorConceptCard } = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard"
);
const { default: TranslatorConceptCard } =
await import("@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
@@ -186,7 +181,7 @@ describe("TranslatorConceptCard", () => {
});
const toggleBtn = container.querySelector(
"button[aria-controls='translator-concept-how-it-works']",
"button[aria-controls='translator-concept-how-it-works']"
) as HTMLButtonElement | null;
// Initially collapsed: should show expand_more
@@ -221,16 +216,14 @@ describe("TranslateFlowDiagram", () => {
});
it("exports a default function component", async () => {
const mod = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram"
);
const mod =
await import("@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram");
expect(typeof mod.default).toBe("function");
});
it("renders all 4 flow node icons (app, source, hub, target)", async () => {
const { default: TranslateFlowDiagram } = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram"
);
const { default: TranslateFlowDiagram } =
await import("@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
@@ -246,9 +239,8 @@ describe("TranslateFlowDiagram", () => {
});
it("renders exactly 3 arrow_forward separators between 4 nodes", async () => {
const { default: TranslateFlowDiagram } = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram"
);
const { default: TranslateFlowDiagram } =
await import("@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
@@ -260,9 +252,8 @@ describe("TranslateFlowDiagram", () => {
});
it("renders a responsive grid container", async () => {
const { default: TranslateFlowDiagram } = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram"
);
const { default: TranslateFlowDiagram } =
await import("@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
@@ -276,9 +267,8 @@ describe("TranslateFlowDiagram", () => {
});
it("i18n fallback: renders labels using fallback strings when translations return keys", async () => {
const { default: TranslateFlowDiagram } = await import(
"@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram"
);
const { default: TranslateFlowDiagram } =
await import("@/app/(dashboard)/dashboard/translator/components/TranslateFlowDiagram");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
@@ -287,13 +277,13 @@ describe("TranslateFlowDiagram", () => {
// When mock returns key, tr() detects key === translation and uses fallback
// The fallback text should appear in the DOM
const text = container.textContent ?? "";
expect(text).toContain("Sua app");
expect(text).toContain("Your app");
expect(text).toContain("ex: SDK Anthropic");
expect(text).toContain("Formato origem");
expect(text).toContain("Source format");
expect(text).toContain("claude");
// 4th node: OpenAI hub
expect(text).toContain("OpenAI (hub)");
expect(text).toContain("Provider destino");
expect(text).toContain("Target provider");
expect(text).toContain("Gemini");
});
});

View File

@@ -6,7 +6,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// ── i18n stub — returns fallback key so we can assert on translateOrFallback ──
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
useTranslations: () => {
const t = (key: string) => key;
t.has = () => false;
return t;
},
}));
// ── Shared component stubs ────────────────────────────────────────────────────
@@ -58,16 +63,13 @@ vi.mock("@/shared/components", () => ({
}));
// ── FORMAT_META stub ──────────────────────────────────────────────────────────
vi.mock(
"@/app/(dashboard)/dashboard/translator/exampleTemplates",
() => ({
FORMAT_META: {
openai: { label: "OpenAI", color: "green" },
claude: { label: "Claude", color: "orange" },
gemini: { label: "Gemini", color: "blue" },
},
}),
);
vi.mock("@/app/(dashboard)/dashboard/translator/exampleTemplates", () => ({
FORMAT_META: {
openai: { label: "OpenAI", color: "green" },
claude: { label: "Claude", color: "orange" },
gemini: { label: "Gemini", color: "blue" },
},
}));
// ── fetch mock helpers ────────────────────────────────────────────────────────
function mockFetchEmpty() {
@@ -76,7 +78,7 @@ function mockFetchEmpty() {
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ success: true, events: [] }),
}),
})
);
}
@@ -90,14 +92,14 @@ function mockFetchWithEvents(
targetFormat?: string;
status?: string;
latency?: number;
}>,
}>
) {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ success: true, events }),
}),
})
);
}
@@ -120,7 +122,7 @@ function makeContainer(): HTMLElement {
*/
async function mountAndFlushInitialFetch(
component: React.ReactElement,
container: HTMLElement,
container: HTMLElement
): Promise<ReturnType<typeof createRoot>> {
const root = createRoot(container);
await act(async () => {
@@ -137,8 +139,9 @@ async function mountAndFlushInitialFetch(
describe("MonitorTab", () => {
beforeEach(() => {
vi.useFakeTimers();
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
@@ -152,17 +155,14 @@ describe("MonitorTab", () => {
// ── 1. Smoke render ──────────────────────────────────────────────────────────
it("exports a default function component", async () => {
const mod = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const mod = await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
expect(typeof mod.default).toBe("function");
});
it("renders the origin hint header (monitorOriginHint) always visible", async () => {
mockFetchEmpty();
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
await mountAndFlushInitialFetch(<MonitorTab />, container);
@@ -176,9 +176,8 @@ describe("MonitorTab", () => {
it("renders 6 StatCards with correct icons", async () => {
mockFetchEmpty();
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
await mountAndFlushInitialFetch(<MonitorTab />, container);
@@ -196,9 +195,8 @@ describe("MonitorTab", () => {
// ── 2. Empty state ───────────────────────────────────────────────────────────
it("shows empty state when events array is empty", async () => {
mockFetchEmpty();
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
await mountAndFlushInitialFetch(<MonitorTab />, container);
@@ -210,30 +208,30 @@ describe("MonitorTab", () => {
it("empty state shows CTA description text from monitorEmptyCta", async () => {
mockFetchEmpty();
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
await mountAndFlushInitialFetch(<MonitorTab />, container);
// When t() mock returns key, translateOrFallback detects key === translation and uses hardcoded fallback
const emptyDescription = container.querySelector("[data-testid='empty-description']");
expect(emptyDescription?.textContent).toContain("Volte para a aba Translate");
expect(emptyDescription?.textContent).toContain("Go back to the Translate tab");
});
it("empty state 'Ir para Translate' button calls onGoToTranslate callback", async () => {
it("empty state 'Go to Translate' button calls onGoToTranslate callback", async () => {
mockFetchEmpty();
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
const onGoToTranslate = vi.fn();
await mountAndFlushInitialFetch(<MonitorTab onGoToTranslate={onGoToTranslate} />, container);
const actionBtn = container.querySelector("[data-testid='empty-action']") as HTMLButtonElement | null;
const actionBtn = container.querySelector(
"[data-testid='empty-action']"
) as HTMLButtonElement | null;
expect(actionBtn).toBeTruthy();
// Label comes from monitorOpenTranslateButton fallback
expect(actionBtn?.textContent).toContain("Ir para Translate");
expect(actionBtn?.textContent).toContain("Go to Translate");
await act(async () => {
actionBtn?.click();
@@ -243,9 +241,8 @@ describe("MonitorTab", () => {
it("empty state action button is not rendered when onGoToTranslate is not provided", async () => {
mockFetchEmpty();
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
await mountAndFlushInitialFetch(<MonitorTab />, container);
@@ -280,9 +277,8 @@ describe("MonitorTab", () => {
];
mockFetchWithEvents(sampleEvents);
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
await mountAndFlushInitialFetch(<MonitorTab />, container);
@@ -309,9 +305,8 @@ describe("MonitorTab", () => {
];
mockFetchWithEvents(sampleEvents);
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
await mountAndFlushInitialFetch(<MonitorTab />, container);
@@ -324,9 +319,8 @@ describe("MonitorTab", () => {
const sampleEvents = [{ id: "x", status: "success" }];
mockFetchWithEvents(sampleEvents);
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
await mountAndFlushInitialFetch(<MonitorTab />, container);
@@ -343,14 +337,13 @@ describe("MonitorTab", () => {
// ── 4. Auto-refresh toggle ───────────────────────────────────────────────────
it("toggle auto-refresh button is present with aria-label and shows live state text", async () => {
mockFetchEmpty();
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
await mountAndFlushInitialFetch(<MonitorTab />, container);
const toggleBtn = container.querySelector(
"[data-testid='auto-refresh-toggle']",
"[data-testid='auto-refresh-toggle']"
) as HTMLButtonElement | null;
expect(toggleBtn).toBeTruthy();
@@ -362,14 +355,13 @@ describe("MonitorTab", () => {
it("clicking toggle changes button text from live to paused", async () => {
mockFetchEmpty();
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
await mountAndFlushInitialFetch(<MonitorTab />, container);
const toggleBtn = container.querySelector(
"[data-testid='auto-refresh-toggle']",
"[data-testid='auto-refresh-toggle']"
) as HTMLButtonElement | null;
expect(toggleBtn).toBeTruthy();
@@ -389,9 +381,8 @@ describe("MonitorTab", () => {
});
vi.stubGlobal("fetch", fetchMock);
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
await mountAndFlushInitialFetch(<MonitorTab />, container);
@@ -412,15 +403,14 @@ describe("MonitorTab", () => {
});
vi.stubGlobal("fetch", fetchMock);
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
await mountAndFlushInitialFetch(<MonitorTab />, container);
// Pause auto-refresh
const toggleBtn = container.querySelector(
"[data-testid='auto-refresh-toggle']",
"[data-testid='auto-refresh-toggle']"
) as HTMLButtonElement | null;
await act(async () => {
toggleBtn?.click();
@@ -452,14 +442,13 @@ describe("MonitorTab", () => {
it("fetch error does not leak stack traces into the DOM", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockRejectedValue(
new Error("Network Error\n at fetch (/some/internal/path.ts:42:10)"),
),
vi
.fn()
.mockRejectedValue(new Error("Network Error\n at fetch (/some/internal/path.ts:42:10)"))
);
const { default: MonitorTab } = await import(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab"
);
const { default: MonitorTab } =
await import("@/app/(dashboard)/dashboard/translator/components/MonitorTab");
const container = makeContainer();
// Don't use mountAndFlushInitialFetch here — we want to let the rejection settle
const root = createRoot(container);

View File

@@ -105,6 +105,7 @@ export default defineConfig({
"tests/unit/ui/combos-page-smoke.test.tsx", // #8618 — pre-existing failure; remove this exclusion when fixed
"tests/unit/ui/evals-tab-smoke.test.tsx", // #8618 — pre-existing failure; remove this exclusion when fixed
],
coverage: {
reportsDirectory: "coverage",
},