Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/update-skills-catalog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Update Skills Catalog

on:
workflow_dispatch:
schedule:
# Weekly, Monday 06:00 UTC.
- cron: "0 6 * * 1"

defaults:
run:
shell: bash
working-directory: web

permissions:
contents: write

jobs:
update-catalog:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: "24.3"
cache: npm
cache-dependency-path: 'web/package-lock.json'

- name: Install dependencies
run: npm ci

- name: Fetch skills catalog
# GITHUB_TOKEN raises the api.github.com rate limit for the per-repo
# star-count lookups.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build:skills-catalog

- name: Commit and push catalog
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add src/data/skills.json
git diff --cached --quiet && echo "No changes, skipping push." && exit 0
git commit -m "chore: refresh skills catalog"
git push origin HEAD:main
10 changes: 10 additions & 0 deletions web/docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const config: Config = {
require.resolve('./plugins/rawMarkdownPlugin'),
require.resolve('./plugins/versionPlugin'),
require.resolve('./plugins/jsonLdPlugin'),
require.resolve('./plugins/skillsPlugin'),
[
'@docusaurus/plugin-content-docs',
{
Expand Down Expand Up @@ -182,6 +183,11 @@ const config: Config = {
label: 'Tools',
position: 'left',
},
{
to: '/skills',
label: 'Skills',
position: 'left',
},
{
to: '/pricing',
label: 'Pricing',
Expand Down Expand Up @@ -242,6 +248,10 @@ const config: Config = {
{
title: 'More',
items: [
{
label: 'Skills',
to: '/skills',
},
{
label: 'Pricing',
to: '/pricing',
Expand Down
190 changes: 190 additions & 0 deletions web/e2e/skills.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { test, expect } from '@playwright/test';

// ── Skills Library (/skills) ────────────────────────────────────────────────

test.describe('Skills Library (/skills)', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.getByRole('navigation').getByRole('link', { name: 'Skills' }).click();
});

test('renders page heading', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Skills Library', level: 1 })).toBeVisible();
});

test('lists skill cards from curated sources', async ({ page }) => {
await expect(page.getByRole('link', { name: /pdf/ }).first()).toBeVisible();
});

test('searching narrows the results', async ({ page }) => {
const search = page.getByRole('searchbox', { name: 'Search skills' });
await search.fill('tdd');
await expect(page.getByText('of 161 skills')).toBeVisible();
await expect(page.getByRole('link', { name: /^tdd/ })).toBeVisible();
});

test('an unmatched search shows the empty state', async ({ page }) => {
await page.getByRole('searchbox', { name: 'Search skills' }).fill('zzzz-not-a-real-skill');
await expect(page.getByText('No skills match your search.')).toBeVisible();
});

test('the provider dropdown multi-selects and filters', async ({ page }) => {
await page.getByRole('button', { name: /All providers/ }).click();
await page.getByRole('checkbox', { name: /Matt Pocock/ }).check();
// Close the dropdown so it doesn't overlay the results.
await page.getByRole('heading', { name: 'Skills Library', level: 1 }).click();
await expect(page.getByText('37 of 161 skills')).toBeVisible();
await expect(page.getByRole('link', { name: /^tdd/ })).toBeVisible();
await expect(page.getByRole('link', { name: /^pdf/ })).toHaveCount(0);
});

test('skill cards navigate to the skill detail page', async ({ page }) => {
const card = page.getByRole('link', { name: /^pdf/ }).first();
await expect(card).toHaveAttribute('href', '/skills/anthropic/pdf');
await expect(card).not.toHaveAttribute('target', '_blank');
});

test('proprietary skills show a notice on their card instead of a description', async ({ page }) => {
const card = page.getByRole('link', { name: /^pdf/ }).first();
await expect(card).toContainText('Proprietary license');
await expect(card).toContainText('Proprietary');
});
});

// ── Ranked source-repos aside ───────────────────────────────────────────────

test.describe('Source repos aside', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.getByRole('navigation').getByRole('link', { name: 'Skills' }).click();
});

test('lists source repos ranked by stars', async ({ page }) => {
const aside = page.getByRole('complementary', { name: /Source repositories by stars/ });
await expect(aside.getByText('Source repos')).toBeVisible();
await expect(aside.getByRole('button', { name: /Superpowers/ })).toBeVisible();
await expect(aside.getByText('258.8k')).toBeVisible();
});

test('clicking a repo in the aside filters the catalog by that provider', async ({ page }) => {
const aside = page.getByRole('complementary', { name: /Source repositories by stars/ });
await aside.getByRole('button', { name: /Microsoft/ }).click();
await expect(page.getByText('32 of 161 skills')).toBeVisible();
});
});

// ── Skill detail page (file browser) ────────────────────────────────────────

test.describe('Skill detail page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.getByRole('navigation').getByRole('link', { name: 'Skills' }).click();
await page.getByRole('link', { name: /^mcp-builder/ }).first().click();
});

