redesign skills library (#8868)
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
@@ -75,18 +75,28 @@ export const MOCK_PROJECTS = [
|
||||
];
|
||||
|
||||
export const MOCK_SKILLS = [
|
||||
{
|
||||
name: "layout",
|
||||
description: "Improves layout, spacing, and visual hierarchy",
|
||||
instructions:
|
||||
"When asked to improve a UI layout, tighten spacing, strengthen hierarchy, and refine composition.",
|
||||
path: "/mock/.agents/skills/layout/SKILL.md",
|
||||
global: true,
|
||||
},
|
||||
{
|
||||
name: "code-review",
|
||||
description: "Reviews code for quality and best practices",
|
||||
instructions:
|
||||
"When asked to review code, analyze the diff and provide feedback on code quality, potential bugs, and best practices.",
|
||||
path: "/mock/.agents/skills/code-review/SKILL.md",
|
||||
global: true,
|
||||
},
|
||||
{
|
||||
name: "test-writer",
|
||||
description: "Generates unit tests for given code",
|
||||
instructions:
|
||||
"When asked to write tests, generate comprehensive unit tests covering edge cases, happy paths, and error scenarios.",
|
||||
path: "/mock/.agents/skills/test-writer/SKILL.md",
|
||||
path: "/tmp/alpha/.goose/skills/test-writer/SKILL.md",
|
||||
global: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -95,7 +95,7 @@ export function buildInitScript(options?: {
|
||||
description: s.description,
|
||||
content: s.instructions ?? s.content ?? "",
|
||||
directory: (s.path ?? ("/mock/.agents/skills/" + s.name + "/SKILL.md")).replace(/\\/SKILL\\.md$/, ""),
|
||||
global: true,
|
||||
global: s.global ?? true,
|
||||
supportingFiles: [],
|
||||
});
|
||||
|
||||
@@ -196,10 +196,10 @@ export function buildInitScript(options?: {
|
||||
content: message.params?.content ?? "",
|
||||
directory: "/mock/.agents/skills/" + (message.params?.name ?? "new-skill"),
|
||||
global: message.params?.global ?? true,
|
||||
supportingFiles: [],
|
||||
},
|
||||
});
|
||||
case "_goose/sources/update": {
|
||||
case "_goose/sources/update":
|
||||
case "goose/sources/update": {
|
||||
const path = message.params?.path ?? "/mock/.agents/skills/updated-skill";
|
||||
const nextName = message.params?.name;
|
||||
const name =
|
||||
@@ -218,14 +218,16 @@ export function buildInitScript(options?: {
|
||||
description: message.params?.description ?? "",
|
||||
content: message.params?.content ?? "",
|
||||
directory,
|
||||
global: true,
|
||||
global: message.params?.global ?? true,
|
||||
supportingFiles: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
case "_goose/sources/delete":
|
||||
case "goose/sources/delete":
|
||||
return jsonRpcResult(message.id, {});
|
||||
case "_goose/sources/export": {
|
||||
case "_goose/sources/export":
|
||||
case "goose/sources/export": {
|
||||
const path = message.params?.path ?? "/mock/.agents/skills/skill";
|
||||
const name = String(path).split("/").filter(Boolean).at(-1) ?? "skill";
|
||||
return jsonRpcResult(message.id, {
|
||||
|
||||
@@ -6,254 +6,125 @@ import {
|
||||
} from "./fixtures/tauri-mock";
|
||||
|
||||
test.describe("Skills view", () => {
|
||||
test("navigates to skills view from sidebar", async ({
|
||||
test("navigates to skills view and shows the redesigned header", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
|
||||
await expect(page.locator("h1", { hasText: "Skills" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Reusable instructions for your AI agents"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("displays skills from mock data", async ({ tauriMocked: page }) => {
|
||||
await navigateToSkills(page);
|
||||
await expect(page.getByText("code-review")).toBeVisible();
|
||||
await expect(page.getByText("test-writer")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Reviews code for quality and best practices"),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Generates unit tests for given code"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows New Skill and Import buttons", async ({ tauriMocked: page }) => {
|
||||
await navigateToSkills(page);
|
||||
// The header has "New Skill" and "Import" buttons
|
||||
await expect(
|
||||
page.getByRole("button", { name: "New Skill" }).first(),
|
||||
page.getByText(/Skills are reusable instructions/),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Import" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "New Skill" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("opens create skill dialog from header button", async ({
|
||||
test("shows skills in the list and opens a dedicated detail page", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
// Click the first "New Skill" button (in the header)
|
||||
await page.getByRole("button", { name: "New Skill" }).first().click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Open layout details" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Open code-review details" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Open test-writer details" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Open test-writer details" })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Back to skills" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("alpha").first()).toBeVisible();
|
||||
await expect(page.getByText("Quality")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("/tmp/alpha/.goose/skills/test-writer/SKILL.md"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("category filtering isolates inferred groups", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
|
||||
await page.getByRole("button", { name: "Filter by category" }).click();
|
||||
await page.getByRole("menuitemcheckbox", { name: "Design" }).click();
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Open layout details" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Open code-review details" }),
|
||||
).not.toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Open test-writer details" }),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("search filters the list", async ({ tauriMocked: page }) => {
|
||||
await navigateToSkills(page);
|
||||
|
||||
await page.getByPlaceholder("Search skills").fill("review");
|
||||
|
||||
await expect(page.getByText("code-review")).toBeVisible();
|
||||
await expect(page.getByText("test-writer")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("project filtering isolates project skills", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
|
||||
await page
|
||||
.getByRole("main")
|
||||
.getByRole("button", { name: "Alpha", exact: true })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Open test-writer details" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Open code-review details" }),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("opens the create skill dialog", async ({ tauriMocked: page }) => {
|
||||
await navigateToSkills(page);
|
||||
|
||||
await page.getByRole("button", { name: "New Skill" }).click();
|
||||
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.locator("h2", { hasText: "New Skill" })).toBeVisible();
|
||||
// Check form fields
|
||||
await expect(dialog.getByPlaceholder("my-skill-name")).toBeVisible();
|
||||
await expect(
|
||||
dialog.getByPlaceholder("What it does and when to use it..."),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
dialog.getByPlaceholder("Markdown instructions the agent will follow..."),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("create skill dialog has disabled Create Skill button when empty", async ({
|
||||
test("shows the empty state when no skills are available", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
await page.getByRole("button", { name: "New Skill" }).first().click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(
|
||||
dialog.getByRole("button", { name: "Create Skill" }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test("create skill dialog enables Create Skill when name and description filled", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
await page.getByRole("button", { name: "New Skill" }).first().click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await dialog.getByPlaceholder("my-skill-name").fill("my-new-skill");
|
||||
await dialog
|
||||
.getByPlaceholder("What it does and when to use it...")
|
||||
.fill("A test skill");
|
||||
await expect(
|
||||
dialog.getByRole("button", { name: "Create Skill" }),
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
test("skill name auto-formats to kebab-case", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
await page.getByRole("button", { name: "New Skill" }).first().click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
const nameInput = dialog.getByPlaceholder("my-skill-name");
|
||||
// Type mixed case with spaces — should auto-format
|
||||
await nameInput.fill("My Skill Name");
|
||||
// The handleNameChange function lowercases and replaces non-alphanumeric with hyphens
|
||||
await expect(nameInput).toHaveValue("my-skill-name");
|
||||
});
|
||||
|
||||
test("shows validation error for trailing hyphen", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
await page.getByRole("button", { name: "New Skill" }).first().click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await dialog.getByPlaceholder("my-skill-name").pressSequentially("test ");
|
||||
await expect(
|
||||
dialog.getByText(
|
||||
"Use 1–64 lowercase letters, numbers, or hyphens. Names cannot start or end with a hyphen.",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("closes skill dialog via Close button", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
await page.getByRole("button", { name: "New Skill" }).first().click();
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expect(page.getByRole("dialog")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("closes skill dialog via Cancel button", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
await page.getByRole("button", { name: "New Skill" }).first().click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Cancel" }).click();
|
||||
await expect(page.getByRole("dialog")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("skill options menu shows correct items", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
await page.getByLabel("Options for code-review").click();
|
||||
const menu = page.getByRole("menu");
|
||||
await expect(menu).toBeVisible();
|
||||
await expect(menu.getByRole("menuitem", { name: "Edit" })).toBeVisible();
|
||||
await expect(
|
||||
menu.getByRole("menuitem", { name: "Duplicate" }),
|
||||
).toBeVisible();
|
||||
await expect(menu.getByRole("menuitem", { name: "Export" })).toBeVisible();
|
||||
await expect(menu.getByRole("menuitem", { name: "Delete" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Edit opens edit dialog with pre-filled editable fields", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
await page.getByLabel("Options for code-review").click();
|
||||
await page.getByRole("menuitem", { name: "Edit" }).click();
|
||||
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.locator("h2", { hasText: "Edit Skill" })).toBeVisible();
|
||||
|
||||
const nameInput = dialog.getByPlaceholder("my-skill-name");
|
||||
const descriptionInput = dialog.getByPlaceholder(
|
||||
"What it does and when to use it...",
|
||||
);
|
||||
const instructionsInput = dialog.getByPlaceholder(
|
||||
"Markdown instructions the agent will follow...",
|
||||
);
|
||||
|
||||
await expect(nameInput).toHaveValue("code-review");
|
||||
await expect(descriptionInput).toHaveValue(
|
||||
"Reviews code for quality and best practices",
|
||||
);
|
||||
await expect(instructionsInput).toHaveValue(
|
||||
"When asked to review code, analyze the diff and provide feedback on code quality, potential bugs, and best practices.",
|
||||
);
|
||||
await expect(
|
||||
dialog.getByText(
|
||||
"Path on disk: /mock/.agents/skills/code-review/SKILL.md",
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await nameInput.fill("renamed-skill");
|
||||
await expect(nameInput).toHaveValue("renamed-skill");
|
||||
await expect(
|
||||
dialog.getByText(
|
||||
"Path on disk: /mock/.agents/skills/renamed-skill/SKILL.md",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Delete triggers confirmation dialog", async ({ tauriMocked: page }) => {
|
||||
await navigateToSkills(page);
|
||||
await page.getByLabel("Options for code-review").click();
|
||||
await page.getByRole("menuitem", { name: "Delete" }).click();
|
||||
await expect(page.getByText("Delete skill?")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/Are you sure you want to delete.*code-review/),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Cancel in delete confirmation closes dialog", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
await page.getByLabel("Options for code-review").click();
|
||||
await page.getByRole("menuitem", { name: "Delete" }).click();
|
||||
await expect(page.getByText("Delete skill?")).toBeVisible();
|
||||
// Find the Cancel button within the delete confirmation container
|
||||
const confirmDialog = page.locator(".max-w-sm", {
|
||||
has: page.getByText("Delete skill?"),
|
||||
});
|
||||
await confirmDialog.getByRole("button", { name: "Cancel" }).click();
|
||||
await expect(page.getByText("Delete skill?")).not.toBeVisible();
|
||||
// Skill should still be listed
|
||||
await expect(page.getByText("code-review")).toBeVisible();
|
||||
});
|
||||
|
||||
test("search filters skills", async ({ tauriMocked: page }) => {
|
||||
await navigateToSkills(page);
|
||||
await page
|
||||
.getByPlaceholder("Search skills by name or description...")
|
||||
.fill("review");
|
||||
await expect(page.getByText("code-review")).toBeVisible();
|
||||
await expect(page.getByText("test-writer")).not.toBeVisible();
|
||||
// Clear search
|
||||
await page
|
||||
.getByPlaceholder("Search skills by name or description...")
|
||||
.clear();
|
||||
await expect(page.getByText("code-review")).toBeVisible();
|
||||
await expect(page.getByText("test-writer")).toBeVisible();
|
||||
});
|
||||
|
||||
test("search with no results shows empty state", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
await navigateToSkills(page);
|
||||
await page
|
||||
.getByPlaceholder("Search skills by name or description...")
|
||||
.fill("nonexistent-xyz");
|
||||
await expect(page.getByText("No matching skills")).toBeVisible();
|
||||
await expect(page.getByText("Try a different search term.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("empty skills state shows create prompt", async ({
|
||||
tauriMocked: page,
|
||||
}) => {
|
||||
// Override with empty skills — must be called BEFORE navigateToSkills
|
||||
await page.addInitScript({
|
||||
content: buildInitScript({ personas: [], skills: [] }),
|
||||
content: buildInitScript({ personas: [], projects: [], skills: [] }),
|
||||
});
|
||||
|
||||
await navigateToSkills(page);
|
||||
|
||||
await expect(page.getByText("No skills yet")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Create a skill or drop a .skill.json file here."),
|
||||
).toBeVisible();
|
||||
// New Skill button in empty state
|
||||
await expect(
|
||||
page.getByRole("button", { name: "New Skill" }).first(),
|
||||
page.getByText("Create a skill or import one to get started."),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "New Skill" })).toHaveCount(
|
||||
2,
|
||||
);
|
||||
await expect(page.getByRole("button", { name: "Import" })).toHaveCount(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user