New awesome agent primitive

This commit is contained in:
Harald Kirschner
2025-12-17 15:27:05 -08:00
parent 8baf6d7223
commit 88956de414
11 changed files with 812 additions and 17 deletions

View File

@@ -39,3 +39,19 @@ The following instructions are only to be applied when performing a code review.
- [ ] The file name is lower case, with words separated by hyphens.
- [ ] Encourage the use of `tools`, but it's not required.
- [ ] Strongly encourage the use of `model` to specify the model that the chat mode is optimised for.
## Agent Skills guide
**Only apply to folders in the `skills/` directory**
- [ ] The skill folder contains a `SKILL.md` file.
- [ ] The SKILL.md has markdown front matter.
- [ ] The SKILL.md has a `name` field.
- [ ] The `name` field value is lowercase with words separated by hyphens.
- [ ] The `name` field matches the folder name.
- [ ] The SKILL.md has a `description` field.
- [ ] The `description` field is not empty, at least 10 characters, and maximum 1024 characters.
- [ ] The `description` field value is wrapped in single quotes.
- [ ] The folder name is lower case, with words separated by hyphens.
- [ ] Any bundled assets (scripts, templates, data files) are referenced in the SKILL.md instructions.
- [ ] Bundled assets are reasonably sized (under 5MB per file).

View File

