206 lines
6.5 KiB
TypeScript
206 lines
6.5 KiB
TypeScript
import { expect, test, type Route } from "@playwright/test";
|
|
import { gotoDashboardRoute } from "./helpers/dashboardAuth";
|
|
|
|
const NAVIGATION_TIMEOUT_MS = 300_000;
|
|
|
|
type AgentSkill = {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
category: "api" | "cli";
|
|
area: string;
|
|
icon: string;
|
|
endpoints?: string[];
|
|
cliCommands?: string[];
|
|
rawUrl: string;
|
|
githubUrl: string;
|
|
};
|
|
|
|
type SkillCoverage = {
|
|
api: { have: number; total: number };
|
|
cli: { have: number; total: number };
|
|
totalSkills: number;
|
|
generatedAt: string;
|
|
};
|
|
|
|
function makeAgentSkills(): AgentSkill[] {
|
|
const skills: AgentSkill[] = [];
|
|
for (let i = 0; i < 22; i++) {
|
|
skills.push({
|
|
id: `omni-skill-${i}`,
|
|
name: `API Skill ${i}`,
|
|
description: `API skill description ${i}`,
|
|
category: "api",
|
|
area: `area-${i}`,
|
|
icon: "api",
|
|
endpoints: [`GET /api/skill-${i}`],
|
|
rawUrl: `https://raw.githubusercontent.com/example/OmniRoute/main/skills/omni-skill-${i}/SKILL.md`,
|
|
githubUrl: `https://github.com/example/OmniRoute/blob/main/skills/omni-skill-${i}/SKILL.md`,
|
|
});
|
|
}
|
|
for (let i = 0; i < 20; i++) {
|
|
skills.push({
|
|
id: `cli-skill-${i}`,
|
|
name: `CLI Skill ${i}`,
|
|
description: `CLI skill description ${i}`,
|
|
category: "cli",
|
|
area: `cli-area-${i}`,
|
|
icon: "terminal",
|
|
cliCommands: [`skill${i} run`],
|
|
rawUrl: `https://raw.githubusercontent.com/example/OmniRoute/main/skills/cli-skill-${i}/SKILL.md`,
|
|
githubUrl: `https://github.com/example/OmniRoute/blob/main/skills/cli-skill-${i}/SKILL.md`,
|
|
});
|
|
}
|
|
return skills;
|
|
}
|
|
|
|
const FULL_COVERAGE: SkillCoverage = {
|
|
api: { have: 22, total: 22 },
|
|
cli: { have: 20, total: 20 },
|
|
totalSkills: 42,
|
|
generatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
async function fulfillJson(route: Route, body: unknown, status = 200) {
|
|
await route.fulfill({
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
async function fulfillText(route: Route, body: string, status = 200) {
|
|
await route.fulfill({
|
|
status,
|
|
contentType: "text/markdown; charset=utf-8",
|
|
body,
|
|
});
|
|
}
|
|
|
|
test.describe("Agent Skills page", () => {
|
|
test.setTimeout(600_000);
|
|
|
|
test("renders SkillsConceptCard with data-testid skills-concept-card-agent", async ({
|
|
page,
|
|
}) => {
|
|
const skills = makeAgentSkills();
|
|
|
|
await page.route(/\/api\/agent-skills(?:\?.*)?$/, async (route) => {
|
|
if (route.request().method() === "GET") {
|
|
await fulfillJson(route, { skills, coverage: FULL_COVERAGE });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await gotoDashboardRoute(page, "/dashboard/agent-skills", {
|
|
timeoutMs: NAVIGATION_TIMEOUT_MS,
|
|
});
|
|
|
|
const conceptCard = page.locator("[data-testid='skills-concept-card-agent']");
|
|
await expect(conceptCard).toBeVisible({ timeout: 15_000 });
|
|
});
|
|
|
|
test("grid shows 42 skill cards when filter is 'all'", async ({ page }) => {
|
|
const skills = makeAgentSkills();
|
|
|
|
await page.route(/\/api\/agent-skills(?:\?.*)?$/, async (route) => {
|
|
if (route.request().method() === "GET") {
|
|
await fulfillJson(route, { skills, coverage: FULL_COVERAGE });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await gotoDashboardRoute(page, "/dashboard/agent-skills", {
|
|
timeoutMs: NAVIGATION_TIMEOUT_MS,
|
|
});
|
|
|
|
// Wait for cards to render
|
|
await expect(page.locator("[data-testid^='skill-card-']").first()).toBeVisible({
|
|
timeout: 15_000,
|
|
});
|
|
|
|
const cards = page.locator("[data-testid^='skill-card-']");
|
|
await expect(cards).toHaveCount(42, { timeout: 15_000 });
|
|
});
|
|
|
|
test("clicking omni-skill-0 card renders markdown in preview pane", async ({
|
|
page,
|
|
}) => {
|
|
const skills = makeAgentSkills();
|
|
const mockMarkdown = "# API Skill 0\n\nThis skill manages connections.";
|
|
|
|
await page.route(/\/api\/agent-skills(?:\?.*)?$/, async (route) => {
|
|
if (route.request().method() === "GET") {
|
|
await fulfillJson(route, { skills, coverage: FULL_COVERAGE });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await page.route(/\/api\/agent-skills\/omni-skill-0\/raw/, async (route) => {
|
|
await fulfillText(route, mockMarkdown);
|
|
});
|
|
|
|
await gotoDashboardRoute(page, "/dashboard/agent-skills", {
|
|
timeoutMs: NAVIGATION_TIMEOUT_MS,
|
|
});
|
|
|
|
// Wait for cards to render and click first card
|
|
const firstCard = page.locator("[data-testid='skill-card-omni-skill-0']");
|
|
await expect(firstCard).toBeVisible({ timeout: 15_000 });
|
|
await firstCard.click();
|
|
|
|
// Preview pane should show non-empty content
|
|
const previewPane = page.locator("[data-testid='skill-preview-pane']");
|
|
await expect(previewPane).toBeVisible({ timeout: 15_000 });
|
|
const previewText = await previewPane.textContent();
|
|
expect(previewText?.trim().length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("cross-link 'Understand the difference' navigates to /dashboard/omni-skills", async ({
|
|
page,
|
|
}) => {
|
|
const skills = makeAgentSkills();
|
|
|
|
await page.route(/\/api\/agent-skills(?:\?.*)?$/, async (route) => {
|
|
if (route.request().method() === "GET") {
|
|
await fulfillJson(route, { skills, coverage: FULL_COVERAGE });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
await gotoDashboardRoute(page, "/dashboard/agent-skills", {
|
|
timeoutMs: NAVIGATION_TIMEOUT_MS,
|
|
});
|
|
|
|
const conceptCard = page.locator("[data-testid='skills-concept-card-agent']");
|
|
await expect(conceptCard).toBeVisible({ timeout: 15_000 });
|
|
|
|
// Find the cross-link inside the concept card
|
|
const crossLink = conceptCard.locator("a");
|
|
await expect(crossLink).toBeVisible({ timeout: 5_000 });
|
|
|
|
await crossLink.click();
|
|
await page.waitForURL(/\/dashboard\/omni-skills/, { timeout: 15_000 });
|
|
expect(page.url()).toContain("/dashboard/omni-skills");
|
|
});
|
|
|
|
test("/dashboard/skills redirects to /dashboard/omni-skills", async ({ page }) => {
|
|
await page.goto("/dashboard/skills", { waitUntil: "commit", timeout: NAVIGATION_TIMEOUT_MS });
|
|
// Next.js redirects /dashboard/skills → /dashboard/omni-skills (next.config.mjs).
|
|
// If auth is required the app then client-redirects to /login (bare path, no /dashboard/ prefix).
|
|
await page.waitForURL(/\/(login|onboarding|dashboard\/(omni-skills|onboarding))/, {
|
|
timeout: 15_000,
|
|
});
|
|
const finalUrl = page.url();
|
|
expect(
|
|
finalUrl.includes("/dashboard/omni-skills") ||
|
|
finalUrl.includes("/login") ||
|
|
finalUrl.includes("/onboarding"),
|
|
).toBe(true);
|
|
});
|
|
});
|