646 lines
23 KiB
TypeScript
646 lines
23 KiB
TypeScript
// @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";
|
|
import type { Root } from "react-dom/client";
|
|
import type { AgentSkill, SkillCoverage } from "../../src/lib/agentSkills/types";
|
|
|
|
// ── i18n stub ────────────────────────────────────────────────────────────────
|
|
vi.mock("next-intl", () => ({
|
|
useTranslations: () => (key: string) => key,
|
|
}));
|
|
|
|
// ── next/link stub ───────────────────────────────────────────────────────────
|
|
vi.mock("next/link", () => ({
|
|
default: ({
|
|
href,
|
|
children,
|
|
className,
|
|
}: {
|
|
href: string;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) => (
|
|
<a href={href} className={className}>
|
|
{children}
|
|
</a>
|
|
),
|
|
}));
|
|
|
|
// ── next/dynamic stub — renders placeholder immediately ───────────────────────
|
|
vi.mock("next/dynamic", () => ({
|
|
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>;
|
|
};
|
|
},
|
|
}));
|
|
|
|
// ── Fixture helpers ──────────────────────────────────────────────────────────
|
|
|
|
function makeSkill(overrides: Partial<AgentSkill> = {}): AgentSkill {
|
|
return {
|
|
id: "omni-providers",
|
|
name: "Providers",
|
|
description: "Manage provider connections and API keys.",
|
|
category: "api",
|
|
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",
|
|
githubUrl: "https://github.com/diegosouzapw/OmniRoute/blob/main/skills/omni-providers/SKILL.md",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function make42Skills(): AgentSkill[] {
|
|
const skills: AgentSkill[] = [];
|
|
for (let i = 0; i < 22; i++) {
|
|
skills.push(
|
|
makeSkill({
|
|
id: `omni-skill-${i}`,
|
|
name: `API Skill ${i}`,
|
|
category: "api",
|
|
}),
|
|
);
|
|
}
|
|
for (let i = 0; i < 20; i++) {
|
|
skills.push(
|
|
makeSkill({
|
|
id: `cli-skill-${i}`,
|
|
name: `CLI Skill ${i}`,
|
|
category: "cli",
|
|
endpoints: undefined,
|
|
cliCommands: [`skill${i} run`, `skill${i} status`],
|
|
}),
|
|
);
|
|
}
|
|
return skills;
|
|
}
|
|
|
|
const FULL_COVERAGE: SkillCoverage = {
|
|
api: { have: 22, total: 22 },
|
|
cli: { have: 20, total: 20 },
|
|
totalSkills: 42,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const PARTIAL_COVERAGE: SkillCoverage = {
|
|
api: { have: 10, total: 22 },
|
|
cli: { have: 8, total: 20 },
|
|
totalSkills: 18,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
// ── Fetch mock factory ───────────────────────────────────────────────────────
|
|
|
|
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") {
|
|
return new Response(JSON.stringify({ skills, coverage }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
if (/\/api\/agent-skills\/.+\/raw/.test(urlStr)) {
|
|
return new Response(rawMarkdown, {
|
|
status: 200,
|
|
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({}), { status: 404 });
|
|
});
|
|
}
|
|
|
|
// ── Test setup ───────────────────────────────────────────────────────────────
|
|
|
|
const cleanupCallbacks: Array<() => void> = [];
|
|
let root: Root | null = null;
|
|
|
|
function makeContainer(): HTMLElement {
|
|
const container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
cleanupCallbacks.push(() => container.remove());
|
|
return container;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
(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) },
|
|
configurable: true,
|
|
});
|
|
// Mock window.location.origin
|
|
Object.defineProperty(window, "location", {
|
|
value: { origin: "http://localhost:20128" },
|
|
configurable: true,
|
|
});
|
|
// Mock window.confirm
|
|
vi.spyOn(window, "confirm").mockReturnValue(false);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (root) {
|
|
await act(async () => {
|
|
root?.unmount();
|
|
});
|
|
root = null;
|
|
}
|
|
while (cleanupCallbacks.length > 0) {
|
|
cleanupCallbacks.pop()?.();
|
|
}
|
|
document.body.innerHTML = "";
|
|
vi.clearAllMocks();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
describe("AgentSkillsPageClient", () => {
|
|
it("renders 42 skill cards after fetch resolves", async () => {
|
|
const skills = make42Skills();
|
|
vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE));
|
|
|
|
const { AgentSkillsPageClient } = await import(
|
|
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
|
|
);
|
|
const container = makeContainer();
|
|
root = createRoot(container);
|
|
await act(async () => {
|
|
root?.render(<AgentSkillsPageClient />);
|
|
});
|
|
|
|
const cards = container.querySelectorAll("[data-testid^='skill-card-']");
|
|
expect(cards.length).toBe(42);
|
|
});
|
|
|
|
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 container = makeContainer();
|
|
root = createRoot(container);
|
|
await act(async () => {
|
|
root?.render(<AgentSkillsPageClient />);
|
|
});
|
|
|
|
// SkillsConceptCard uses i18n key conceptCard.agent.title
|
|
expect(container.textContent).toContain("conceptCard.agent.title");
|
|
});
|
|
|
|
it("filter API shows only api-category cards", async () => {
|
|
const skills = make42Skills();
|
|
vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE));
|
|
|
|
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;
|
|
expect(filterApiBtn).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
filterApiBtn?.click();
|
|
});
|
|
|
|
const cards = container.querySelectorAll("[data-testid^='skill-card-']");
|
|
expect(cards.length).toBe(22);
|
|
});
|
|
|
|
it("filter CLI shows only cli-category cards", async () => {
|
|
const skills = make42Skills();
|
|
vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE));
|
|
|
|
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;
|
|
await act(async () => {
|
|
filterCliBtn?.click();
|
|
});
|
|
|
|
const cards = container.querySelectorAll("[data-testid^='skill-card-']");
|
|
expect(cards.length).toBe(20);
|
|
});
|
|
|
|
it("clicking a card triggers preview fetch after 200ms debounce", async () => {
|
|
vi.useFakeTimers();
|
|
const skills = make42Skills();
|
|
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 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;
|
|
expect(firstCard).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
firstCard?.click();
|
|
});
|
|
|
|
// 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"),
|
|
);
|
|
expect(rawFetchCallsBefore.length).toBe(0);
|
|
|
|
// Advance timers past the 200ms debounce
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(200);
|
|
});
|
|
|
|
// 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"),
|
|
);
|
|
expect(rawFetchCallsAfter.length).toBeGreaterThan(0);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
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 container = makeContainer();
|
|
root = createRoot(container);
|
|
await act(async () => {
|
|
root?.render(<AgentSkillsPageClient />);
|
|
});
|
|
|
|
const emptyState = container.querySelector("[data-testid='skill-preview-empty']");
|
|
expect(emptyState).not.toBeNull();
|
|
});
|
|
|
|
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 container = makeContainer();
|
|
root = createRoot(container);
|
|
await act(async () => {
|
|
root?.render(<AgentSkillsPageClient />);
|
|
});
|
|
|
|
const coverageBar = container.querySelector("[data-testid='coverage-bar']");
|
|
expect(coverageBar).not.toBeNull();
|
|
|
|
const progressBars = container.querySelectorAll("[role='progressbar']");
|
|
expect(progressBars.length).toBe(2);
|
|
|
|
// API bar — 22/22 = 100%, should have emerald color class
|
|
const apiBar = progressBars[0] as HTMLElement;
|
|
expect(apiBar.className).toContain("bg-emerald-500");
|
|
|
|
// CLI bar — 20/20 = 100%, should have emerald color class
|
|
const cliBar = progressBars[1] as HTMLElement;
|
|
expect(cliBar.className).toContain("bg-emerald-500");
|
|
});
|
|
|
|
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 container = makeContainer();
|
|
root = createRoot(container);
|
|
await act(async () => {
|
|
root?.render(<AgentSkillsPageClient />);
|
|
});
|
|
|
|
const generateBtn = container.querySelector("[data-testid='generate-button']");
|
|
expect(generateBtn).toBeNull();
|
|
});
|
|
|
|
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 container = makeContainer();
|
|
root = createRoot(container);
|
|
await act(async () => {
|
|
root?.render(<AgentSkillsPageClient />);
|
|
});
|
|
|
|
const generateBtn = container.querySelector("[data-testid='generate-button']");
|
|
expect(generateBtn).not.toBeNull();
|
|
});
|
|
|
|
it("search filters cards by name", async () => {
|
|
const skills = make42Skills();
|
|
vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE));
|
|
|
|
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;
|
|
expect(searchInput).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
if (searchInput) {
|
|
searchInput.value = "API Skill 0";
|
|
searchInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
// React uses onChange
|
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
window.HTMLInputElement.prototype,
|
|
"value",
|
|
)?.set;
|
|
nativeInputValueSetter?.call(searchInput, "API Skill 0");
|
|
searchInput.dispatchEvent(new Event("change", { bubbles: true }));
|
|
}
|
|
});
|
|
|
|
// After search, cards with "API Skill 0" in name should be visible
|
|
// (at minimum the one exact match)
|
|
const cards = container.querySelectorAll("[data-testid^='skill-card-']");
|
|
expect(cards.length).toBeLessThanOrEqual(42);
|
|
});
|
|
|
|
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 container = makeContainer();
|
|
root = createRoot(container);
|
|
await act(async () => {
|
|
root?.render(<AgentSkillsPageClient />);
|
|
});
|
|
|
|
const linksBar = container.querySelector("[data-testid='mcp-a2a-links-bar']");
|
|
expect(linksBar).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
// ── CoverageBar isolated tests ───────────────────────────────────────────────
|
|
|
|
describe("CoverageBar", () => {
|
|
it("renders two progressbars with correct aria attributes", async () => {
|
|
const { CoverageBar } = await import(
|
|
"../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar"
|
|
);
|
|
const container = makeContainer();
|
|
const localRoot = createRoot(container);
|
|
await act(async () => {
|
|
localRoot.render(<CoverageBar coverage={FULL_COVERAGE} />);
|
|
});
|
|
|
|
const bars = container.querySelectorAll("[role='progressbar']");
|
|
expect(bars.length).toBe(2);
|
|
|
|
const apiBar = bars[0] as HTMLElement;
|
|
expect(apiBar.getAttribute("aria-valuenow")).toBe("22");
|
|
expect(apiBar.getAttribute("aria-valuemax")).toBe("22");
|
|
|
|
const cliBar = bars[1] as HTMLElement;
|
|
expect(cliBar.getAttribute("aria-valuenow")).toBe("20");
|
|
expect(cliBar.getAttribute("aria-valuemax")).toBe("20");
|
|
|
|
await act(async () => localRoot.unmount());
|
|
});
|
|
|
|
it("applies red color class when coverage is below 75%", async () => {
|
|
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 },
|
|
totalSkills: 5,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const container = makeContainer();
|
|
const localRoot = createRoot(container);
|
|
await act(async () => {
|
|
localRoot.render(<CoverageBar coverage={lowCoverage} />);
|
|
});
|
|
|
|
const bars = container.querySelectorAll("[role='progressbar']");
|
|
const apiBar = bars[0] as HTMLElement;
|
|
expect(apiBar.className).toContain("bg-red-500");
|
|
|
|
await act(async () => localRoot.unmount());
|
|
});
|
|
|
|
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 partialCoverage: SkillCoverage = {
|
|
api: { have: 18, total: 22 }, // ~81.8% = amber
|
|
cli: { have: 15, total: 20 }, // 75% = amber
|
|
totalSkills: 33,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const container = makeContainer();
|
|
const localRoot = createRoot(container);
|
|
await act(async () => {
|
|
localRoot.render(<CoverageBar coverage={partialCoverage} />);
|
|
});
|
|
|
|
const bars = container.querySelectorAll("[role='progressbar']");
|
|
const apiBar = bars[0] as HTMLElement;
|
|
expect(apiBar.className).toContain("bg-amber-400");
|
|
|
|
await act(async () => localRoot.unmount());
|
|
});
|
|
});
|
|
|
|
// ── SkillCard isolated tests ─────────────────────────────────────────────────
|
|
|
|
describe("SkillCard", () => {
|
|
it("renders skill name and description", async () => {
|
|
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);
|
|
await act(async () => {
|
|
localRoot.render(<SkillCard skill={skill} selected={false} onClick={() => {}} />);
|
|
});
|
|
|
|
expect(container.textContent).toContain("Providers");
|
|
expect(container.textContent).toContain("Manage connections");
|
|
|
|
await act(async () => localRoot.unmount());
|
|
});
|
|
|
|
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 container = makeContainer();
|
|
const localRoot = createRoot(container);
|
|
await act(async () => {
|
|
localRoot.render(<SkillCard skill={makeSkill()} selected={false} onClick={() => {}} />);
|
|
});
|
|
|
|
const btn = container.querySelector("[role='button']") as HTMLElement | null;
|
|
expect(btn).not.toBeNull();
|
|
expect(btn?.getAttribute("aria-pressed")).toBe("false");
|
|
|
|
await act(async () => localRoot.unmount());
|
|
});
|
|
|
|
it("has aria-pressed=true when selected", async () => {
|
|
const { SkillCard } = await import(
|
|
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"
|
|
);
|
|
const container = makeContainer();
|
|
const localRoot = createRoot(container);
|
|
await act(async () => {
|
|
localRoot.render(<SkillCard skill={makeSkill()} selected={true} onClick={() => {}} />);
|
|
});
|
|
|
|
const btn = container.querySelector("[role='button']") as HTMLElement | null;
|
|
expect(btn?.getAttribute("aria-pressed")).toBe("true");
|
|
|
|
await act(async () => localRoot.unmount());
|
|
});
|
|
|
|
it("calls onClick when clicked", async () => {
|
|
const { SkillCard } = await import(
|
|
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"
|
|
);
|
|
const handleClick = vi.fn();
|
|
const container = makeContainer();
|
|
const localRoot = createRoot(container);
|
|
await act(async () => {
|
|
localRoot.render(<SkillCard skill={makeSkill()} selected={false} onClick={handleClick} />);
|
|
});
|
|
|
|
const btn = container.querySelector("[role='button']") as HTMLElement | null;
|
|
await act(async () => btn?.click());
|
|
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
|
|
await act(async () => localRoot.unmount());
|
|
});
|
|
|
|
it("shows first 2 endpoints as chips for API skill", async () => {
|
|
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"],
|
|
});
|
|
const container = makeContainer();
|
|
const localRoot = createRoot(container);
|
|
await act(async () => {
|
|
localRoot.render(<SkillCard skill={skill} selected={false} onClick={() => {}} />);
|
|
});
|
|
|
|
const chips = container.querySelectorAll("code");
|
|
expect(chips.length).toBe(2);
|
|
expect(chips[0].textContent).toBe("POST /api/providers");
|
|
expect(chips[1].textContent).toBe("GET /api/providers");
|
|
|
|
await act(async () => localRoot.unmount());
|
|
});
|
|
});
|
|
|
|
// ── SkillPreviewPane isolated tests ──────────────────────────────────────────
|
|
|
|
describe("SkillPreviewPane", () => {
|
|
it("renders empty state when skillId is null", async () => {
|
|
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} />,
|
|
);
|
|
});
|
|
|
|
const empty = container.querySelector("[data-testid='skill-preview-empty']");
|
|
expect(empty).not.toBeNull();
|
|
expect(container.textContent).toContain("previewEmpty");
|
|
|
|
await act(async () => localRoot.unmount());
|
|
});
|
|
|
|
it("renders markdown when skillId and markdown are provided", async () => {
|
|
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="# Providers\nContent here."
|
|
loading={false}
|
|
/>,
|
|
);
|
|
});
|
|
|
|
const preview = container.querySelector("[data-testid='skill-preview-pane']");
|
|
expect(preview).not.toBeNull();
|
|
expect(container.textContent).toContain("Providers");
|
|
|
|
await act(async () => localRoot.unmount());
|
|
});
|
|
|
|
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 container = makeContainer();
|
|
const localRoot = createRoot(container);
|
|
await act(async () => {
|
|
localRoot.render(
|
|
<SkillPreviewPane skillId="omni-providers" markdown="" loading={false} />,
|
|
);
|
|
});
|
|
|
|
// markdown is "" (falsy) — should show error state
|
|
const errorEl = container.querySelector("[data-testid='skill-preview-error']");
|
|
expect(errorEl).not.toBeNull();
|
|
|
|
await act(async () => localRoot.unmount());
|
|
});
|
|
});
|