@@ -7,6 +7,7 @@ The Awesome GitHub Copilot repository is a community-driven collection of custom
- **Agents** - Specialized GitHub Copilot agents that integrate with MCP servers
- **Prompts** - Task-specific prompts for code generation and problem-solving
- **Instructions** - Coding standards and best practices applied to specific file patterns
- **Skills** - Self-contained folders with instructions and bundled resources for specialized tasks
- **Collections** - Curated collections organized around specific themes and workflows
## Repository Structure
@@ -16,6 +17,7 @@ The Awesome GitHub Copilot repository is a community-driven collection of custom
├── agents/ # Custom GitHub Copilot agent definitions (.agent.md files)
├── prompts/ # Task-specific prompts (.prompt.md files)
├── instructions/ # Coding standards and guidelines (.instructions.md files)
├── skills/ # Agent Skills folders (each with SKILL.md and optional bundled assets)
├── collections/ # Curated collections of resources (.md files)
├── docs/ # Documentation for different resource types
├── eng/ # Build and automation scripts
@@ -36,13 +38,19 @@ npm run collection:validate
# Create a new collection
npm run collection:create -- --id <collection-id> --tags <tags>
# Validate agent skills
npm run skill:validate
# Create a new skill
npm run skill:create -- --name <skill-name>
```
## Development Workflow
### Working with Agents, Prompts, and Instructions
### Working with Agents, Prompts, Instructions, and Skills
All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction files (`*.instructions.md`) must include proper markdown front matter:
All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction files (`*.instructions.md`) must include proper markdown front matter. Agent Skills are folders containing a `SKILL.md` file with frontmatter and optional bundled assets:
#### Agent Files (*.agent.md)
- Must have `description` field (wrapped in single quotes)
@@ -62,20 +70,40 @@ All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction fi
- Must have `applyTo` field specifying file patterns (e.g., `'**.js, **.ts'`)
- File names should be lower case with words separated by hyphens
#### Agent Skills (skills/*/SKILL.md)
- Each skill is a folder containing a `SKILL.md` file
- SKILL.md must have `name` field (lowercase with hyphens, matching folder name, max 64 characters)
- SKILL.md must have `description` field (wrapped in single quotes, 10-1024 characters)
- Folder names should be lower case with words separated by hyphens
- Skills can include bundled assets (scripts, templates, data files)
- Bundled assets should be referenced in the SKILL.md instructions
- Asset files should be reasonably sized (under 5MB per file)
- Skills follow the [Agent Skills specification](https://github.com/anthropics/skills/blob/main/spec/skill-client-integration.md)
### Adding New Resources
When adding a new agent, prompt, or instruction file:
When adding a new agent, prompt, instruction, or skill:
**For Agents, Prompts, and Instructions:**
1. Create the file with proper front matter
2. Add the file to the appropriate directory
3. Update the README.md by running: `npm run build`
4. Verify the resource appears in the generated README
**For Skills:**
1. Run `npm run skill:create` to scaffold a new skill folder
2. Edit the generated SKILL.md file with your instructions
3. Add any bundled assets (scripts, templates, data) to the skill folder
4. Run `npm run skill:validate` to validate the skill structure
5. Update the README.md by running: `npm run build`
6. Verify the skill appears in the generated README
### Testing Instructions
```bash
# Run all validation checks
npm run collection:validate
npm run skill:validate
# Build and verify README generation
npm run build
@@ -148,6 +176,15 @@ For agent files (*.agent.md):
- [ ] Includes `model` field (strongly recommended)
- [ ] Considers using `tools` field
For skills (skills/*/):
- [ ] Folder contains a SKILL.md file
- [ ] SKILL.md has markdown front matter
- [ ] Has `name` field matching folder name (lowercase with hyphens, max 64 characters)
- [ ] Has non-empty `description` field wrapped in single quotes (10-1024 characters)
- [ ] Folder name is lower case with hyphens
- [ ] Any bundled assets are referenced in SKILL.md
- [ ] Bundled assets are under 5MB per file
## Contributing
This is a community-driven project. Contributions are welcome! Please see:

25
docs/README.skills.md Normal file
View File

@@ -0,0 +1,25 @@
# 🎯 Agent Skills
Agent Skills are self-contained folders with instructions and bundled resources that enhance AI capabilities for specialized tasks. Based on the [Agent Skills specification](https://github.com/agentskills/agentskills), each skill contains a `SKILL.md` file with detailed instructions that agents load on-demand.
Skills differ from other primitives by supporting bundled assets (scripts, code samples, reference data) that agents can utilize when performing specialized tasks.
### How to Use Agent Skills
**What's Included:**
- Each skill is a folder containing a `SKILL.md` instruction file
- Skills may include helper scripts, code templates, or reference data
- Skills follow the Agent Skills specification for maximum compatibility
**When to Use:**
- Skills are ideal for complex, repeatable workflows that benefit from bundled resources
- Use skills when you need code templates, helper utilities, or reference data alongside instructions
- Skills provide progressive disclosure - loaded only when needed for specific tasks
**Usage:**
- Browse the skills table below to find relevant capabilities
- Copy the skill folder to your local skills directory
- Reference skills in your prompts or let the agent discover them automatically
| Name | Description | Bundled Assets |
| ---- | ----------- | -------------- |
| [webapp-testing](../skills/webapp-testing/SKILL.md) | Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs. | `test-helper.js` |

View File

@@ -77,6 +77,29 @@ Custom agents for GitHub Copilot, making it easy for users and organizations to
- Access installed agents through the VS Code Chat interface, assign them in CCA, or through Copilot CLI (coming soon)
- Agents will have access to tools from configured MCP servers
- Follow agent-specific instructions for optimal usage`,
skillsSection: `## 🎯 Agent Skills
Agent Skills are self-contained folders with instructions and bundled resources that enhance AI capabilities for specialized tasks. Based on the [Agent Skills specification](https://github.com/anthropics/skills), each skill contains a \`SKILL.md\` file with detailed instructions that agents load on-demand.
Skills differ from other primitives by supporting bundled assets (scripts, code samples, reference data) that agents can utilize when performing specialized tasks.`,
skillsUsage: `### How to Use Agent Skills
**What's Included:**
- Each skill is a folder containing a \`SKILL.md\` instruction file
- Skills may include helper scripts, code templates, or reference data
- Skills follow the Agent Skills specification for maximum compatibility
**When to Use:**
- Skills are ideal for complex, repeatable workflows that benefit from bundled resources
- Use skills when you need code templates, helper utilities, or reference data alongside instructions
- Skills provide progressive disclosure - loaded only when needed for specific tasks
**Usage:**
- Browse the skills table below to find relevant capabilities
- Copy the skill folder to your local skills directory
- Reference skills in your prompts or let the agent discover them automatically`,
};
const vscodeInstallImage =
@@ -98,9 +121,16 @@ const ROOT_FOLDER = path.join(__dirname, "..");
const INSTRUCTIONS_DIR = path.join(ROOT_FOLDER, "instructions");
const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts");
const AGENTS_DIR = path.join(ROOT_FOLDER, "agents");
const SKILLS_DIR = path.join(ROOT_FOLDER, "skills");
const COLLECTIONS_DIR = path.join(ROOT_FOLDER, "collections");
const MAX_COLLECTION_ITEMS = 50;
// Agent Skills validation constants
const SKILL_NAME_MIN_LENGTH = 1;
const SKILL_NAME_MAX_LENGTH = 64;
const SKILL_DESCRIPTION_MIN_LENGTH = 10;
const SKILL_DESCRIPTION_MAX_LENGTH = 1024;
const DOCS_DIR = path.join(ROOT_FOLDER, "docs");
export {
@@ -113,8 +143,13 @@ export {
INSTRUCTIONS_DIR,
PROMPTS_DIR,
AGENTS_DIR,
SKILLS_DIR,
COLLECTIONS_DIR,
MAX_COLLECTION_ITEMS,
SKILL_NAME_MIN_LENGTH,
SKILL_NAME_MAX_LENGTH,
SKILL_DESCRIPTION_MIN_LENGTH,
SKILL_DESCRIPTION_MAX_LENGTH,
DOCS_DIR,
};

219
eng/create-skill.mjs Normal file
View File

@@ -0,0 +1,219 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import readline from "readline";
import { SKILLS_DIR } from "./constants.mjs";
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function prompt(question) {
return new Promise((resolve) => {
rl.question(question, resolve);
});
}
function parseArgs() {
const args = process.argv.slice(2);
const out = { name: undefined, description: undefined };
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === "--name" || a === "-n") {
out.name = args[i + 1];
i++;
} else if (a.startsWith("--name=")) {
out.name = a.split("=")[1];
} else if (a === "--description" || a === "-d") {
out.description = args[i + 1];
i++;
} else if (a.startsWith("--description=")) {
out.description = a.split("=")[1];
} else if (!a.startsWith("-") && !out.name) {
out.name = a;
}
}
return out;
}
async function createSkillTemplate() {
try {
console.log("🎯 Agent Skills Creator");
console.log(
"This tool will help you create a new skill following the Agent Skills specification.\n"
);
const parsed = parseArgs();
// Get skill name
let skillName = parsed.name;
if (!skillName) {
skillName = await prompt("Skill name (lowercase, hyphens only): ");
}
// Validate skill name format
if (!skillName) {
console.error("❌ Skill name is required");
process.exit(1);
}
if (!/^[a-z0-9-]+$/.test(skillName)) {
console.error(
"❌ Skill name must contain only lowercase letters, numbers, and hyphens"
);
process.exit(1);
}
const skillFolder = path.join(SKILLS_DIR, skillName);
// Check if folder already exists
if (fs.existsSync(skillFolder)) {
console.log(`⚠️ Skill folder ${skillName} already exists at ${skillFolder}`);
console.log("💡 Please choose a different name or edit the existing skill.");
process.exit(1);
}
// Get description
let description = parsed.description;
if (!description) {
description = await prompt(
"Description (what this skill does and when to use it): "
);
}
if (!description || description.trim().length < 10) {
console.error(
"❌ Description is required and must be at least 10 characters (max 1024)"
);
process.exit(1);
}
// Get skill title (display name)
const defaultTitle = skillName
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
let skillTitle = await prompt(`Skill title (default: ${defaultTitle}): `);
if (!skillTitle.trim()) {
skillTitle = defaultTitle;
}
// Create skill folder
fs.mkdirSync(skillFolder, { recursive: true });
// Create SKILL.md template
const skillMdContent = `---
name: ${skillName}
description: ${description}
---
# ${skillTitle}
This skill provides [brief overview of what this skill does].
## When to Use This Skill
Use this skill when you need to:
- [Primary use case]
- [Secondary use case]
- [Additional use case]
## Prerequisites
- [Required tool/environment]
- [Optional dependency]
## Core Capabilities
### 1. [Capability Name]
[Description of what this capability does]
### 2. [Capability Name]
[Description of what this capability does]
## Usage Examples
### Example 1: [Use Case]
\`\`\`[language]
// Example code or instructions
\`\`\`
### Example 2: [Use Case]
\`\`\`[language]
// Example code or instructions
\`\`\`
## Guidelines
1. **[Guideline 1]** - [Explanation]
2. **[Guideline 2]** - [Explanation]
3. **[Guideline 3]** - [Explanation]
## Common Patterns
### Pattern: [Pattern Name]
\`\`\`[language]
// Example pattern
\`\`\`
### Pattern: [Pattern Name]
\`\`\`[language]
// Example pattern
\`\`\`
## Limitations
- [Limitation 1]
- [Limitation 2]
- [Limitation 3]
`;
const skillFilePath = path.join(skillFolder, "SKILL.md");
fs.writeFileSync(skillFilePath, skillMdContent);
console.log(`\n✅ Created skill folder: ${skillFolder}`);
console.log(`✅ Created SKILL.md: ${skillFilePath}`);
// Ask if they want to add bundled assets
const addAssets = await prompt(
"\nWould you like to add bundled assets? (helper scripts, templates, etc.) [y/N]: "
);
if (addAssets.toLowerCase() === "y" || addAssets.toLowerCase() === "yes") {
console.log(
"\n📁 You can now add files to the skill folder manually or using your editor."
);
console.log(
" Common bundled assets: helper scripts, code templates, reference data"
);
console.log(` Skill folder location: ${skillFolder}`);
}
console.log("\n📝 Next steps:");
console.log("1. Edit SKILL.md to complete the skill instructions");
console.log("2. Add any bundled assets (scripts, templates, data) to the skill folder");
console.log("3. Run 'npm run skill:validate' to validate the skill");
console.log("4. Run 'npm run build' to generate documentation");
console.log("\n📖 Resources:");
console.log(
" - Anthropic Skills Spec: https://github.com/anthropics/skills/blob/main/spec/skill-client-integration.md"
);
console.log(
" - Project Documentation: AGENTS.md (section on Agent Skills)"
);
} catch (error) {
console.error(`❌ Error creating skill template: ${error.message}`);
process.exit(1);
} finally {
rl.close();
}
}
// Run the interactive creation process
createSkillTemplate();

View File

@@ -9,6 +9,7 @@ import {
extractMcpServers,
extractMcpServerConfigs,
parseFrontmatter,
parseSkillMetadata,
} from "./yaml-parser.mjs";
import {
TEMPLATES,
@@ -19,6 +20,7 @@ import {
ROOT_FOLDER,
PROMPTS_DIR,
AGENTS_DIR,
SKILLS_DIR,
COLLECTIONS_DIR,
INSTRUCTIONS_DIR,
DOCS_DIR,
@@ -477,6 +479,64 @@ function generateAgentsSection(agentsDir, registryNames = []) {
});
}
/**
* Generate the skills section with a table of all skills
*/
function generateSkillsSection(skillsDir) {
if (!fs.existsSync(skillsDir)) {
console.log(`Skills directory does not exist: ${skillsDir}`);
return "";
}
// Get all skill folders (directories)
const skillFolders = fs
.readdirSync(skillsDir)
.filter((file) => {
const filePath = path.join(skillsDir, file);
return fs.statSync(filePath).isDirectory();
});
// Parse each skill folder
const skillEntries = skillFolders
.map((folder) => {
const skillPath = path.join(skillsDir, folder);
const metadata = parseSkillMetadata(skillPath);
if (!metadata) return null;
return {
folder,
name: metadata.name,
description: metadata.description,
assets: metadata.assets,
};
})
.filter((entry) => entry !== null)
.sort((a, b) => a.name.localeCompare(b.name));
console.log(`Found ${skillEntries.length} skill(s)`);
if (skillEntries.length === 0) {
return "";
}
// Create table header
let content =
"| Name | Description | Bundled Assets |\n| ---- | ----------- | -------------- |\n";
// Generate table rows for each skill
for (const skill of skillEntries) {
const link = `../skills/${skill.folder}/SKILL.md`;
const assetsList =
skill.assets.length > 0
? skill.assets.map((a) => `\`${a}\``).join("<br />")
: "None";
content += `| [${skill.name}](${link}) | ${skill.description} | ${assetsList} |\n`;
}
return `${TEMPLATES.skillsSection}\n${TEMPLATES.skillsUsage}\n\n${content}`;
}
/**
* Unified generator for chat modes & agents (future consolidation)
* @param {Object} cfg
@@ -886,6 +946,7 @@ async function main() {
);
const promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# ");
const agentsHeader = TEMPLATES.agentsSection.replace(/^##\s/m, "# ");
const skillsHeader = TEMPLATES.skillsSection.replace(/^##\s/m, "# ");
const collectionsHeader = TEMPLATES.collectionsSection.replace(
/^##\s/m,
"# "
@@ -914,6 +975,15 @@ async function main() {
registryNames
);
// Generate skills README
const skillsReadme = buildCategoryReadme(
generateSkillsSection,
SKILLS_DIR,
skillsHeader,
TEMPLATES.skillsUsage,
registryNames
);
// Generate collections README
const collectionsReadme = buildCategoryReadme(
generateCollectionsSection,
@@ -935,6 +1005,7 @@ async function main() {
);
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme);
writeFileIfChanged(
path.join(DOCS_DIR, "README.collections.md"),
collectionsReadme

172
eng/validate-skills.mjs Normal file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import { parseSkillMetadata } from "./yaml-parser.mjs";
import {
ROOT_FOLDER,
SKILLS_DIR,
SKILL_NAME_MIN_LENGTH,
SKILL_NAME_MAX_LENGTH,
SKILL_DESCRIPTION_MIN_LENGTH,
SKILL_DESCRIPTION_MAX_LENGTH,
} from "./constants.mjs";
// Validation functions
function validateSkillName(name) {
if (!name || typeof name !== "string") {
return "name is required and must be a string";
}
if (!/^[a-z0-9-]+$/.test(name)) {
return "name must contain only lowercase letters, numbers, and hyphens";
}
if (name.length < SKILL_NAME_MIN_LENGTH || name.length > SKILL_NAME_MAX_LENGTH) {
return `name must be between ${SKILL_NAME_MIN_LENGTH} and ${SKILL_NAME_MAX_LENGTH} characters`;
}
return null;
}
function validateSkillDescription(description) {
if (!description || typeof description !== "string") {
return "description is required and must be a string";
}
if (description.length < SKILL_DESCRIPTION_MIN_LENGTH) {
return `description must be at least ${SKILL_DESCRIPTION_MIN_LENGTH} characters`;
}
if (description.length > SKILL_DESCRIPTION_MAX_LENGTH) {
return `description must not exceed ${SKILL_DESCRIPTION_MAX_LENGTH} characters`;
}
return null;
}
function validateSkillFolder(folderPath, folderName) {
const errors = [];
// Check if SKILL.md exists
const skillFile = path.join(folderPath, "SKILL.md");
if (!fs.existsSync(skillFile)) {
errors.push("Missing SKILL.md file");
return errors; // Cannot proceed without SKILL.md
}
// Parse and validate frontmatter
const metadata = parseSkillMetadata(folderPath);
if (!metadata) {
errors.push("Failed to parse SKILL.md frontmatter");
return errors;
}
// Validate name field
const nameError = validateSkillName(metadata.name);
if (nameError) {
errors.push(`name: ${nameError}`);
} else {
// Validate that folder name matches skill name
if (metadata.name !== folderName) {
errors.push(
`Folder name "${folderName}" does not match skill name "${metadata.name}"`
);
}
}
// Validate description field
const descError = validateSkillDescription(metadata.description);
if (descError) {
errors.push(`description: ${descError}`);
}
// Check for reasonable file sizes in bundled assets
const MAX_ASSET_SIZE = 5 * 1024 * 1024; // 5 MB
for (const asset of metadata.assets) {
const assetPath = path.join(folderPath, asset);
try {
const stats = fs.statSync(assetPath);
if (stats.size > MAX_ASSET_SIZE) {
errors.push(
`Bundled asset "${asset}" exceeds maximum size of 5MB (${(
stats.size /
1024 /
1024
).toFixed(2)}MB)`
);
}
} catch (error) {
errors.push(`Cannot access bundled asset "${asset}": ${error.message}`);
}
}
return errors;
}
// Main validation function
function validateSkills() {
if (!fs.existsSync(SKILLS_DIR)) {
console.log("No skills directory found - validation skipped");
return true;
}
const skillFolders = fs
.readdirSync(SKILLS_DIR)
.filter((file) => {
const filePath = path.join(SKILLS_DIR, file);
return fs.statSync(filePath).isDirectory();
});
if (skillFolders.length === 0) {
console.log("No skill folders found - validation skipped");
return true;
}
console.log(`Validating ${skillFolders.length} skill folder(s)...`);
let hasErrors = false;
const usedNames = new Set();
for (const folder of skillFolders) {
const folderPath = path.join(SKILLS_DIR, folder);
console.log(`\nValidating ${folder}...`);
const errors = validateSkillFolder(folderPath, folder);
if (errors.length > 0) {
console.error(`❌ Validation errors in ${folder}:`);
errors.forEach((error) => console.error(` - ${error}`));
hasErrors = true;
} else {
console.log(`${folder} is valid`);
// Check for duplicate names (only if no errors)
const metadata = parseSkillMetadata(folderPath);
if (metadata) {
if (usedNames.has(metadata.name)) {
console.error(
`❌ Duplicate skill name "${metadata.name}" found in ${folder}`
);
hasErrors = true;
} else {
usedNames.add(metadata.name);
}
}
}
}
if (!hasErrors) {
console.log(`\n✅ All ${skillFolders.length} skills are valid`);
}
return !hasErrors;
}
// Run validation
try {
const isValid = validateSkills();
if (!isValid) {
console.error("\n❌ Skill validation failed");
process.exit(1);
}
console.log("\n🎉 Skill validation passed");
} catch (error) {
console.error(`Error during validation: ${error.message}`);
console.error(error.stack);
process.exit(1);
}

View File

@@ -1,5 +1,6 @@
// YAML parser for collection files and frontmatter parsing using vfile-matter
import fs from "fs";
import path from "path";
import yaml from "js-yaml";
import { VFile } from "vfile";
import { matter } from "vfile-matter";
@@ -137,11 +138,56 @@ function extractMcpServerConfigs(filePath) {
});
}
/**
* Parse SKILL.md frontmatter and list bundled assets in a skill folder
* @param {string} skillPath - Path to skill folder
* @returns {object|null} Skill metadata with name, description, and assets array
*/
function parseSkillMetadata(skillPath) {
return safeFileOperation(
() => {
const skillFile = path.join(skillPath, "SKILL.md");
if (!fs.existsSync(skillFile)) {
return null;
}
const frontmatter = parseFrontmatter(skillFile);
// Validate required fields
if (!frontmatter?.name || !frontmatter?.description) {
console.warn(
`Invalid skill at ${skillPath}: missing name or description in frontmatter`
);
return null;
}
// List bundled assets (all files except SKILL.md)
const assets = fs
.readdirSync(skillPath)
.filter((file) => {
const filePath = path.join(skillPath, file);
return file !== "SKILL.md" && fs.statSync(filePath).isFile();
})
.sort();
return {
name: frontmatter.name,
description: frontmatter.description,
assets,
path: skillPath,
};
},
skillPath,
null
);
}
export {
parseCollectionYaml,
parseFrontmatter,
extractAgentMetadata,
extractMcpServers,
extractMcpServerConfigs,
parseSkillMetadata,
safeFileOperation,
};

View File

@@ -11,7 +11,9 @@
"contributors:generate": "all-contributors generate",
"contributors:check": "all-contributors check",
"collection:validate": "node ./eng/validate-collections.mjs",
"collection:create": "node ./eng/create-collection.mjs"
"collection:create": "node ./eng/create-collection.mjs",
"skill:validate": "node ./eng/validate-skills.mjs",
"skill:create": "node ./eng/create-skill.mjs"
},
"repository": {
"type": "git",

View File

@@ -0,0 +1,116 @@
---
name: webapp-testing
description: Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
---
# Web Application Testing
This skill enables comprehensive testing and debugging of local web applications using Playwright automation.
## When to Use This Skill
Use this skill when you need to:
- Test frontend functionality in a real browser
- Verify UI behavior and interactions
- Debug web application issues
- Capture screenshots for documentation or debugging
- Inspect browser console logs
- Validate form submissions and user flows
- Check responsive design across viewports
## Prerequisites
- Node.js installed on the system
- A locally running web application (or accessible URL)
- Playwright will be installed automatically if not present
## Core Capabilities
### 1. Browser Automation
- Navigate to URLs
- Click buttons and links
- Fill form fields
- Select dropdowns
- Handle dialogs and alerts
### 2. Verification
- Assert element presence
- Verify text content
- Check element visibility
- Validate URLs
- Test responsive behavior
### 3. Debugging
- Capture screenshots
- View console logs
- Inspect network requests
- Debug failed tests
## Usage Examples
### Example 1: Basic Navigation Test
```javascript
// Navigate to a page and verify title
await page.goto('http://localhost:3000');
const title = await page.title();
console.log('Page title:', title);
```
### Example 2: Form Interaction
```javascript
// Fill out and submit a form
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
```
### Example 3: Screenshot Capture
```javascript
// Capture a screenshot for debugging
await page.screenshot({ path: 'debug.png', fullPage: true });
```
## Guidelines
1. **Always verify the app is running** - Check that the local server is accessible before running tests
2. **Use explicit waits** - Wait for elements or navigation to complete before interacting
3. **Capture screenshots on failure** - Take screenshots to help debug issues
4. **Clean up resources** - Always close the browser when done
5. **Handle timeouts gracefully** - Set reasonable timeouts for slow operations
6. **Test incrementally** - Start with simple interactions before complex flows
7. **Use selectors wisely** - Prefer data-testid or role-based selectors over CSS classes
## Common Patterns
### Pattern: Wait for Element
```javascript
await page.waitForSelector('#element-id', { state: 'visible' });
```
### Pattern: Check if Element Exists
```javascript
const exists = await page.locator('#element-id').count() > 0;
```
### Pattern: Get Console Logs
```javascript
page.on('console', msg => console.log('Browser log:', msg.text()));
```
### Pattern: Handle Errors
```javascript
try {
await page.click('#button');
} catch (error) {
await page.screenshot({ path: 'error.png' });
throw error;
}
```
## Limitations
- Requires Node.js environment
- Cannot test native mobile apps (use React Native Testing Library instead)
- May have issues with complex authentication flows
- Some modern frameworks may require specific configuration

View File

@@ -0,0 +1,56 @@
/**
* Helper utilities for web application testing with Playwright
*/
/**
* Wait for a condition to be true with timeout
* @param {Function} condition - Function that returns boolean
* @param {number} timeout - Timeout in milliseconds
* @param {number} interval - Check interval in milliseconds
*/
async function waitForCondition(condition, timeout = 5000, interval = 100) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (await condition()) {
return true;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
throw new Error('Condition not met within timeout');
}
/**
* Capture browser console logs
* @param {Page} page - Playwright page object
* @returns {Array} Array of console messages
*/
function captureConsoleLogs(page) {
const logs = [];
page.on('console', msg => {
logs.push({
type: msg.type(),
text: msg.text(),
timestamp: new Date().toISOString()
});
});
return logs;
}
/**
* Take screenshot with automatic naming
* @param {Page} page - Playwright page object
* @param {string} name - Base name for screenshot
*/
async function captureScreenshot(page, name) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${name}-${timestamp}.png`;
await page.screenshot({ path: filename, fullPage: true });
console.log(`Screenshot saved: ${filename}`);
return filename;
}
module.exports = {
waitForCondition,
captureConsoleLogs,
captureScreenshot
};