test('renders skill heading and repo GitHub link', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'mcp-builder', level: 1 })).toBeVisible();
await expect(page.getByTestId('repo-github-link')).toHaveAttribute(
'href',
'https://github.com/anthropics/skills/tree/main/skills/mcp-builder',
);
});

test('file tree lists the skill files', async ({ page }) => {
await expect(page.getByRole('button', { name: 'SKILL.md' })).toBeVisible();
await expect(page.getByRole('button', { name: 'mcp_best_practices.md' })).toBeVisible();
});

test('SKILL.md is selected by default and its content loads', async ({ page }) => {
// Content is fetched live from GitHub's raw CDN, so give the network
// request more headroom than the default UI-interaction assertions.
test.setTimeout(30_000);
await expect(page.getByTestId('viewer-path')).toHaveText('SKILL.md');
await expect(page.getByTestId('viewer-body')).toContainText('MCP', { timeout: 20_000 });
});

test('clicking another file swaps the preview and its GitHub link', async ({ page }) => {
await page.getByRole('button', { name: 'mcp_best_practices.md' }).click();
await expect(page.getByTestId('viewer-path')).toHaveText('reference/mcp_best_practices.md');
await expect(page.getByTestId('viewer-github-link')).toHaveAttribute(
'href',
'https://github.com/anthropics/skills/blob/main/skills/mcp-builder/reference/mcp_best_practices.md',
);
});

test('breadcrumb link returns to the Skills Library', async ({ page }) => {
await page.getByRole('link', { name: 'Skills Library' }).click();
await expect(page.getByRole('heading', { name: 'Skills Library', level: 1 })).toBeVisible();
});

test('shows a detected license label', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'mcp-builder', level: 1 })).toBeVisible();
await expect(page.getByTestId('license-badge')).toHaveText('Apache-2.0');
});

test('markdown files default to a Preview tab, with Code as a second tab', async ({ page }) => {
test.setTimeout(30_000);
const tabs = page.getByRole('tablist');
await expect(tabs.getByRole('tab', { name: 'Preview' })).toHaveAttribute('aria-selected', 'true');
await expect(tabs.getByRole('tab', { name: 'Code' })).toHaveAttribute('aria-selected', 'false');
const viewerBody = page.getByTestId('viewer-body');
// The rendered SKILL.md has several h1s; any one being visible proves the
// markdown was rendered rather than shown as raw source.
await expect(viewerBody.getByRole('heading', { level: 1 }).first()).toBeVisible({ timeout: 20_000 });
});

test('switching to the Code tab shows the raw markdown source', async ({ page }) => {
test.setTimeout(30_000);
const viewerBody = page.getByTestId('viewer-body');
await expect(viewerBody.getByRole('heading', { level: 1 }).first()).toBeVisible({ timeout: 20_000 });
await page.getByRole('tab', { name: 'Code' }).click();
await expect(page.getByRole('tab', { name: 'Code' })).toHaveAttribute('aria-selected', 'true');
await expect(viewerBody.getByRole('heading', { level: 1 })).toHaveCount(0);
await expect(viewerBody).toContainText('#');
});
});

// ── Proprietary skill detail page ───────────────────────────────────────────

test.describe('Proprietary skill detail page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.getByRole('navigation').getByRole('link', { name: 'Skills' }).click();
await page.getByRole('link', { name: /^pdf/ }).first().click();
});

test('shows a proprietary callout, no file browser, but keeps the GitHub link', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'pdf', level: 1 })).toBeVisible();
await expect(page.getByTestId('proprietary-callout')).toBeVisible();
await expect(page.getByTestId('skill-file-browser')).toHaveCount(0);
await expect(page.getByTestId('repo-github-link')).toHaveAttribute(
'href',
'https://github.com/anthropics/skills/tree/main/skills/pdf',
);
});
});

// ── Skill detail page — mobile ──────────────────────────────────────────────

test.describe('Skill detail page on mobile', () => {
test.use({ viewport: { width: 375, height: 667 } });

test.beforeEach(async ({ page }) => {
await page.goto('/');
// The navbar collapses to a hamburger at this width.
await page.getByLabel('Toggle navigation bar').click();
await page.getByRole('link', { name: 'Skills' }).filter({ visible: true }).first().click();
await page.getByRole('link', { name: /^mcp-builder/ }).first().click();
});

test('shows a file dropdown instead of the folder tree', async ({ page }) => {
await expect(page.getByLabel('Choose a file')).toBeVisible();
await expect(page.getByRole('button', { name: 'SKILL.md' })).not.toBeVisible();
});

test('picking a file from the dropdown updates the preview', async ({ page }) => {
await expect(page.getByTestId('viewer-path')).toHaveText('SKILL.md');
await page.getByLabel('Choose a file').selectOption('reference/mcp_best_practices.md');
await expect(page.getByTestId('viewer-path')).toHaveText('reference/mcp_best_practices.md');
});
});
76 changes: 75 additions & 1 deletion web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading