Files
2026-07-13 13:39:12 +08:00

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);
});
});