mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 17:52:31 +03:00
fix(test): revive orphaned vitest tests and fix CI routing (#8718)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user