Initial commit
This commit is contained in:
13
.claude-plugin/plugin.json
Normal file
13
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "testing-tools",
|
||||||
|
"description": "Automated testing frameworks and quality assurance tooling",
|
||||||
|
"version": "0.0.0-2025.11.28",
|
||||||
|
"author": {
|
||||||
|
"name": "Connor",
|
||||||
|
"email": "noreply@claudex.dev"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills/playwright-e2e-automation",
|
||||||
|
"./skills/tdd-automation"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# testing-tools
|
||||||
|
|
||||||
|
Automated testing frameworks and quality assurance tooling
|
||||||
252
plugin.lock.json
Normal file
252
plugin.lock.json
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:cskiro/claudex:testing-tools",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "59d8d5a9413c2b77bbff9e4b086c829e43d7bc58",
|
||||||
|
"treeHash": "2dc0bfaee4f19cf1c633db815c08c030f987af6ed843b3fcbeac2f41ab17f9bb",
|
||||||
|
"generatedAt": "2025-11-28T10:15:53.396710Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "testing-tools",
|
||||||
|
"description": "Automated testing frameworks and quality assurance tooling"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "f57fbeb0403191907d95002e4d253b110e9b19a0719ab9ee636cca7c7df18f89"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "6565f6863caa8227078ea99c283e13cad386601dc40d25b128d34b8ccb147090"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/CHANGELOG.md",
|
||||||
|
"sha256": "d9f9efe4427863d22a2d03411ff895c0fac387d4fd45c56f726f23bf8a3b07b6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/index.js",
|
||||||
|
"sha256": "90c4a1bbf4160ba42bb48817db81d772bd95fad38d00c5ca9ac30e63e5946f68"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/README.md",
|
||||||
|
"sha256": "cb63dad5b043467be51b1378fd656de13e6a79e48e64e8364df1d4d88f2ab84f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/SKILL.md",
|
||||||
|
"sha256": "00939e4fd5d00c2ee7a25e707c70261dd770736c26fa77e122cb7f2de3b4e424"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/utils/merge-claude-md.js",
|
||||||
|
"sha256": "38c6ce20de4b25bc907bbfaf56b880f9b5929f52abca2ebb97fc9970945d8070"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/utils/update-package-json.js",
|
||||||
|
"sha256": "6446f58996930956eefb6ff0c22524e0a3a57d7c514c6327b0cd6843c53b3ea8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/utils/detect-project-type.js",
|
||||||
|
"sha256": "24ef9590db105fc12142403b991c1115cf1c4e8e38da85d5c1ab961dbaf3b42b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/utils/install-hooks.js",
|
||||||
|
"sha256": "25c2bfa2aa5fe0949a2c6617960913901291c4ae0e207e736a89d8ab796d941d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/utils/validate-claude-md.js",
|
||||||
|
"sha256": "60d4189b8ac6de073cda7d03530f1afb7663551ea901121bfcee999102045aba"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/examples/.gitkeep",
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/scripts/remove-tdd-section.js",
|
||||||
|
"sha256": "e4a36075b01e97e4e792ae59275571112cf29f2222f292dfa60ea21a48bb4fc8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/scripts/validate-tdd.js",
|
||||||
|
"sha256": "1c32502fbbb283d3bd4d22a02a06e0c2ccc3711e0ed3265aa872043cb4bdfe8b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/scripts/generate-test.js",
|
||||||
|
"sha256": "2192171feb30235713e2d47a41b207e5ee3e333d61fda0235dcb1957def0aa0b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/scripts/rollback-tdd.js",
|
||||||
|
"sha256": "ebb1cb0ffc8d7e7b6051b6c7ab2119c0452edf7632f5b6429559991daf12f0d4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/templates/pre-commit.sh",
|
||||||
|
"sha256": "74f425ce3746e6e3689dee79c12a576bf655edcd056849027c5ba82a1ab1ce0a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/templates/tdd-auto-enforcer.sh",
|
||||||
|
"sha256": "23771c535d8f973104a98376d326147e3253bf96d2c3edb089d3c10959982761"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tdd-automation/reference/.gitkeep",
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/CHANGELOG.md",
|
||||||
|
"sha256": "de35c47ba531aeffdc0ec4e31ee6ab24404cdc6cc73095df922a64a2ab1aa6c0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/README.md",
|
||||||
|
"sha256": "5c4ce8a2a3c30a75926dd9df3990ff6a892b35bccd28ea6a884e301589b5e53e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/SKILL.md",
|
||||||
|
"sha256": "95c51cd493687b67af40094614a07c09265e588b3ad334224f366b36b066dc75"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/examples/react-vite/example-test.spec.ts",
|
||||||
|
"sha256": "8c394a3bc1e4822153e9423b3d6b155981c7105a4354cee6368426ac63d8b24e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/examples/react-vite/example-page-object.ts",
|
||||||
|
"sha256": "2fa7ca1a5b2e4030819d54663d4fbe644ac693ec3e8af6fee52961f585c89415"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/examples/reports/fix-recommendations.example.md",
|
||||||
|
"sha256": "58858f2209acd03cf96b01e6a8cee51f4e63a3738380aea0d73743c09feb7221"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/examples/reports/visual-analysis-report.example.md",
|
||||||
|
"sha256": "2d6ae998818c0b7711355753dc4edecb2dda80c5077d9764f6acf3acf0bb787d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/workflow/phase-2-setup.md",
|
||||||
|
"sha256": "e26132ccd86c5f033dbc9babeeccc10362cae5d000dccd36117a590b44a26c2f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/workflow/phase-8-export.md",
|
||||||
|
"sha256": "2c195085edb931dfe77e885b7a7797618b00a288cbd37337da85486a30512e1a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/workflow/phase-6-regression.md",
|
||||||
|
"sha256": "b593116efded3aa9627d67c2d75f5a0f023df15b55214fbb188a67d278d25065"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/workflow/phase-7-fixes.md",
|
||||||
|
"sha256": "70d28e5a594739f8be69f8fc4ec2db6ada3ffebf26a399d1eb2bbea55cd8943d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/workflow/phase-2.5-preflight.md",
|
||||||
|
"sha256": "4393f5fbf4d43c5a7dcce5708a4a294bd2687aecabd8fc7b195d0a95c36f0b79"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/workflow/phase-4-capture.md",
|
||||||
|
"sha256": "b70c817f68be0084e124f5cd25adec226e5bc17e082847c93e6dc3a3ae688497"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/workflow/phase-1-discovery.md",
|
||||||
|
"sha256": "2f118396ca808b57327fee8314a73f415ce821304f2d3bcacdc48f549984d400"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/workflow/phase-3-generation.md",
|
||||||
|
"sha256": "df8bc6957d137d07203d2159279386fadaec29fbc0b04e36b34e31de0e33df26"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/workflow/phase-5-analysis.md",
|
||||||
|
"sha256": "8ac791730d996918cb85b314003daaedee13b923844d54db9d786382f2303eeb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/scripts/README.md",
|
||||||
|
"sha256": "94e9b2dfe339d6a2bd2efd4b752d87f3f1aa5ed8172e274bdf0eaa110c5c7026"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/templates/playwright.config.template.ts",
|
||||||
|
"sha256": "368e5e4449eecbe589f1a39b4c174f62a67e3f194544f2a88befb87fcb38e455"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/templates/test-spec.template.ts",
|
||||||
|
"sha256": "5ef5857248eb2d701d7d9cc504db0edff0285ed16062c2f742a180e030cf9746"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/templates/global-setup.template.ts",
|
||||||
|
"sha256": "1cf33e734671f33b127502bb5dea3bb97098a4e8625713e3e4d7a342a5ece75c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/templates/global-teardown.template.ts",
|
||||||
|
"sha256": "f46fe5a8912bb7f657d0f8e8b2126b2404abdbd881c4ed54b6d2b8c19461d80f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/templates/page-object.template.ts",
|
||||||
|
"sha256": "e0b67e6841818823a335cbff71eec1c8c61e301fa07dd4fa7d162836b19178f4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/templates/screenshot-helper.template.ts",
|
||||||
|
"sha256": "34803983e5a29aa26e34bcf81eeddac634cf415fbec049645a897930c5d921d8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/templates/css/vanilla.css",
|
||||||
|
"sha256": "b2168b3de50640e7f705e19d54f414d2d6d8a85f1bf38017eec5036e24785f78"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/templates/css/tailwind-v3.css",
|
||||||
|
"sha256": "8afe45f5f6ab2a62dd106cc385aae7376c9a875450f3b0599f7a9044f8b81b98"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/templates/css/tailwind-v4.css",
|
||||||
|
"sha256": "f06f1da8a4dcebafb41ab576d4ed9658f96c8a70791ae7e92cbe3f53b1962a93"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/templates/configs/postcss-tailwind-v3.js",
|
||||||
|
"sha256": "dd28449f67a88d120997e27864b6a1ca6277dc23e3f0fbefeee9b3ad2e068b9b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/templates/configs/postcss-tailwind-v4.js",
|
||||||
|
"sha256": "0cb3d2cb69c8f4020e5f06f9763185d4be38b05ddc92c05d10746ea738e644e3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/data/playwright-best-practices.md",
|
||||||
|
"sha256": "ab54c39d74de70e77b06f8a226a19283266a26850972e5eea8a30e1dd699d837"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/data/framework-detection-patterns.yaml",
|
||||||
|
"sha256": "c743ddeb7724ba4e5f938c361831d9859f86ed5d134b055b558204b6c09fd434"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/data/error-patterns.yaml",
|
||||||
|
"sha256": "85805085b6cd51dc3bf1884c52ef9707090530199d851a1190a05d61e6b07e7b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/data/accessibility-checks.md",
|
||||||
|
"sha256": "967bd933ee183026ea27dcd9164d3ea00c21991e4ae8ad560ac6b569cd151c89"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/data/common-ui-bugs.md",
|
||||||
|
"sha256": "ccb66a51d6e1a7c5182e92a0a19634178bcbe2ee733f939f0399e91be48b5334"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/data/framework-versions.yaml",
|
||||||
|
"sha256": "abf39a2a28fbcd43e0d9675ec5d4dbab32328ce1d3ae8aaa2c268a4ea2fe3fa6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/reference/ci-cd-integration.md",
|
||||||
|
"sha256": "7cbe26dada1adb3502c2a43fd3fe7924edf3671c5ef14f39aa17e1b53b602160"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/playwright-e2e-automation/reference/troubleshooting.md",
|
||||||
|
"sha256": "d2e2e7cfdfbf9dd9feec023829aa693b0e4305ee7b03c5f930d2bb1e0340de50"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "2dc0bfaee4f19cf1c633db815c08c030f987af6ed843b3fcbeac2f41ab17f9bb"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
22
skills/playwright-e2e-automation/CHANGELOG.md
Normal file
22
skills/playwright-e2e-automation/CHANGELOG.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
- Refactored to Anthropic progressive disclosure pattern
|
||||||
|
- Updated description with "Use PROACTIVELY when..." format
|
||||||
|
- Removed version/author/category/tags from frontmatter
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
- Added framework version detection (Tailwind v3/v4, React 17-19, Next.js 13-14)
|
||||||
|
- Added pre-flight health check (Phase 2.5)
|
||||||
|
- Added error pattern recovery database
|
||||||
|
- Fixed Tailwind CSS v4 compatibility
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
- Initial release with zero-setup Playwright automation
|
||||||
|
- Multi-framework support: React/Vite, Next.js, Node.js, static
|
||||||
|
- LLM-powered visual analysis for UI bug detection
|
||||||
|
- Visual regression testing with baseline comparison
|
||||||
|
- Fix recommendations with file:line references
|
||||||
387
skills/playwright-e2e-automation/README.md
Normal file
387
skills/playwright-e2e-automation/README.md
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
# Playwright E2E Automation
|
||||||
|
|
||||||
|
> Automated Playwright e2e testing framework with LLM-powered visual debugging, screenshot analysis, and regression testing
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Trigger Phrases
|
||||||
|
|
||||||
|
Simply ask Claude Code:
|
||||||
|
|
||||||
|
```
|
||||||
|
"set up playwright testing for my app"
|
||||||
|
"help me debug UI issues with screenshots"
|
||||||
|
"create e2e tests with visual regression"
|
||||||
|
"analyze my app's UI with screenshots"
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Happens
|
||||||
|
|
||||||
|
This skill will automatically:
|
||||||
|
|
||||||
|
1. **Detect your application** - Identifies React/Vite, Node.js, static sites, or full-stack apps
|
||||||
|
2. **Detect framework versions** (NEW) - Determines Tailwind v3 vs v4, React version, etc.
|
||||||
|
3. **Pre-flight validation** (NEW) - Checks app loads before running tests, catches config errors early
|
||||||
|
4. **Install Playwright** - Runs `npm init playwright@latest` with optimal configuration
|
||||||
|
5. **Generate test suite** - Creates screenshot-enabled tests with version-appropriate templates
|
||||||
|
6. **Capture screenshots** - Takes full-page screenshots at key interaction points
|
||||||
|
7. **Analyze visually** - Uses LLM vision to identify UI bugs, layout issues, accessibility problems
|
||||||
|
8. **Detect regressions** - Compares against baselines to find unexpected visual changes
|
||||||
|
9. **Generate fixes** - Produces actionable code recommendations with file:line references
|
||||||
|
10. **Export test suite** - Provides production-ready tests you can run independently
|
||||||
|
|
||||||
|
**Total time**: ~5-8 minutes (one-time setup)
|
||||||
|
**New in v0.2.0**: Version detection and pre-flight validation prevent configuration errors
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Zero-Setup Automation
|
||||||
|
|
||||||
|
No configuration required. The skill:
|
||||||
|
|
||||||
|
- Detects your framework automatically (React, Vite, Next.js, Express, etc.)
|
||||||
|
- Installs Playwright and browsers without prompts
|
||||||
|
- Generates optimal configuration based on your app type
|
||||||
|
- Creates tests following best practices
|
||||||
|
- Runs everything end-to-end
|
||||||
|
|
||||||
|
### Multi-Framework Support
|
||||||
|
|
||||||
|
Works with:
|
||||||
|
|
||||||
|
- **React/Vite** - Modern React apps with Vite dev server
|
||||||
|
- **Next.js** - Server-side rendered React applications
|
||||||
|
- **Node.js/Express** - Backend services with HTML responses
|
||||||
|
- **Static HTML/CSS/JS** - Traditional web applications
|
||||||
|
- **Full-stack** - Combined frontend + backend applications
|
||||||
|
|
||||||
|
### Version-Aware Configuration (NEW in v0.2.0)
|
||||||
|
|
||||||
|
The skill now detects installed framework versions and adapts automatically:
|
||||||
|
|
||||||
|
**Tailwind CSS**:
|
||||||
|
- **v3.x**: Uses `@tailwind base; @tailwind components; @tailwind utilities;` syntax
|
||||||
|
- **v4.x**: Uses `@import "tailwindcss";` syntax and `@tailwindcss/postcss` plugin
|
||||||
|
|
||||||
|
**React**:
|
||||||
|
- **v17**: Classic JSX transform (requires React import)
|
||||||
|
- **v18+**: Automatic JSX transform (no import needed)
|
||||||
|
|
||||||
|
**Detection Process**:
|
||||||
|
1. Reads `package.json` dependencies
|
||||||
|
2. Matches versions against compatibility database
|
||||||
|
3. Selects appropriate templates (CSS, PostCSS config, etc.)
|
||||||
|
4. Warns about breaking changes or unknown versions
|
||||||
|
|
||||||
|
**Pre-flight Validation**:
|
||||||
|
- Loads app in browser before running tests
|
||||||
|
- Monitors console for critical errors
|
||||||
|
- Matches errors against known patterns (Tailwind v4 syntax, PostCSS plugin, etc.)
|
||||||
|
- Provides specific fix steps with file:line references
|
||||||
|
- **Prevents cascade failures**: One config error won't fail all 10 tests
|
||||||
|
|
||||||
|
**Example Error Detection**:
|
||||||
|
```
|
||||||
|
❌ Pre-flight check failed: Tailwind CSS v4 syntax mismatch
|
||||||
|
|
||||||
|
Root cause: CSS file uses @tailwind directives but v4 requires @import
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
1. Update src/index.css:
|
||||||
|
Change from: @tailwind base; @tailwind components; @tailwind utilities;
|
||||||
|
Change to: @import "tailwindcss";
|
||||||
|
|
||||||
|
2. Update postcss.config.js:
|
||||||
|
Change from: plugins: { tailwindcss: {} }
|
||||||
|
Change to: plugins: { '@tailwindcss/postcss': {} }
|
||||||
|
|
||||||
|
3. Restart dev server: npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### LLM-Powered Visual Analysis
|
||||||
|
|
||||||
|
Automatically identifies:
|
||||||
|
|
||||||
|
- **UI Bugs** - Broken layouts, overlapping elements, cut-off text
|
||||||
|
- **Accessibility Issues** - Color contrast, missing labels, improper heading hierarchy
|
||||||
|
- **Responsive Problems** - Elements not scaling, overflow on mobile
|
||||||
|
- **Visual Regressions** - Unexpected changes from baseline screenshots
|
||||||
|
- **Missing Elements** - Expected UI components not rendered
|
||||||
|
|
||||||
|
### Actionable Fix Recommendations
|
||||||
|
|
||||||
|
Generates specific fixes with:
|
||||||
|
|
||||||
|
- File paths and line numbers (`src/components/Button.tsx:45`)
|
||||||
|
- Current code snippets showing the issue
|
||||||
|
- Recommended code changes
|
||||||
|
- Explanation of why the fix works
|
||||||
|
- Priority level (critical, high, medium, low)
|
||||||
|
|
||||||
|
### Production-Ready Test Suite
|
||||||
|
|
||||||
|
Exports include:
|
||||||
|
|
||||||
|
- Organized test files following best practices
|
||||||
|
- Page object models for maintainability
|
||||||
|
- Screenshot helpers and utilities
|
||||||
|
- npm scripts for test execution
|
||||||
|
- README with usage instructions
|
||||||
|
- CI/CD integration examples
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Example 1: Initial Setup for React App
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Set up Playwright testing for my Vite React app"
|
||||||
|
|
||||||
|
Claude (via skill):
|
||||||
|
✅ Detected React + Vite application
|
||||||
|
✅ Installing Playwright...
|
||||||
|
✅ Generated playwright.config.ts with base URL http://localhost:5173
|
||||||
|
✅ Created 5 test specs covering critical journeys
|
||||||
|
✅ Captured 23 screenshots across desktop, tablet, mobile viewports
|
||||||
|
✅ Visual analysis complete: Found 3 issues (1 high, 2 medium)
|
||||||
|
✅ Generated fix recommendations in fix-recommendations.md
|
||||||
|
✅ Test suite exported to tests/e2e/
|
||||||
|
|
||||||
|
Next steps:
|
||||||
|
- Review fix-recommendations.md
|
||||||
|
- Run tests: npm run test:e2e
|
||||||
|
- Update baselines after fixes: npm run test:e2e:update-snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Visual Regression Detection
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Check for visual regressions after my recent changes"
|
||||||
|
|
||||||
|
Claude (via skill):
|
||||||
|
✅ Running existing Playwright test suite...
|
||||||
|
✅ Captured 23 screenshots
|
||||||
|
✅ Comparing against baselines...
|
||||||
|
|
||||||
|
⚠️ Found 4 visual changes:
|
||||||
|
- Home page hero section: Layout shift detected (12% difference)
|
||||||
|
- Navigation menu: New dropdown added (expected change)
|
||||||
|
- Contact form: Button text color changed (2% difference)
|
||||||
|
- Footer: Logo size increased (5% difference)
|
||||||
|
|
||||||
|
📊 Visual regression report: screenshots/diffs/report.html
|
||||||
|
|
||||||
|
Would you like to:
|
||||||
|
1. Accept these changes as new baseline
|
||||||
|
2. Review specific diffs
|
||||||
|
3. Get fix recommendations for unintended changes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Bug Investigation
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Users report the checkout button is cut off on mobile"
|
||||||
|
|
||||||
|
Claude (via skill):
|
||||||
|
✅ Generating mobile viewport test...
|
||||||
|
✅ Captured screenshot: screenshots/current/checkout-mobile.png
|
||||||
|
✅ Visual analysis complete
|
||||||
|
|
||||||
|
🐛 Issue confirmed: Button text truncated on 375px viewport
|
||||||
|
|
||||||
|
Location: src/components/CheckoutButton.tsx:28
|
||||||
|
|
||||||
|
Current code:
|
||||||
|
```tsx
|
||||||
|
<button className="px-6 py-3 text-xl font-bold">
|
||||||
|
Complete Purchase
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended fix:
|
||||||
|
```tsx
|
||||||
|
<button className="px-4 py-2 text-sm sm:text-base md:text-xl font-bold max-w-full">
|
||||||
|
Complete Purchase
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Reasoning: Fixed padding and font size cause overflow on narrow screens.
|
||||||
|
Added responsive sizing and max-width constraint.
|
||||||
|
|
||||||
|
Test added: tests/e2e/checkout-responsive.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Gets Generated
|
||||||
|
|
||||||
|
After running this skill, you'll have:
|
||||||
|
|
||||||
|
```
|
||||||
|
your-project/
|
||||||
|
├── playwright.config.ts # Playwright configuration
|
||||||
|
├── tests/
|
||||||
|
│ └── e2e/
|
||||||
|
│ ├── setup/
|
||||||
|
│ │ └── global-setup.ts # Dev server startup
|
||||||
|
│ ├── pages/
|
||||||
|
│ │ └── *.page.ts # Page object models
|
||||||
|
│ ├── specs/
|
||||||
|
│ │ └── *.spec.ts # Test specifications
|
||||||
|
│ └── utils/
|
||||||
|
│ └── screenshot-helper.ts
|
||||||
|
├── screenshots/
|
||||||
|
│ ├── baselines/ # Reference images
|
||||||
|
│ ├── current/ # Latest run
|
||||||
|
│ └── diffs/ # Visual comparisons
|
||||||
|
├── fix-recommendations.md # Generated fix suggestions
|
||||||
|
├── visual-analysis-report.md # LLM analysis results
|
||||||
|
└── package.json # Updated with test scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
**Typical execution time** (React app with 5 critical journeys):
|
||||||
|
|
||||||
|
- Application detection: ~5 seconds
|
||||||
|
- Playwright installation: ~2-3 minutes (one-time)
|
||||||
|
- Test generation: ~30 seconds
|
||||||
|
- Test execution: ~30-60 seconds
|
||||||
|
- Visual analysis: ~1-2 minutes
|
||||||
|
- Regression comparison: ~10 seconds
|
||||||
|
- Fix generation: ~30 seconds
|
||||||
|
|
||||||
|
**Total**: ~5-8 minutes (excluding one-time Playwright install)
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
The skill generates GitHub Actions workflow examples. Basic setup:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Playwright E2E Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
- run: npm ci
|
||||||
|
- run: npx playwright install --with-deps
|
||||||
|
- run: npm run test:e2e
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-screenshots
|
||||||
|
path: screenshots/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Baseline Management
|
||||||
|
|
||||||
|
**In CI**:
|
||||||
|
- Store baselines in repository: `git add screenshots/baselines/`
|
||||||
|
- Tests fail if visual diffs exceed threshold
|
||||||
|
- Review artifacts before merging
|
||||||
|
|
||||||
|
**Locally**:
|
||||||
|
- Update baselines: `npm run test:e2e:update-snapshots`
|
||||||
|
- Commit updated baselines after review
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Custom Test Generation
|
||||||
|
|
||||||
|
After initial setup, you can:
|
||||||
|
|
||||||
|
1. **Add more tests** - Follow the pattern in generated specs
|
||||||
|
2. **Customize viewports** - Edit playwright.config.ts
|
||||||
|
3. **Add custom assertions** - Extend screenshot helpers
|
||||||
|
4. **Configure browsers** - Enable Firefox/WebKit in config
|
||||||
|
5. **Adjust thresholds** - Modify visual diff sensitivity
|
||||||
|
|
||||||
|
### Visual Analysis Customization
|
||||||
|
|
||||||
|
The skill's analysis focuses on:
|
||||||
|
|
||||||
|
- WCAG 2.1 AA accessibility compliance (see `data/accessibility-checks.md`)
|
||||||
|
- Common UI bug patterns (see `data/common-ui-bugs.md`)
|
||||||
|
- Framework-specific best practices
|
||||||
|
|
||||||
|
### Integration with Existing Tests
|
||||||
|
|
||||||
|
This skill complements your existing test suite:
|
||||||
|
|
||||||
|
- **Unit tests** (Vitest/Jest) - Test logic and calculations
|
||||||
|
- **Integration tests** - Test component interaction
|
||||||
|
- **E2E tests** (Playwright) - Test full user workflows + visual regression
|
||||||
|
|
||||||
|
All three work together without conflicts.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Application not detected"
|
||||||
|
|
||||||
|
**Solution**: Specify manually
|
||||||
|
```
|
||||||
|
"Set up Playwright for my [framework] app running on port [port]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Dev server not running"
|
||||||
|
|
||||||
|
**Solution**: The skill will attempt to start it automatically. If that fails:
|
||||||
|
```bash
|
||||||
|
npm run dev # Start your dev server first
|
||||||
|
```
|
||||||
|
Then re-run the skill.
|
||||||
|
|
||||||
|
### "Screenshot capture timeout"
|
||||||
|
|
||||||
|
**Solution**: Increase timeout in playwright.config.ts:
|
||||||
|
```typescript
|
||||||
|
timeout: 60000, // 60 seconds instead of default 30
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Visual analysis found too many false positives"
|
||||||
|
|
||||||
|
**Solution**: Adjust the visual diff threshold:
|
||||||
|
```typescript
|
||||||
|
expect(await page.screenshot()).toMatchSnapshot({
|
||||||
|
maxDiffPixelRatio: 0.05, // Allow 5% difference
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **Node.js**: >=16.0.0
|
||||||
|
- **npm**: >=7.0.0
|
||||||
|
- **Disk space**: ~500MB for Playwright browsers (one-time)
|
||||||
|
- **Memory**: ~500MB during test execution
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Baseline management** - Commit baselines to git, update deliberately
|
||||||
|
2. **Screenshot organization** - Use .gitignore for current/diffs, keep baselines
|
||||||
|
3. **Test critical paths** - Focus on user journeys that matter (80/20 rule)
|
||||||
|
4. **Run in CI** - Catch regressions before production
|
||||||
|
5. **Review diffs carefully** - Not all changes are bugs
|
||||||
|
6. **Use semantic selectors** - Prefer getByRole over CSS selectors
|
||||||
|
7. **Capture context** - Take screenshots before AND after interactions
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
- [Playwright Documentation](https://playwright.dev/)
|
||||||
|
- [Visual Regression Testing Guide](https://playwright.dev/docs/test-snapshots)
|
||||||
|
- [Accessibility Testing](https://playwright.dev/docs/accessibility-testing)
|
||||||
|
- [CI/CD Integration](https://playwright.dev/docs/ci)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Issues with this skill? Please report at:
|
||||||
|
- [Claude Code Issues](https://github.com/anthropics/claude-code/issues)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Created with**: skill-creator v0.1.0
|
||||||
|
**Skill Version**: 0.1.0
|
||||||
|
**Last Updated**: 2025-11-01
|
||||||
141
skills/playwright-e2e-automation/SKILL.md
Normal file
141
skills/playwright-e2e-automation/SKILL.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
---
|
||||||
|
name: playwright-e2e-automation
|
||||||
|
description: Use PROACTIVELY when setting up e2e testing, debugging UI issues, or creating regression test suites. Automated Playwright framework with LLM-powered visual analysis, screenshot capture, and fix recommendations with file:line references. Zero-setup for React/Vite, Node.js, static sites, and full-stack applications. Not for unit testing, API-only testing, or mobile native apps.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Playwright E2E Automation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This skill automates the complete Playwright e2e testing lifecycle with LLM-powered visual debugging. It detects your app type, installs Playwright, generates tests, captures screenshots, analyzes for UI bugs, and produces fix recommendations with file paths and line numbers.
|
||||||
|
|
||||||
|
**Key Capabilities**:
|
||||||
|
- Zero-setup automation with multi-framework support
|
||||||
|
- Visual debugging with screenshot capture and LLM analysis
|
||||||
|
- Regression testing with baseline comparison
|
||||||
|
- Actionable fix recommendations with file:line references
|
||||||
|
- CI/CD ready test suite export
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
**Trigger Phrases**:
|
||||||
|
- "set up playwright testing for my app"
|
||||||
|
- "help me debug UI issues with screenshots"
|
||||||
|
- "create e2e tests with visual regression"
|
||||||
|
- "analyze my app's UI with screenshots"
|
||||||
|
- "generate playwright tests for [my app]"
|
||||||
|
|
||||||
|
**Use Cases**:
|
||||||
|
- Setting up Playwright testing from scratch
|
||||||
|
- Debugging visual/UI bugs hard to describe in text
|
||||||
|
- Creating screenshot-based regression testing
|
||||||
|
- Generating e2e test suites for new applications
|
||||||
|
- Identifying accessibility issues through visual inspection
|
||||||
|
|
||||||
|
**NOT for**:
|
||||||
|
- Unit testing or component testing (use Vitest/Jest)
|
||||||
|
- API-only testing without UI
|
||||||
|
- Performance/load testing
|
||||||
|
- Mobile native app testing (use Detox/Appium)
|
||||||
|
|
||||||
|
## Response Style
|
||||||
|
|
||||||
|
- **Automated**: Execute entire workflow with minimal user intervention
|
||||||
|
- **Informative**: Clear progress updates at each phase
|
||||||
|
- **Visual**: Always capture and analyze screenshots
|
||||||
|
- **Actionable**: Generate specific fixes with file paths and line numbers
|
||||||
|
|
||||||
|
## Quick Decision Matrix
|
||||||
|
|
||||||
|
| User Request | Action | Reference |
|
||||||
|
|--------------|--------|-----------|
|
||||||
|
| "set up playwright" | Full setup workflow | `workflow/phase-1-discovery.md` → `phase-2-setup.md` |
|
||||||
|
| "debug UI issues" | Capture + Analyze | `workflow/phase-4-capture.md` → `phase-5-analysis.md` |
|
||||||
|
| "check for regressions" | Compare baselines | `workflow/phase-6-regression.md` |
|
||||||
|
| "generate fix recommendations" | Analyze + Generate | `workflow/phase-7-fixes.md` |
|
||||||
|
| "export test suite" | Package for CI/CD | `workflow/phase-8-export.md` |
|
||||||
|
|
||||||
|
## Workflow Overview
|
||||||
|
|
||||||
|
### Phase 1: Application Discovery
|
||||||
|
Detect app type, framework versions, and optimal configuration.
|
||||||
|
→ **Details**: `workflow/phase-1-discovery.md`
|
||||||
|
|
||||||
|
### Phase 2: Playwright Setup
|
||||||
|
Install Playwright and generate configuration.
|
||||||
|
→ **Details**: `workflow/phase-2-setup.md`
|
||||||
|
|
||||||
|
### Phase 2.5: Pre-flight Health Check
|
||||||
|
Validate app loads correctly before full test suite.
|
||||||
|
→ **Details**: `workflow/phase-2.5-preflight.md`
|
||||||
|
|
||||||
|
### Phase 3: Test Generation
|
||||||
|
Create screenshot-enabled tests for critical workflows.
|
||||||
|
→ **Details**: `workflow/phase-3-generation.md`
|
||||||
|
|
||||||
|
### Phase 4: Screenshot Capture
|
||||||
|
Run tests and capture visual data.
|
||||||
|
→ **Details**: `workflow/phase-4-capture.md`
|
||||||
|
|
||||||
|
### Phase 5: Visual Analysis
|
||||||
|
LLM-powered analysis to identify UI bugs.
|
||||||
|
→ **Details**: `workflow/phase-5-analysis.md`
|
||||||
|
|
||||||
|
### Phase 6: Regression Detection
|
||||||
|
Compare screenshots against baselines.
|
||||||
|
→ **Details**: `workflow/phase-6-regression.md`
|
||||||
|
|
||||||
|
### Phase 7: Fix Generation
|
||||||
|
Map issues to source code with actionable fixes.
|
||||||
|
→ **Details**: `workflow/phase-7-fixes.md`
|
||||||
|
|
||||||
|
### Phase 8: Test Suite Export
|
||||||
|
Package production-ready test suite.
|
||||||
|
→ **Details**: `workflow/phase-8-export.md`
|
||||||
|
|
||||||
|
## Important Reminders
|
||||||
|
|
||||||
|
1. **Capture before AND after interactions** - Provides context for visual debugging
|
||||||
|
2. **Use semantic selectors** - Prefer getByRole, getByLabel over CSS selectors
|
||||||
|
3. **Baseline management is critical** - Keep in sync with intentional UI changes
|
||||||
|
4. **LLM analysis is supplementary** - Use alongside automated assertions
|
||||||
|
5. **Test critical paths first** - Focus on user journeys that matter most (80/20 rule)
|
||||||
|
6. **Screenshots are large** - Consider .gitignore for screenshots/, use CI artifacts
|
||||||
|
7. **Run tests in CI** - Catch visual regressions before production
|
||||||
|
8. **Update baselines deliberately** - Review diffs carefully before accepting
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Requires Node.js >= 16
|
||||||
|
- Browser download needs ~500MB disk space
|
||||||
|
- Screenshot comparison requires consistent rendering (may vary across OS)
|
||||||
|
- LLM analysis adds ~5-10 seconds per screenshot
|
||||||
|
- Not suitable for testing behind VPNs without additional configuration
|
||||||
|
|
||||||
|
## Reference Materials
|
||||||
|
|
||||||
|
| Resource | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `workflow/*.md` | Detailed phase instructions |
|
||||||
|
| `reference/troubleshooting.md` | Common issues and fixes |
|
||||||
|
| `reference/ci-cd-integration.md` | GitHub Actions, GitLab CI examples |
|
||||||
|
| `data/framework-versions.yaml` | Version compatibility database |
|
||||||
|
| `data/error-patterns.yaml` | Known error patterns with recovery |
|
||||||
|
| `templates/` | Config and test templates |
|
||||||
|
| `examples/` | Sample setups for different frameworks |
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] Playwright installed with browsers
|
||||||
|
- [ ] Configuration generated for app type
|
||||||
|
- [ ] Test suite created (3-5 critical journey tests)
|
||||||
|
- [ ] Screenshots captured and organized
|
||||||
|
- [ ] Visual analysis completed with issue categorization
|
||||||
|
- [ ] Regression comparison performed
|
||||||
|
- [ ] Fix recommendations generated
|
||||||
|
- [ ] Test suite exported with documentation
|
||||||
|
- [ ] All tests executable via `npm run test:e2e`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total time**: ~5-8 minutes (excluding one-time Playwright install)
|
||||||
414
skills/playwright-e2e-automation/data/accessibility-checks.md
Normal file
414
skills/playwright-e2e-automation/data/accessibility-checks.md
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
# Accessibility Checks for Visual Analysis
|
||||||
|
|
||||||
|
WCAG 2.1 AA compliance criteria for LLM-powered screenshot analysis.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
When analyzing screenshots, check for these accessibility violations. This guide follows WCAG 2.1 Level AA standards.
|
||||||
|
|
||||||
|
## 1. Color Contrast
|
||||||
|
|
||||||
|
### Minimum Contrast Ratios
|
||||||
|
|
||||||
|
**Text:**
|
||||||
|
- Normal text (< 18pt or < 14pt bold): **4.5:1**
|
||||||
|
- Large text (≥ 18pt or ≥ 14pt bold): **3:1**
|
||||||
|
|
||||||
|
**UI Components:**
|
||||||
|
- Form inputs, buttons, icons: **3:1** against background
|
||||||
|
|
||||||
|
### Common Violations in Screenshots
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Light gray text on white background (2:1 ratio)
|
||||||
|
✅ Dark gray #595959 on white #FFFFFF (7:1 ratio)
|
||||||
|
|
||||||
|
❌ Blue link #4A90E2 on light blue #E8F4FF (1.8:1 ratio)
|
||||||
|
✅ Blue link #0066CC on white #FFFFFF (8.2:1 ratio)
|
||||||
|
|
||||||
|
❌ Gray placeholder text #CCCCCC on white (1.6:1 ratio)
|
||||||
|
✅ Gray placeholder text #757575 on white (4.6:1 ratio)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Indicators
|
||||||
|
|
||||||
|
When analyzing screenshots, look for:
|
||||||
|
- Pale or faded text that's hard to read
|
||||||
|
- Low-contrast buttons that don't stand out
|
||||||
|
- Links that blend into surrounding text
|
||||||
|
- Disabled states that are barely distinguishable
|
||||||
|
|
||||||
|
## 2. Text Size and Readability
|
||||||
|
|
||||||
|
### Minimum Font Sizes
|
||||||
|
|
||||||
|
- Body text: **16px** minimum (1rem)
|
||||||
|
- Small text acceptable: **14px** for secondary content
|
||||||
|
- Avoid: Text smaller than **12px** (fails WCAG)
|
||||||
|
|
||||||
|
### Common Violations
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Body text at 12px - too small for many users
|
||||||
|
✅ Body text at 16px or larger
|
||||||
|
|
||||||
|
❌ Mobile text at 10px - illegible on small screens
|
||||||
|
✅ Mobile text at 14px minimum
|
||||||
|
|
||||||
|
❌ Long paragraphs with no line height spacing
|
||||||
|
✅ Line height 1.5x for body text (e.g., 16px text with 24px line height)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Indicators
|
||||||
|
|
||||||
|
- Text that appears squished or compressed
|
||||||
|
- Long lines of text with minimal spacing
|
||||||
|
- Tiny labels on buttons or form fields
|
||||||
|
|
||||||
|
## 3. Focus Indicators
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
All interactive elements must have **visible focus indicators**:
|
||||||
|
- Minimum **2px** outline or border
|
||||||
|
- Contrast ratio of **3:1** against background
|
||||||
|
- Clearly visible when tabbing through interface
|
||||||
|
|
||||||
|
### Common Violations
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ No visible outline when button is focused
|
||||||
|
✅ Blue 2px outline appears on focus
|
||||||
|
|
||||||
|
❌ Focus outline same color as background (invisible)
|
||||||
|
✅ High-contrast focus outline (e.g., black on white)
|
||||||
|
|
||||||
|
❌ Focus state only indicated by subtle background color change
|
||||||
|
✅ Focus state with outline + background color change
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Indicators in Screenshots
|
||||||
|
|
||||||
|
Look for:
|
||||||
|
- Focused element (if screenshot captures tab state)
|
||||||
|
- Absence of visible outline or border
|
||||||
|
- Focus indicator that's too subtle or low-contrast
|
||||||
|
|
||||||
|
## 4. Form Labels and Instructions
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Every form input must have a visible **<label>** or aria-label
|
||||||
|
- Labels must be **adjacent** to their inputs
|
||||||
|
- Required fields must be clearly indicated
|
||||||
|
- Error messages must be **visible and associated** with inputs
|
||||||
|
|
||||||
|
### Common Violations
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Input with only placeholder text (disappears when typing)
|
||||||
|
✅ Input with persistent label above or beside it
|
||||||
|
|
||||||
|
❌ Label far away from input (hard to associate)
|
||||||
|
✅ Label immediately adjacent to input
|
||||||
|
|
||||||
|
❌ Required field marked only with color (red border)
|
||||||
|
✅ Required field marked with * and "Required" text
|
||||||
|
|
||||||
|
❌ Error message in different part of page
|
||||||
|
✅ Error message directly below input field
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Indicators
|
||||||
|
|
||||||
|
- Inputs without visible labels
|
||||||
|
- Placeholder text used as labels (disappears on focus)
|
||||||
|
- Required fields indicated only by color
|
||||||
|
- Error states without clear error text
|
||||||
|
|
||||||
|
## 5. Heading Hierarchy
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Headings must follow logical order: **H1 → H2 → H3** (no skipping)
|
||||||
|
- Page should have exactly **one H1** (page title)
|
||||||
|
- Headings should be **visually distinct** from body text
|
||||||
|
|
||||||
|
### Common Violations
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Page with H1 → H4 (skips H2, H3)
|
||||||
|
✅ Page with H1 → H2 → H3
|
||||||
|
|
||||||
|
❌ Multiple H1 headings on same page
|
||||||
|
✅ Single H1 for page title, H2s for sections
|
||||||
|
|
||||||
|
❌ Heading text same size as body text
|
||||||
|
✅ Headings progressively larger: H3 < H2 < H1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Indicators in Screenshots
|
||||||
|
|
||||||
|
- Headings that don't look like headings (same size as body)
|
||||||
|
- Missing visual hierarchy (all headings same size)
|
||||||
|
- Text that looks like headings but isn't (bold body text)
|
||||||
|
|
||||||
|
## 6. Alternative Text for Images
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Decorative images: Empty alt="" or aria-hidden="true"
|
||||||
|
- Informative images: Descriptive alt text
|
||||||
|
- Complex images (charts, graphs): Detailed description
|
||||||
|
|
||||||
|
### Common Violations
|
||||||
|
|
||||||
|
**Note:** Can't always detect from screenshots alone, but can identify likely issues:
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Icon buttons with no visible text label (likely missing aria-label)
|
||||||
|
✅ Icon buttons with visible text label or tooltip
|
||||||
|
|
||||||
|
❌ Charts/graphs with no accompanying data table or description
|
||||||
|
✅ Charts with descriptive caption or linked data table
|
||||||
|
|
||||||
|
❌ Images that convey important info but might lack alt text
|
||||||
|
✅ Important info also available in visible text
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Indicators
|
||||||
|
|
||||||
|
- Icon-only buttons without text labels
|
||||||
|
- Charts/infographics without textual explanations
|
||||||
|
- Images that appear to contain important information
|
||||||
|
|
||||||
|
## 7. Keyboard Navigation
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- All interactive elements accessible via keyboard
|
||||||
|
- Logical tab order (top to bottom, left to right)
|
||||||
|
- No keyboard traps
|
||||||
|
- Skip links for navigation
|
||||||
|
|
||||||
|
### Visual Analysis Cues
|
||||||
|
|
||||||
|
**Can identify potential issues from screenshots:**
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Custom dropdown without visible keyboard focus states
|
||||||
|
✅ Standard HTML select or custom with clear focus indicators
|
||||||
|
|
||||||
|
❌ Modal dialog with no visible close button (might trap keyboard)
|
||||||
|
✅ Modal with visible, accessible close button
|
||||||
|
|
||||||
|
❌ Navigation menu requiring hover (might be keyboard inaccessible)
|
||||||
|
✅ Navigation menu that works on click/enter
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Touch Target Size
|
||||||
|
|
||||||
|
### Minimum Sizes (Mobile)
|
||||||
|
|
||||||
|
- Interactive elements: **44x44 CSS pixels** minimum
|
||||||
|
- Adequate spacing between targets: **8px** minimum
|
||||||
|
|
||||||
|
### Common Violations
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Mobile buttons at 32x32px (too small)
|
||||||
|
✅ Mobile buttons at 48x48px
|
||||||
|
|
||||||
|
❌ Links in mobile menu spaced 4px apart (accidental taps)
|
||||||
|
✅ Links spaced 12px apart
|
||||||
|
|
||||||
|
❌ Checkbox at 16x16px on mobile (hard to tap)
|
||||||
|
✅ Checkbox with expanded tap area 44x44px
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Indicators in Mobile Screenshots
|
||||||
|
|
||||||
|
- Tiny buttons that would be hard to tap accurately
|
||||||
|
- Densely packed clickable elements
|
||||||
|
- Links or buttons too close together
|
||||||
|
|
||||||
|
## 9. Responsive Design
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Content readable without horizontal scrolling
|
||||||
|
- No text truncation
|
||||||
|
- Proper scaling on different viewports
|
||||||
|
- No overlapping content
|
||||||
|
|
||||||
|
### Common Violations
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Desktop layout on mobile with horizontal scroll
|
||||||
|
✅ Mobile-optimized layout with no horizontal scroll
|
||||||
|
|
||||||
|
❌ Text cut off at viewport edge
|
||||||
|
✅ Text wraps properly within viewport
|
||||||
|
|
||||||
|
❌ Fixed-width elements overflow on small screens
|
||||||
|
✅ Flexible/responsive elements scale to screen size
|
||||||
|
|
||||||
|
❌ Overlapping elements on mobile (buttons on top of text)
|
||||||
|
✅ Elements stack vertically with proper spacing
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Indicators Across Viewports
|
||||||
|
|
||||||
|
When comparing desktop/tablet/mobile screenshots:
|
||||||
|
- Text that gets cut off on smaller screens
|
||||||
|
- Overlapping or compressed elements
|
||||||
|
- Horizontal scrollbars on mobile
|
||||||
|
- Unreadable small text on mobile
|
||||||
|
|
||||||
|
## 10. Color Not Sole Indicator
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Information must not rely on **color alone**
|
||||||
|
- Use patterns, icons, or text in addition to color
|
||||||
|
|
||||||
|
### Common Violations
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Required fields indicated only by red border
|
||||||
|
✅ Required fields with red border + "*" icon + "Required" text
|
||||||
|
|
||||||
|
❌ Success/error only shown by green/red color
|
||||||
|
✅ Success/error shown by color + icon + text message
|
||||||
|
|
||||||
|
❌ Chart legend with only colored boxes
|
||||||
|
✅ Chart legend with colored boxes + patterns + labels
|
||||||
|
|
||||||
|
❌ Form validation using only red/green highlighting
|
||||||
|
✅ Form validation with color + icons + error text
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Indicators
|
||||||
|
|
||||||
|
- Status indicators using only color
|
||||||
|
- Charts relying solely on color to differentiate data
|
||||||
|
- Form states indicated only by color changes
|
||||||
|
- Links distinguished only by color (not underline)
|
||||||
|
|
||||||
|
## Visual Analysis Workflow
|
||||||
|
|
||||||
|
When analyzing a screenshot for accessibility:
|
||||||
|
|
||||||
|
### Step 1: Text and Contrast
|
||||||
|
1. Check all text for sufficient contrast (4.5:1 for body, 3:1 for large)
|
||||||
|
2. Verify text is large enough (16px minimum)
|
||||||
|
3. Check line height and spacing for readability
|
||||||
|
|
||||||
|
### Step 2: Interactive Elements
|
||||||
|
1. Identify all buttons, links, form inputs
|
||||||
|
2. Verify they have sufficient size (44x44px on mobile)
|
||||||
|
3. Check for visible focus indicators (if focus state captured)
|
||||||
|
4. Ensure adequate spacing between targets
|
||||||
|
|
||||||
|
### Step 3: Form Elements
|
||||||
|
1. Check each input has visible label
|
||||||
|
2. Verify required fields clearly marked (not just color)
|
||||||
|
3. Look for error messages (should be near inputs)
|
||||||
|
|
||||||
|
### Step 4: Structure
|
||||||
|
1. Check heading hierarchy (visual size progression)
|
||||||
|
2. Verify logical content flow
|
||||||
|
3. Look for proper spacing and organization
|
||||||
|
|
||||||
|
### Step 5: Responsive Issues
|
||||||
|
1. Check for text truncation or cutoff
|
||||||
|
2. Look for overlapping elements
|
||||||
|
3. Verify no horizontal scroll
|
||||||
|
4. Ensure touch targets appropriate for viewport
|
||||||
|
|
||||||
|
### Step 6: Color Usage
|
||||||
|
1. Identify any color-only indicators
|
||||||
|
2. Verify status messages use icons or text too
|
||||||
|
3. Check charts/graphs have non-color differentiation
|
||||||
|
|
||||||
|
## Severity Levels
|
||||||
|
|
||||||
|
When reporting accessibility issues from screenshots:
|
||||||
|
|
||||||
|
### Critical (P0)
|
||||||
|
- Contrast ratio < 3:1 for any text
|
||||||
|
- Missing form labels
|
||||||
|
- Keyboard trap (if detectable)
|
||||||
|
- Content not accessible without horizontal scroll
|
||||||
|
|
||||||
|
### High (P1)
|
||||||
|
- Contrast ratio 3:1-4.4:1 for normal text
|
||||||
|
- Touch targets < 44x44px on mobile
|
||||||
|
- Heading hierarchy violations
|
||||||
|
- Color as sole indicator for critical info
|
||||||
|
|
||||||
|
### Medium (P2)
|
||||||
|
- Text size < 14px for body content
|
||||||
|
- Insufficient spacing between touch targets (< 8px)
|
||||||
|
- Inconsistent focus indicators
|
||||||
|
- Minor responsive issues
|
||||||
|
|
||||||
|
### Low (P3)
|
||||||
|
- Line height < 1.4 for long text blocks
|
||||||
|
- Decorative images possibly missing alt (can't confirm from screenshot)
|
||||||
|
- Minor visual hierarchy inconsistencies
|
||||||
|
|
||||||
|
## Example Analysis Output
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Accessibility Issues Found
|
||||||
|
|
||||||
|
### Critical (1)
|
||||||
|
1. **Insufficient color contrast on form labels**
|
||||||
|
- Location: Contact form, all input labels
|
||||||
|
- Issue: Light gray #AAAAAA on white #FFFFFF (2.6:1 ratio)
|
||||||
|
- Requirement: 4.5:1 for normal text
|
||||||
|
- Fix: Use darker gray #595959 (7:1 ratio)
|
||||||
|
|
||||||
|
### High (2)
|
||||||
|
1. **Missing visible labels on inputs**
|
||||||
|
- Location: Email and password fields
|
||||||
|
- Issue: Only placeholder text, no persistent label
|
||||||
|
- Fix: Add visible <label> elements above inputs
|
||||||
|
|
||||||
|
2. **Touch targets too small on mobile**
|
||||||
|
- Location: Social media icons in footer
|
||||||
|
- Issue: Icons are 24x24px (below 44x44px minimum)
|
||||||
|
- Fix: Increase tap area to 44x44px with padding
|
||||||
|
|
||||||
|
### Medium (1)
|
||||||
|
1. **Body text too small**
|
||||||
|
- Location: Article content
|
||||||
|
- Issue: 14px font size (recommended 16px minimum)
|
||||||
|
- Fix: Increase base font size to 16px
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tools for Automated Checking
|
||||||
|
|
||||||
|
While visual analysis is manual, recommend these tools for comprehensive checks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Integrate axe-core in Playwright tests
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import AxeBuilder from '@axe-core/playwright';
|
||||||
|
|
||||||
|
test('should not have accessibility violations', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
|
||||||
|
|
||||||
|
expect(accessibilityScanResults.violations).toEqual([]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**References:**
|
||||||
|
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||||
|
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
||||||
|
- [Axe DevTools](https://www.deque.com/axe/devtools/)
|
||||||
557
skills/playwright-e2e-automation/data/common-ui-bugs.md
Normal file
557
skills/playwright-e2e-automation/data/common-ui-bugs.md
Normal file
@@ -0,0 +1,557 @@
|
|||||||
|
# Common UI Bugs - Visual Analysis Guide
|
||||||
|
|
||||||
|
Patterns and indicators for identifying UI bugs from screenshots during LLM-powered visual analysis.
|
||||||
|
|
||||||
|
## Layout Issues
|
||||||
|
|
||||||
|
### 1. Overlapping Elements
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Text overlapping other text
|
||||||
|
- Buttons overlapping images or other buttons
|
||||||
|
- Content extending beyond container boundaries
|
||||||
|
- Z-index issues causing incorrect stacking
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Modal dialog overlapped by dropdown menu
|
||||||
|
❌ Footer content overlapping main content
|
||||||
|
❌ Notification banner covering navigation
|
||||||
|
❌ Search results hidden behind fixed header
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Look for any elements that appear on top of others unexpectedly
|
||||||
|
- Check if all content is fully visible and not obscured
|
||||||
|
- Verify layering makes sense (modals on top, backgrounds behind)
|
||||||
|
|
||||||
|
### 2. Text Truncation / Overflow
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Text cut off mid-word or mid-letter
|
||||||
|
- Ellipsis (...) in unexpected places
|
||||||
|
- Content extending outside visible area
|
||||||
|
- Horizontal scrollbars on text containers
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Button text: "Continue to Chec..." (truncated on mobile)
|
||||||
|
❌ Table header: "Customer N..." instead of "Customer Name"
|
||||||
|
❌ Card title cut off at viewport edge
|
||||||
|
❌ Long email addresses broken into random positions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Check if all text is fully visible
|
||||||
|
- Look for truncation indicators (...)
|
||||||
|
- Verify important text isn't cut off
|
||||||
|
- Check if text wraps properly on smaller viewports
|
||||||
|
|
||||||
|
### 3. Broken Grid/Flexbox Layouts
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Cards or items with inconsistent sizes
|
||||||
|
- Uneven spacing between elements
|
||||||
|
- Elements not aligned in columns/rows
|
||||||
|
- One element significantly larger/smaller than siblings
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Product grid: 3 cards same height, 1 card twice as tall
|
||||||
|
❌ Navigation items: uneven spacing (10px, 20px, 15px)
|
||||||
|
❌ Form inputs: labels misaligned with inputs
|
||||||
|
❌ Cards: some with images, some without, causing height mismatch
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Check if grid items are evenly sized
|
||||||
|
- Verify consistent spacing between elements
|
||||||
|
- Look for alignment issues in rows/columns
|
||||||
|
- Identify items breaking out of grid structure
|
||||||
|
|
||||||
|
### 4. Responsive Breakpoint Issues
|
||||||
|
|
||||||
|
**Visual Indicators (comparing viewport sizes):**
|
||||||
|
- Desktop layout on mobile (very small text, horizontal scroll)
|
||||||
|
- Mobile layout on desktop (everything too large, wasted space)
|
||||||
|
- Sudden jumps in layout between similar viewport sizes
|
||||||
|
- Media queries not triggering at expected breakpoints
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Desktop: 3-column layout → Mobile: Still 3 columns (too cramped)
|
||||||
|
✅ Desktop: 3-column layout → Tablet: 2 columns → Mobile: 1 column
|
||||||
|
|
||||||
|
❌ Desktop: 16px text → Mobile: 16px text (too small on small screen)
|
||||||
|
✅ Desktop: 16px text → Mobile: 14px text with increased line height
|
||||||
|
|
||||||
|
❌ Fixed sidebar pushes main content off screen on tablet
|
||||||
|
✅ Sidebar collapses to hamburger menu on tablet
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Compare same page across desktop (1280px+), tablet (768px), mobile (375px)
|
||||||
|
- Check if layout adapts appropriately at each size
|
||||||
|
- Verify no horizontal scrolling on mobile
|
||||||
|
- Ensure touch targets are 44x44px minimum on mobile
|
||||||
|
|
||||||
|
## Component-Specific Issues
|
||||||
|
|
||||||
|
### 5. Form Validation Problems
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Error messages in wrong location (far from input)
|
||||||
|
- No visible error state (input looks normal despite error)
|
||||||
|
- Success state not clearly indicated
|
||||||
|
- Disabled buttons without indication why
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Error message at top of page, input in middle (hard to associate)
|
||||||
|
✅ Error message directly below relevant input
|
||||||
|
|
||||||
|
❌ Invalid input: red border only (color-blind users miss it)
|
||||||
|
✅ Invalid input: red border + error icon + error text
|
||||||
|
|
||||||
|
❌ Submit button disabled, no explanation why
|
||||||
|
✅ Submit button disabled with tooltip "Complete required fields"
|
||||||
|
|
||||||
|
❌ Multiple validation errors shown as one generic message
|
||||||
|
✅ Each field shows its specific error message
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Check if error states are clearly visible
|
||||||
|
- Verify error messages are near their inputs
|
||||||
|
- Look for validation indicators beyond just color
|
||||||
|
- Confirm required fields are clearly marked
|
||||||
|
|
||||||
|
### 6. Button States
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Active/hover/focus states indistinguishable
|
||||||
|
- Disabled buttons look clickable
|
||||||
|
- Primary vs secondary buttons unclear
|
||||||
|
- Loading/submitting state not indicated
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Disabled button: gray text on light gray (looks clickable)
|
||||||
|
✅ Disabled button: clear visual indication (opacity, cursor, label)
|
||||||
|
|
||||||
|
❌ Primary and secondary buttons identical appearance
|
||||||
|
✅ Primary: bold color, secondary: outline only
|
||||||
|
|
||||||
|
❌ Button clicked but no loading indicator (looks broken)
|
||||||
|
✅ Button shows spinner or "Loading..." text when clicked
|
||||||
|
|
||||||
|
❌ Hover state same as default state (no feedback)
|
||||||
|
✅ Hover state: darker background or subtle animation
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Verify different button states are visually distinct
|
||||||
|
- Check if disabled buttons clearly look non-interactive
|
||||||
|
- Look for visual feedback on interactive states
|
||||||
|
- Ensure primary actions are visually prominent
|
||||||
|
|
||||||
|
### 7. Image Loading Issues
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Broken image icons (usually a small icon or alt text)
|
||||||
|
- Missing images (blank space where image should be)
|
||||||
|
- Images with wrong aspect ratio (stretched/squashed)
|
||||||
|
- Low-resolution images appearing pixelated
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Product image slot shows broken image icon
|
||||||
|
❌ Profile picture area: empty circle (image failed to load)
|
||||||
|
❌ Banner image: 16:9 image stretched to 1:1 (distorted)
|
||||||
|
❌ Thumbnail: tiny image scaled up 3x (pixelated)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Look for broken image indicators
|
||||||
|
- Check if all images loaded successfully
|
||||||
|
- Verify images maintain proper aspect ratios
|
||||||
|
- Identify pixelated or low-quality images
|
||||||
|
|
||||||
|
### 8. Table/List Issues
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Headers not aligned with columns
|
||||||
|
- Inconsistent row heights
|
||||||
|
- Text overflow in cells
|
||||||
|
- Missing borders or inconsistent borders
|
||||||
|
- Poor mobile table handling (horizontal scroll)
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Table headers offset from column data
|
||||||
|
❌ One row 2x height of others (content wrapping differently)
|
||||||
|
❌ Cell content: "john.doe@verylongemailaddr..." (truncated)
|
||||||
|
❌ Mobile table: requires horizontal scroll to see all columns
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Verify column headers align with data
|
||||||
|
- Check for consistent row heights
|
||||||
|
- Look for text overflow in cells
|
||||||
|
- Ensure tables are readable on mobile (responsive design)
|
||||||
|
|
||||||
|
## Content Issues
|
||||||
|
|
||||||
|
### 9. Missing Content
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Empty sections (just headers, no content)
|
||||||
|
- Placeholder text in production ("Lorem ipsum", "TBD", "Coming soon")
|
||||||
|
- Missing images or icons where expected
|
||||||
|
- Incomplete sentences or paragraphs
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Section header "Latest Articles" with no articles below
|
||||||
|
❌ Product description: "Lorem ipsum dolor sit amet..."
|
||||||
|
❌ Icon placeholder: gray square instead of actual icon
|
||||||
|
❌ Bio section: ends mid-sentence
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Identify any placeholder content
|
||||||
|
- Look for empty sections or containers
|
||||||
|
- Check if all expected content is present
|
||||||
|
- Verify no incomplete text
|
||||||
|
|
||||||
|
### 10. Inconsistent Spacing/Padding
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Uneven margins between sections
|
||||||
|
- Inconsistent padding inside containers
|
||||||
|
- Elements touching edges (no breathing room)
|
||||||
|
- Random spacing that doesn't follow a system
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Section 1: 40px margin → Section 2: 25px margin → Section 3: 35px
|
||||||
|
✅ All sections: consistent 40px margin
|
||||||
|
|
||||||
|
❌ Card padding: 16px top, 20px right, 14px bottom, 18px left
|
||||||
|
✅ Card padding: 16px all sides
|
||||||
|
|
||||||
|
❌ Button text touching button edge (no padding)
|
||||||
|
✅ Button: 12px vertical, 20px horizontal padding
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Check for consistent spacing throughout page
|
||||||
|
- Verify elements have appropriate padding
|
||||||
|
- Look for crowded areas with insufficient spacing
|
||||||
|
- Identify spacing that breaks visual rhythm
|
||||||
|
|
||||||
|
## Typography Issues
|
||||||
|
|
||||||
|
### 11. Font Rendering Problems
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Jagged or pixelated text
|
||||||
|
- Text weight too thin (hard to read)
|
||||||
|
- Inconsistent font families
|
||||||
|
- Line height too tight or too loose
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Body text: font-weight 100 (barely visible)
|
||||||
|
✅ Body text: font-weight 400 (readable)
|
||||||
|
|
||||||
|
❌ Headings: Arial → Body: Times New Roman (inconsistent)
|
||||||
|
✅ All text: consistent font family
|
||||||
|
|
||||||
|
❌ Long paragraph: line-height 1.0 (text touching)
|
||||||
|
✅ Long paragraph: line-height 1.5 (readable spacing)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Check if text is crisp and readable
|
||||||
|
- Verify consistent font families
|
||||||
|
- Look for appropriate line height (1.4-1.6 for body text)
|
||||||
|
- Ensure font weights are accessible
|
||||||
|
|
||||||
|
### 12. Text Alignment Issues
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Center-aligned paragraphs (hard to read)
|
||||||
|
- Inconsistent alignment within a section
|
||||||
|
- Right-aligned text in LTR layout without reason
|
||||||
|
- Justified text with large gaps
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Long paragraph: center-aligned (hard to follow)
|
||||||
|
✅ Long paragraph: left-aligned
|
||||||
|
|
||||||
|
❌ Form: labels left-aligned, some center-aligned randomly
|
||||||
|
✅ Form: all labels consistently left-aligned
|
||||||
|
|
||||||
|
❌ Justified text: large gaps between words ("rivers")
|
||||||
|
✅ Left-aligned text with ragged right edge
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Check if alignment aids readability
|
||||||
|
- Verify consistent alignment within sections
|
||||||
|
- Look for awkward gaps in justified text
|
||||||
|
- Ensure alignment makes sense for content type
|
||||||
|
|
||||||
|
## Interactive Element Issues
|
||||||
|
|
||||||
|
### 13. Hover/Focus States Missing
|
||||||
|
|
||||||
|
**Note:** Only detectable if screenshot captures focused/hovered state
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Link looks identical to surrounding text (no underline, same color)
|
||||||
|
- Focused input indistinguishable from unfocused
|
||||||
|
- Hovered button shows no change
|
||||||
|
- Dropdown menu items don't highlight on hover
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Link: blue text, no underline, same as normal text
|
||||||
|
✅ Link: blue text with underline, or different color
|
||||||
|
|
||||||
|
❌ Input focused: looks identical to unfocused state
|
||||||
|
✅ Input focused: blue border or outline appears
|
||||||
|
|
||||||
|
❌ Menu item hovered: no visual change
|
||||||
|
✅ Menu item hovered: background color change
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis (if interactive state captured):**
|
||||||
|
- Verify interactive elements show visual feedback
|
||||||
|
- Check if focused element has clear indicator
|
||||||
|
- Look for hover states that provide feedback
|
||||||
|
- Ensure keyboard focus is visible
|
||||||
|
|
||||||
|
### 14. Icon Issues
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Icons misaligned with text
|
||||||
|
- Icons wrong size (too large or too small)
|
||||||
|
- Icons wrong color (low contrast, invisible)
|
||||||
|
- Icon-only buttons without labels or tooltips
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Icon button: 16x16px icon in 48x48px button (looks lost)
|
||||||
|
✅ Icon button: 24x24px icon in 48x48px button (balanced)
|
||||||
|
|
||||||
|
❌ Icon: white on light gray background (barely visible)
|
||||||
|
✅ Icon: dark gray on light gray (clear contrast)
|
||||||
|
|
||||||
|
❌ Icon: baseline-aligned with text (appears raised)
|
||||||
|
✅ Icon: center-aligned with text
|
||||||
|
|
||||||
|
❌ Icon-only button with no label (unclear purpose)
|
||||||
|
✅ Icon button with aria-label or visible text label
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Check if icons are appropriately sized
|
||||||
|
- Verify icons have sufficient contrast
|
||||||
|
- Look for proper alignment with adjacent text
|
||||||
|
- Ensure icon buttons have clear purpose
|
||||||
|
|
||||||
|
## Color and Theme Issues
|
||||||
|
|
||||||
|
### 15. Dark Mode Issues
|
||||||
|
|
||||||
|
**Visual Indicators (when comparing light/dark screenshots):**
|
||||||
|
- White text on light background (inverted incorrectly)
|
||||||
|
- Hard-coded colors not switching with theme
|
||||||
|
- Images/logos with wrong theme variant
|
||||||
|
- Insufficient contrast in dark mode
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Dark mode: #333 text on #000 background (low contrast)
|
||||||
|
✅ Dark mode: #E0E0E0 text on #1A1A1A background
|
||||||
|
|
||||||
|
❌ Light mode logo on dark background (invisible)
|
||||||
|
✅ Dark mode variant logo displayed
|
||||||
|
|
||||||
|
❌ Input background: white in both modes (wrong in dark)
|
||||||
|
✅ Input background: white in light, #2A2A2A in dark
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis (compare light/dark if available):**
|
||||||
|
- Verify all colors invert appropriately
|
||||||
|
- Check contrast ratios in both modes
|
||||||
|
- Look for hard-coded colors that don't adapt
|
||||||
|
- Ensure images/logos have correct variants
|
||||||
|
|
||||||
|
### 16. Brand Color Misuse
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Too many competing colors
|
||||||
|
- Brand colors used incorrectly (primary for everything)
|
||||||
|
- Status colors confusing (green for error, red for success)
|
||||||
|
- Inaccessible color combinations
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ All buttons primary color (no hierarchy)
|
||||||
|
✅ Primary button: brand color, secondary: gray/outline
|
||||||
|
|
||||||
|
❌ Success message in red, error in green (confusing)
|
||||||
|
✅ Success in green, error in red, warning in amber
|
||||||
|
|
||||||
|
❌ 8 different colors used on one page (chaotic)
|
||||||
|
✅ Consistent color palette: 2-3 main colors + neutrals
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Check if color usage is consistent and meaningful
|
||||||
|
- Verify status colors match conventions (green=success, red=error)
|
||||||
|
- Look for excessive color variety
|
||||||
|
- Ensure brand colors used appropriately
|
||||||
|
|
||||||
|
## Animation and Transition Issues
|
||||||
|
|
||||||
|
**Note:** Difficult to detect from static screenshots, but can infer
|
||||||
|
|
||||||
|
### 17. Loading States
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Content area completely empty (no skeleton/spinner)
|
||||||
|
- "Loading..." text with no visual indicator
|
||||||
|
- Sudden content appearance (jarring)
|
||||||
|
- Infinite loading (screenshot shows spinner forever)
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Empty white space while loading (looks broken)
|
||||||
|
✅ Skeleton UI placeholders during load
|
||||||
|
|
||||||
|
❌ Just text "Loading..." (static, looks stuck)
|
||||||
|
✅ Animated spinner + "Loading..." text
|
||||||
|
|
||||||
|
❌ Screenshot from 30 seconds ago: still loading (timeout issue)
|
||||||
|
✅ Content loads within reasonable time (< 3 seconds)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis:**
|
||||||
|
- Look for loading indicators
|
||||||
|
- Check if empty states have placeholders
|
||||||
|
- Identify potential timeout issues (loading too long)
|
||||||
|
|
||||||
|
## Mobile-Specific Issues
|
||||||
|
|
||||||
|
### 18. Fixed Positioning Problems
|
||||||
|
|
||||||
|
**Visual Indicators:**
|
||||||
|
- Fixed header covering content (not enough top padding)
|
||||||
|
- Fixed footer hiding interactive elements
|
||||||
|
- Input fields hidden behind keyboard (inferred)
|
||||||
|
- Fixed elements overlapping each other
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Fixed header: covers first line of content
|
||||||
|
✅ Content has top padding equal to header height
|
||||||
|
|
||||||
|
❌ Fixed "Chat with us" button: covers form submit button
|
||||||
|
✅ Fixed button repositions when other content appears
|
||||||
|
|
||||||
|
❌ Input field: likely behind keyboard when focused
|
||||||
|
✅ Page scrolls input into view above keyboard
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis (mobile viewports):**
|
||||||
|
- Check if fixed headers leave room for content
|
||||||
|
- Verify fixed elements don't overlap important content
|
||||||
|
- Look for sufficient padding to account for fixed elements
|
||||||
|
|
||||||
|
### 19. Orientation Issues
|
||||||
|
|
||||||
|
**Visual Indicators (portrait vs landscape):**
|
||||||
|
- Content cut off in landscape mode
|
||||||
|
- Poor use of available space in landscape
|
||||||
|
- Fixed height elements that don't adapt
|
||||||
|
- Horizontal layout forced into vertical space
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
❌ Portrait: shows full content → Landscape: content cut off
|
||||||
|
✅ Both orientations show full content
|
||||||
|
|
||||||
|
❌ Landscape: wide empty margins, cramped center content
|
||||||
|
✅ Landscape: content uses available width appropriately
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screenshot Analysis (if both orientations available):**
|
||||||
|
- Compare same page in portrait and landscape
|
||||||
|
- Verify content adapts to available space
|
||||||
|
- Check if all content remains accessible
|
||||||
|
|
||||||
|
## Analysis Priority
|
||||||
|
|
||||||
|
When analyzing screenshots, prioritize issues by impact:
|
||||||
|
|
||||||
|
### Critical (Stop immediately)
|
||||||
|
1. Content completely missing or invisible
|
||||||
|
2. Major layout breaks (overlapping, off-screen)
|
||||||
|
3. Severe contrast violations (< 3:1)
|
||||||
|
4. Broken images or core UI elements
|
||||||
|
|
||||||
|
### High (Fix soon)
|
||||||
|
1. Text truncation losing important info
|
||||||
|
2. Form validation not visible
|
||||||
|
3. Responsive breakpoint failures
|
||||||
|
4. Touch targets too small (< 44px)
|
||||||
|
|
||||||
|
### Medium (Fix in next iteration)
|
||||||
|
1. Inconsistent spacing
|
||||||
|
2. Minor alignment issues
|
||||||
|
3. Missing hover/focus states
|
||||||
|
4. Moderate contrast issues (3:1-4.4:1)
|
||||||
|
|
||||||
|
### Low (Polish)
|
||||||
|
1. Minor typography inconsistencies
|
||||||
|
2. Slight spacing irregularities
|
||||||
|
3. Non-critical icon sizing
|
||||||
|
4. Subtle animation issues
|
||||||
|
|
||||||
|
## Generating Bug Reports
|
||||||
|
|
||||||
|
For each issue found, provide:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### [Issue Title]
|
||||||
|
|
||||||
|
**Severity**: Critical | High | Medium | Low
|
||||||
|
|
||||||
|
**Location**: [Specific page/component where visible]
|
||||||
|
|
||||||
|
**Screenshot**: `path/to/screenshot.png` (timestamp: YYYY-MM-DD HH:MM:SS)
|
||||||
|
|
||||||
|
**Viewport**: Desktop 1280x720 | Tablet 768x1024 | Mobile 375x667
|
||||||
|
|
||||||
|
**Description**: [Clear description of what's wrong]
|
||||||
|
|
||||||
|
**Expected Behavior**: [What should appear instead]
|
||||||
|
|
||||||
|
**Likely Cause**: [Technical reason, e.g., "Missing max-width constraint", "Improper flexbox configuration"]
|
||||||
|
|
||||||
|
**Recommended Fix**:
|
||||||
|
- **File**: `src/components/Button.tsx`
|
||||||
|
- **Line**: 45
|
||||||
|
- **Current**: \`className="px-4 text-xl"\`
|
||||||
|
- **Fixed**: \`className="px-4 text-sm sm:text-base md:text-xl max-w-full"\`
|
||||||
|
- **Reasoning**: Text size needs responsive scaling and max-width to prevent overflow on mobile
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember**: Focus on bugs that impact **usability** and **accessibility**. Not every minor imperfection is critical. Prioritize issues that prevent users from completing tasks or accessing content.
|
||||||
415
skills/playwright-e2e-automation/data/error-patterns.yaml
Normal file
415
skills/playwright-e2e-automation/data/error-patterns.yaml
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
# Error Pattern Recovery Database
|
||||||
|
# Maps common error patterns to diagnosis and recovery steps
|
||||||
|
|
||||||
|
# This file is used for:
|
||||||
|
# 1. Pre-flight health checks - detect errors before running full test suite
|
||||||
|
# 2. Test failure analysis - provide actionable fixes when tests fail
|
||||||
|
# 3. User guidance - self-service troubleshooting
|
||||||
|
|
||||||
|
# ========== CSS & STYLING ERRORS ==========
|
||||||
|
css_errors:
|
||||||
|
tailwind_v4_syntax_mismatch:
|
||||||
|
pattern: "Cannot apply unknown utility class"
|
||||||
|
alternative_patterns:
|
||||||
|
- "Utilities must be known at build time"
|
||||||
|
- "Unknown utility class"
|
||||||
|
|
||||||
|
diagnosis: "Tailwind CSS v4 detected but v3 syntax used in CSS file"
|
||||||
|
|
||||||
|
severity: "critical"
|
||||||
|
category: "configuration"
|
||||||
|
|
||||||
|
root_cause: |
|
||||||
|
Tailwind CSS v4 changed from @tailwind directives to @import syntax.
|
||||||
|
Your CSS file likely still uses the old @tailwind directives.
|
||||||
|
|
||||||
|
detection_method: "console_error"
|
||||||
|
|
||||||
|
recovery_steps:
|
||||||
|
- step: "Identify your main CSS file"
|
||||||
|
details: "Usually src/index.css, src/App.css, or src/globals.css"
|
||||||
|
|
||||||
|
- step: "Update CSS directives"
|
||||||
|
from: |
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
to: |
|
||||||
|
@import "tailwindcss";
|
||||||
|
files_to_check:
|
||||||
|
- "src/index.css"
|
||||||
|
- "src/App.css"
|
||||||
|
- "src/globals.css"
|
||||||
|
- "src/styles/globals.css"
|
||||||
|
|
||||||
|
- step: "Update PostCSS configuration"
|
||||||
|
from: |
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
}
|
||||||
|
to: |
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
autoprefixer: {},
|
||||||
|
}
|
||||||
|
files_to_check:
|
||||||
|
- "postcss.config.js"
|
||||||
|
- "postcss.config.cjs"
|
||||||
|
- "postcss.config.mjs"
|
||||||
|
|
||||||
|
- step: "Restart dev server"
|
||||||
|
command: "npm run dev"
|
||||||
|
reason: "CSS changes require server restart"
|
||||||
|
|
||||||
|
- step: "Clear browser cache and reload"
|
||||||
|
details: "Hard refresh: Ctrl+Shift+R (Windows) or Cmd+Shift+R (Mac)"
|
||||||
|
|
||||||
|
prevention: |
|
||||||
|
The skill now detects Tailwind version and uses appropriate templates.
|
||||||
|
This error should not occur in new setups.
|
||||||
|
|
||||||
|
documentation: "https://tailwindcss.com/docs/upgrade-guide"
|
||||||
|
|
||||||
|
related_errors:
|
||||||
|
- "postcss_plugin_not_found"
|
||||||
|
|
||||||
|
postcss_plugin_not_found:
|
||||||
|
pattern: "Plugin tailwindcss not found"
|
||||||
|
alternative_patterns:
|
||||||
|
- "Cannot find module 'tailwindcss'"
|
||||||
|
- "postcss plugin tailwindcss not found"
|
||||||
|
|
||||||
|
diagnosis: "PostCSS configuration uses old Tailwind v3 plugin name with Tailwind v4"
|
||||||
|
|
||||||
|
severity: "critical"
|
||||||
|
category: "configuration"
|
||||||
|
|
||||||
|
root_cause: |
|
||||||
|
Tailwind CSS v4 renamed its PostCSS plugin from 'tailwindcss' to '@tailwindcss/postcss'.
|
||||||
|
Your postcss.config.js still references the old plugin name.
|
||||||
|
|
||||||
|
recovery_steps:
|
||||||
|
- step: "Update postcss.config.js"
|
||||||
|
from: |
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
to: |
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
file: "postcss.config.js"
|
||||||
|
|
||||||
|
- step: "Verify @tailwindcss/postcss is installed"
|
||||||
|
command: "npm list @tailwindcss/postcss"
|
||||||
|
if_not_installed: "npm install -D @tailwindcss/postcss"
|
||||||
|
|
||||||
|
- step: "Restart dev server"
|
||||||
|
command: "npm run dev"
|
||||||
|
|
||||||
|
documentation: "https://tailwindcss.com/docs/upgrade-guide#migrating-from-v3"
|
||||||
|
|
||||||
|
# ========== ACCESSIBILITY ERRORS ==========
|
||||||
|
accessibility_errors:
|
||||||
|
heading_hierarchy_violation:
|
||||||
|
pattern: "heading-order - Heading levels should only increase by one"
|
||||||
|
alternative_patterns:
|
||||||
|
- "Heading levels should increase by one"
|
||||||
|
- "heading-order violation"
|
||||||
|
|
||||||
|
diagnosis: "WCAG heading hierarchy violation - skipped heading levels"
|
||||||
|
|
||||||
|
severity: "moderate"
|
||||||
|
category: "accessibility"
|
||||||
|
|
||||||
|
root_cause: |
|
||||||
|
HTML heading elements (h1-h6) must follow logical order without skipping levels.
|
||||||
|
For example: h1 → h2 → h3 is correct, but h1 → h3 (skipping h2) is incorrect.
|
||||||
|
|
||||||
|
recovery_steps:
|
||||||
|
- step: "Locate the problematic heading in test output"
|
||||||
|
details: "Playwright accessibility tests will show file and line number"
|
||||||
|
|
||||||
|
- step: "Check heading hierarchy in that component"
|
||||||
|
example: |
|
||||||
|
❌ Bad:
|
||||||
|
<h1>Page Title</h1>
|
||||||
|
<h3>Section</h3> <!-- Skips h2 -->
|
||||||
|
|
||||||
|
✅ Good:
|
||||||
|
<h1>Page Title</h1>
|
||||||
|
<h2>Section</h2>
|
||||||
|
|
||||||
|
- step: "Fix heading levels to follow order"
|
||||||
|
details: "Ensure each heading is only one level deeper than its parent"
|
||||||
|
|
||||||
|
- step: "Re-run accessibility tests"
|
||||||
|
command: "npm run test:e2e -- accessibility.spec.ts"
|
||||||
|
|
||||||
|
prevention: |
|
||||||
|
Always outline content structure before implementing:
|
||||||
|
- Page title: h1 (only one per page)
|
||||||
|
- Main sections: h2
|
||||||
|
- Subsections: h3
|
||||||
|
- Sub-subsections: h4
|
||||||
|
|
||||||
|
documentation: "https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html"
|
||||||
|
|
||||||
|
missing_form_labels:
|
||||||
|
pattern: "Form elements must have labels"
|
||||||
|
alternative_patterns:
|
||||||
|
- "label - Form elements must have labels"
|
||||||
|
- "Inputs must have associated labels"
|
||||||
|
|
||||||
|
diagnosis: "Form input missing associated label element"
|
||||||
|
|
||||||
|
severity: "high"
|
||||||
|
category: "accessibility"
|
||||||
|
|
||||||
|
root_cause: |
|
||||||
|
Every form input must have an associated <label> element for screen reader users.
|
||||||
|
Placeholder text alone is NOT sufficient.
|
||||||
|
|
||||||
|
recovery_steps:
|
||||||
|
- step: "Add label element to input"
|
||||||
|
from: |
|
||||||
|
<input type="email" placeholder="Email" />
|
||||||
|
to: |
|
||||||
|
<label htmlFor="email">Email Address</label>
|
||||||
|
<input id="email" type="email" placeholder="Email" />
|
||||||
|
|
||||||
|
- step: "Alternative: Use aria-label if visual label not desired"
|
||||||
|
example: |
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
aria-label="Search"
|
||||||
|
placeholder="Search..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
- step: "Ensure label and input are properly associated"
|
||||||
|
details: "Use 'htmlFor' attribute matching input 'id'"
|
||||||
|
|
||||||
|
- step: "Re-run accessibility tests"
|
||||||
|
command: "npm run test:e2e -- accessibility.spec.ts"
|
||||||
|
|
||||||
|
documentation: "https://www.w3.org/WAI/tutorials/forms/labels/"
|
||||||
|
|
||||||
|
insufficient_color_contrast:
|
||||||
|
pattern: "color-contrast - Elements must meet minimum color contrast ratio"
|
||||||
|
alternative_patterns:
|
||||||
|
- "color contrast ratio"
|
||||||
|
- "contrast ratio"
|
||||||
|
|
||||||
|
diagnosis: "Text or UI element has insufficient color contrast (WCAG 2.1 AA violation)"
|
||||||
|
|
||||||
|
severity: "high"
|
||||||
|
category: "accessibility"
|
||||||
|
|
||||||
|
root_cause: |
|
||||||
|
WCAG 2.1 AA requires:
|
||||||
|
- Normal text: 4.5:1 contrast ratio
|
||||||
|
- Large text (18pt+ or 14pt+ bold): 3:1 contrast ratio
|
||||||
|
- UI components: 3:1 contrast ratio
|
||||||
|
|
||||||
|
recovery_steps:
|
||||||
|
- step: "Identify low-contrast element from test output"
|
||||||
|
details: "Test results show which element and current ratio"
|
||||||
|
|
||||||
|
- step: "Use WebAIM contrast checker"
|
||||||
|
url: "https://webaim.org/resources/contrastchecker/"
|
||||||
|
details: "Test current colors and find compliant alternatives"
|
||||||
|
|
||||||
|
- step: "Update color values"
|
||||||
|
example: |
|
||||||
|
❌ Bad: #AAAAAA on #FFFFFF (2.6:1 ratio)
|
||||||
|
✅ Good: #595959 on #FFFFFF (7:1 ratio)
|
||||||
|
|
||||||
|
For Tailwind:
|
||||||
|
❌ text-gray-400 → ✅ text-gray-700
|
||||||
|
|
||||||
|
- step: "Re-run accessibility tests"
|
||||||
|
command: "npm run test:e2e -- accessibility.spec.ts"
|
||||||
|
|
||||||
|
prevention: |
|
||||||
|
Use established color palettes with pre-tested contrast ratios.
|
||||||
|
Tailwind's default palette (500+ for text) generally meets WCAG AA.
|
||||||
|
|
||||||
|
documentation: "https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html"
|
||||||
|
|
||||||
|
# ========== BUILD & CONFIGURATION ERRORS ==========
|
||||||
|
build_errors:
|
||||||
|
vite_port_in_use:
|
||||||
|
pattern: "Port 5173 is in use"
|
||||||
|
alternative_patterns:
|
||||||
|
- "EADDRINUSE"
|
||||||
|
- "address already in use"
|
||||||
|
|
||||||
|
diagnosis: "Vite dev server port already in use by another process"
|
||||||
|
|
||||||
|
severity: "moderate"
|
||||||
|
category: "environment"
|
||||||
|
|
||||||
|
recovery_steps:
|
||||||
|
- step: "Find process using port 5173"
|
||||||
|
command: "lsof -i :5173"
|
||||||
|
platforms: ["mac", "linux"]
|
||||||
|
|
||||||
|
- step: "Kill the process"
|
||||||
|
command: "kill -9 <PID>"
|
||||||
|
details: "Replace <PID> with process ID from previous command"
|
||||||
|
|
||||||
|
- step: "Alternative: Use different port"
|
||||||
|
details: "Add to vite.config.ts:"
|
||||||
|
config: |
|
||||||
|
server: {
|
||||||
|
port: 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
- step: "Restart dev server"
|
||||||
|
command: "npm run dev"
|
||||||
|
|
||||||
|
playwright_browsers_not_installed:
|
||||||
|
pattern: "Executable doesn't exist"
|
||||||
|
alternative_patterns:
|
||||||
|
- "browserType.launch: Executable doesn't exist"
|
||||||
|
- "Browser not found"
|
||||||
|
|
||||||
|
diagnosis: "Playwright browser binaries not installed"
|
||||||
|
|
||||||
|
severity: "critical"
|
||||||
|
category: "installation"
|
||||||
|
|
||||||
|
recovery_steps:
|
||||||
|
- step: "Install Playwright browsers"
|
||||||
|
command: "npx playwright install"
|
||||||
|
details: "Downloads Chromium, Firefox, and WebKit"
|
||||||
|
|
||||||
|
- step: "Install system dependencies (Linux only)"
|
||||||
|
command: "npx playwright install-deps"
|
||||||
|
platforms: ["linux"]
|
||||||
|
|
||||||
|
- step: "Verify installation"
|
||||||
|
command: "npx playwright test --list"
|
||||||
|
expected: "Should list available tests without errors"
|
||||||
|
|
||||||
|
typescript_strict_errors:
|
||||||
|
pattern: "TypeScript strict mode errors"
|
||||||
|
alternative_patterns:
|
||||||
|
- "TS2345"
|
||||||
|
- "TS2322"
|
||||||
|
- "Type 'any' is not assignable"
|
||||||
|
|
||||||
|
diagnosis: "TypeScript strict mode violations in generated code"
|
||||||
|
|
||||||
|
severity: "moderate"
|
||||||
|
category: "type_safety"
|
||||||
|
|
||||||
|
recovery_steps:
|
||||||
|
- step: "Review TypeScript errors in output"
|
||||||
|
details: "Each error shows file, line, and type issue"
|
||||||
|
|
||||||
|
- step: "Add explicit type annotations"
|
||||||
|
example: |
|
||||||
|
❌ const data = await fetch(url);
|
||||||
|
✅ const data: Response = await fetch(url);
|
||||||
|
|
||||||
|
- step: "Use type assertions cautiously"
|
||||||
|
example: |
|
||||||
|
const element = page.locator('#id') as Locator;
|
||||||
|
|
||||||
|
- step: "Fix null/undefined handling"
|
||||||
|
example: |
|
||||||
|
❌ const text = element.textContent();
|
||||||
|
✅ const text = await element.textContent() ?? '';
|
||||||
|
|
||||||
|
# ========== RESPONSIVE & LAYOUT ERRORS ==========
|
||||||
|
layout_errors:
|
||||||
|
horizontal_scroll_mobile:
|
||||||
|
pattern: "Horizontal scroll detected on mobile viewport"
|
||||||
|
|
||||||
|
diagnosis: "Content wider than viewport on mobile, causing horizontal scroll"
|
||||||
|
|
||||||
|
severity: "moderate"
|
||||||
|
category: "responsive"
|
||||||
|
|
||||||
|
root_cause: |
|
||||||
|
Fixed-width elements or overflow content exceeds mobile viewport width.
|
||||||
|
Common causes: large images, wide tables, fixed px widths.
|
||||||
|
|
||||||
|
recovery_steps:
|
||||||
|
- step: "Identify overflowing element"
|
||||||
|
details: "Use browser DevTools → Elements → Computed → scroll width"
|
||||||
|
|
||||||
|
- step: "Make element responsive"
|
||||||
|
example: |
|
||||||
|
❌ width: 800px;
|
||||||
|
✅ max-width: 100%;
|
||||||
|
|
||||||
|
❌ className="w-[800px]"
|
||||||
|
✅ className="w-full max-w-screen-lg"
|
||||||
|
|
||||||
|
- step: "Add overflow handling"
|
||||||
|
example: |
|
||||||
|
className="overflow-x-auto" // For tables
|
||||||
|
className="overflow-hidden" // For containers
|
||||||
|
|
||||||
|
- step: "Test on mobile viewport"
|
||||||
|
command: "npm run test:e2e -- --project=mobile-chrome"
|
||||||
|
|
||||||
|
# ========== RECOVERY STRATEGIES ==========
|
||||||
|
recovery_strategies:
|
||||||
|
incremental_fix:
|
||||||
|
description: "Fix errors one at a time, test after each fix"
|
||||||
|
when_to_use: "Multiple related errors"
|
||||||
|
steps:
|
||||||
|
- "Fix highest severity error first"
|
||||||
|
- "Run tests to verify fix"
|
||||||
|
- "Move to next error"
|
||||||
|
- "Repeat until all errors resolved"
|
||||||
|
|
||||||
|
clean_slate:
|
||||||
|
description: "Remove generated files and regenerate"
|
||||||
|
when_to_use: "Configuration completely broken"
|
||||||
|
steps:
|
||||||
|
- "Delete generated playwright.config.ts"
|
||||||
|
- "Delete tests/ directory"
|
||||||
|
- "Clear node_modules/.cache/"
|
||||||
|
- "Re-run skill"
|
||||||
|
|
||||||
|
version_rollback:
|
||||||
|
description: "Downgrade to last working version"
|
||||||
|
when_to_use: "Breaking change in newly installed package"
|
||||||
|
steps:
|
||||||
|
- "Check package.json for recently updated packages"
|
||||||
|
- "Install previous version: npm install package@previous-version"
|
||||||
|
- "Lock version in package.json"
|
||||||
|
- "Test to confirm working"
|
||||||
|
|
||||||
|
# ========== ERROR SEVERITY LEVELS ==========
|
||||||
|
severity_definitions:
|
||||||
|
critical:
|
||||||
|
impact: "App won't build or run"
|
||||||
|
response_time: "Fix immediately"
|
||||||
|
examples: ["Syntax errors", "Missing dependencies", "Config errors"]
|
||||||
|
|
||||||
|
high:
|
||||||
|
impact: "Major functionality broken"
|
||||||
|
response_time: "Fix within hours"
|
||||||
|
examples: ["Accessibility violations", "Failed test cases", "Type errors"]
|
||||||
|
|
||||||
|
moderate:
|
||||||
|
impact: "Reduced usability or performance"
|
||||||
|
response_time: "Fix within days"
|
||||||
|
examples: ["Layout issues", "Minor accessibility", "Warnings"]
|
||||||
|
|
||||||
|
low:
|
||||||
|
impact: "Cosmetic or minor issues"
|
||||||
|
response_time: "Fix when convenient"
|
||||||
|
examples: ["Code style", "Documentation", "Minor visual bugs"]
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
# Framework Detection Patterns
|
||||||
|
# Used to automatically identify application type and determine optimal Playwright configuration
|
||||||
|
|
||||||
|
# Detection happens in phases:
|
||||||
|
# 1. Read package.json dependencies
|
||||||
|
# 2. Check for config files
|
||||||
|
# 3. Identify dev server command
|
||||||
|
# 4. Determine base URL and port
|
||||||
|
|
||||||
|
frameworks:
|
||||||
|
# ========== REACT + VITE ==========
|
||||||
|
react_vite:
|
||||||
|
name: "React + Vite"
|
||||||
|
priority: 1 # Check first (most common modern setup)
|
||||||
|
|
||||||
|
detection:
|
||||||
|
package_json_dependencies:
|
||||||
|
required:
|
||||||
|
- "react"
|
||||||
|
- "vite"
|
||||||
|
optional:
|
||||||
|
- "react-dom"
|
||||||
|
- "@vitejs/plugin-react"
|
||||||
|
|
||||||
|
config_files:
|
||||||
|
- "vite.config.ts"
|
||||||
|
- "vite.config.js"
|
||||||
|
- "vite.config.mjs"
|
||||||
|
|
||||||
|
indicators:
|
||||||
|
- "src/main.tsx exists"
|
||||||
|
- "src/main.jsx exists"
|
||||||
|
- "index.html with Vite script tag"
|
||||||
|
|
||||||
|
configuration:
|
||||||
|
base_url: "http://localhost:5173"
|
||||||
|
dev_server_command: "npm run dev"
|
||||||
|
build_command: "npm run build"
|
||||||
|
preview_command: "npm run preview"
|
||||||
|
|
||||||
|
playwright_config:
|
||||||
|
timeout: 30000
|
||||||
|
retries: 2
|
||||||
|
use:
|
||||||
|
viewport: { width: 1280, height: 720 }
|
||||||
|
screenshot: "only-on-failure"
|
||||||
|
video: "retain-on-failure"
|
||||||
|
|
||||||
|
projects:
|
||||||
|
- name: "chromium"
|
||||||
|
use:
|
||||||
|
browserName: "chromium"
|
||||||
|
- name: "mobile"
|
||||||
|
use:
|
||||||
|
...devices["iPhone 13"]
|
||||||
|
|
||||||
|
webServer:
|
||||||
|
command: "npm run dev"
|
||||||
|
url: "http://localhost:5173"
|
||||||
|
reuseExistingServer: true
|
||||||
|
timeout: 120000
|
||||||
|
|
||||||
|
test_generation:
|
||||||
|
entry_point: "src/App.tsx"
|
||||||
|
routing: "react-router-dom" # If detected in dependencies
|
||||||
|
state_management: ["redux", "zustand", "jotai"] # Check for these
|
||||||
|
common_pages:
|
||||||
|
- "/"
|
||||||
|
- "/about"
|
||||||
|
- "/contact"
|
||||||
|
- "/login"
|
||||||
|
- "/dashboard"
|
||||||
|
|
||||||
|
# ========== NEXT.JS ==========
|
||||||
|
nextjs:
|
||||||
|
name: "Next.js"
|
||||||
|
priority: 2
|
||||||
|
|
||||||
|
detection:
|
||||||
|
package_json_dependencies:
|
||||||
|
required:
|
||||||
|
- "next"
|
||||||
|
- "react"
|
||||||
|
optional:
|
||||||
|
- "react-dom"
|
||||||
|
|
||||||
|
config_files:
|
||||||
|
- "next.config.js"
|
||||||
|
- "next.config.mjs"
|
||||||
|
- "next.config.ts"
|
||||||
|
|
||||||
|
indicators:
|
||||||
|
- "app/ directory exists" # App Router
|
||||||
|
- "pages/ directory exists" # Pages Router
|
||||||
|
- ".next/ directory exists"
|
||||||
|
|
||||||
|
configuration:
|
||||||
|
base_url: "http://localhost:3000"
|
||||||
|
dev_server_command: "npm run dev"
|
||||||
|
build_command: "npm run build"
|
||||||
|
|
||||||
|
playwright_config:
|
||||||
|
timeout: 45000 # Next.js can be slower on first load
|
||||||
|
retries: 2
|
||||||
|
|
||||||
|
webServer:
|
||||||
|
command: "npm run dev"
|
||||||
|
url: "http://localhost:3000"
|
||||||
|
reuseExistingServer: true
|
||||||
|
timeout: 120000
|
||||||
|
|
||||||
|
test_generation:
|
||||||
|
router_type: "app" # or "pages" - detect from directory structure
|
||||||
|
entry_point: "app/page.tsx" # App Router
|
||||||
|
api_routes: "app/api/" # Check if API routes exist
|
||||||
|
common_pages:
|
||||||
|
- "/"
|
||||||
|
- "/about"
|
||||||
|
- "/blog"
|
||||||
|
- "/contact"
|
||||||
|
|
||||||
|
# ========== CREATE REACT APP ==========
|
||||||
|
create_react_app:
|
||||||
|
name: "Create React App"
|
||||||
|
priority: 3
|
||||||
|
|
||||||
|
detection:
|
||||||
|
package_json_dependencies:
|
||||||
|
required:
|
||||||
|
- "react"
|
||||||
|
- "react-scripts"
|
||||||
|
optional:
|
||||||
|
- "react-dom"
|
||||||
|
|
||||||
|
config_files:
|
||||||
|
- "public/index.html"
|
||||||
|
|
||||||
|
indicators:
|
||||||
|
- "src/index.js exists"
|
||||||
|
- "src/index.tsx exists"
|
||||||
|
- "package.json scripts.start includes react-scripts"
|
||||||
|
|
||||||
|
configuration:
|
||||||
|
base_url: "http://localhost:3000"
|
||||||
|
dev_server_command: "npm start"
|
||||||
|
build_command: "npm run build"
|
||||||
|
|
||||||
|
playwright_config:
|
||||||
|
timeout: 30000
|
||||||
|
retries: 2
|
||||||
|
|
||||||
|
webServer:
|
||||||
|
command: "npm start"
|
||||||
|
url: "http://localhost:3000"
|
||||||
|
reuseExistingServer: true
|
||||||
|
timeout: 120000
|
||||||
|
|
||||||
|
test_generation:
|
||||||
|
entry_point: "src/App.js"
|
||||||
|
routing: "react-router-dom"
|
||||||
|
|
||||||
|
# ========== NODE.JS + EXPRESS ==========
|
||||||
|
express:
|
||||||
|
name: "Node.js + Express"
|
||||||
|
priority: 4
|
||||||
|
|
||||||
|
detection:
|
||||||
|
package_json_dependencies:
|
||||||
|
required:
|
||||||
|
- "express"
|
||||||
|
optional:
|
||||||
|
- "ejs"
|
||||||
|
- "pug"
|
||||||
|
- "handlebars"
|
||||||
|
|
||||||
|
indicators:
|
||||||
|
- "app.js exists"
|
||||||
|
- "server.js exists"
|
||||||
|
- "index.js exists"
|
||||||
|
- "views/ directory exists"
|
||||||
|
|
||||||
|
configuration:
|
||||||
|
base_url: "http://localhost:3000" # Default, check process.env.PORT
|
||||||
|
dev_server_command: "npm run dev"
|
||||||
|
alternative_commands:
|
||||||
|
- "npm start"
|
||||||
|
- "node server.js"
|
||||||
|
- "nodemon server.js"
|
||||||
|
|
||||||
|
playwright_config:
|
||||||
|
timeout: 30000
|
||||||
|
retries: 2
|
||||||
|
|
||||||
|
webServer:
|
||||||
|
command: "npm run dev"
|
||||||
|
url: "http://localhost:3000"
|
||||||
|
reuseExistingServer: true
|
||||||
|
|
||||||
|
test_generation:
|
||||||
|
entry_point: "server.js"
|
||||||
|
template_engine: "ejs" # Detect from dependencies
|
||||||
|
api_endpoints: true # Generate API tests
|
||||||
|
common_routes:
|
||||||
|
- "/"
|
||||||
|
- "/api/health"
|
||||||
|
- "/api/users"
|
||||||
|
|
||||||
|
# ========== STATIC HTML/CSS/JS ==========
|
||||||
|
static:
|
||||||
|
name: "Static HTML/CSS/JS"
|
||||||
|
priority: 10 # Check last (fallback)
|
||||||
|
|
||||||
|
detection:
|
||||||
|
indicators:
|
||||||
|
- "index.html exists in root"
|
||||||
|
- "No package.json"
|
||||||
|
- "No build tools detected"
|
||||||
|
|
||||||
|
configuration:
|
||||||
|
base_url: "http://localhost:8080"
|
||||||
|
dev_server_command: "npx serve ."
|
||||||
|
install_dev_server: "npm install -g serve" # Install if needed
|
||||||
|
|
||||||
|
playwright_config:
|
||||||
|
timeout: 15000 # Faster, no build step
|
||||||
|
retries: 1
|
||||||
|
|
||||||
|
webServer:
|
||||||
|
command: "npx serve . -l 8080"
|
||||||
|
url: "http://localhost:8080"
|
||||||
|
reuseExistingServer: true
|
||||||
|
|
||||||
|
test_generation:
|
||||||
|
entry_point: "index.html"
|
||||||
|
detect_pages_from:
|
||||||
|
- "HTML files in root"
|
||||||
|
- "Links in index.html"
|
||||||
|
|
||||||
|
# ========== ASTRO ==========
|
||||||
|
astro:
|
||||||
|
name: "Astro"
|
||||||
|
priority: 5
|
||||||
|
|
||||||
|
detection:
|
||||||
|
package_json_dependencies:
|
||||||
|
required:
|
||||||
|
- "astro"
|
||||||
|
|
||||||
|
config_files:
|
||||||
|
- "astro.config.mjs"
|
||||||
|
|
||||||
|
indicators:
|
||||||
|
- "src/pages/ directory exists"
|
||||||
|
|
||||||
|
configuration:
|
||||||
|
base_url: "http://localhost:4321"
|
||||||
|
dev_server_command: "npm run dev"
|
||||||
|
|
||||||
|
playwright_config:
|
||||||
|
webServer:
|
||||||
|
command: "npm run dev"
|
||||||
|
url: "http://localhost:4321"
|
||||||
|
reuseExistingServer: true
|
||||||
|
|
||||||
|
# ========== SVELTE + VITE ==========
|
||||||
|
svelte_vite:
|
||||||
|
name: "Svelte + Vite"
|
||||||
|
priority: 6
|
||||||
|
|
||||||
|
detection:
|
||||||
|
package_json_dependencies:
|
||||||
|
required:
|
||||||
|
- "svelte"
|
||||||
|
- "vite"
|
||||||
|
optional:
|
||||||
|
- "@sveltejs/vite-plugin-svelte"
|
||||||
|
|
||||||
|
config_files:
|
||||||
|
- "svelte.config.js"
|
||||||
|
- "vite.config.js"
|
||||||
|
|
||||||
|
configuration:
|
||||||
|
base_url: "http://localhost:5173"
|
||||||
|
dev_server_command: "npm run dev"
|
||||||
|
|
||||||
|
# ========== VUE + VITE ==========
|
||||||
|
vue_vite:
|
||||||
|
name: "Vue + Vite"
|
||||||
|
priority: 7
|
||||||
|
|
||||||
|
detection:
|
||||||
|
package_json_dependencies:
|
||||||
|
required:
|
||||||
|
- "vue"
|
||||||
|
- "vite"
|
||||||
|
optional:
|
||||||
|
- "@vitejs/plugin-vue"
|
||||||
|
|
||||||
|
config_files:
|
||||||
|
- "vite.config.ts"
|
||||||
|
- "vite.config.js"
|
||||||
|
|
||||||
|
configuration:
|
||||||
|
base_url: "http://localhost:5173"
|
||||||
|
dev_server_command: "npm run dev"
|
||||||
|
|
||||||
|
# ========== NUXT ==========
|
||||||
|
nuxt:
|
||||||
|
name: "Nuxt"
|
||||||
|
priority: 8
|
||||||
|
|
||||||
|
detection:
|
||||||
|
package_json_dependencies:
|
||||||
|
required:
|
||||||
|
- "nuxt"
|
||||||
|
|
||||||
|
config_files:
|
||||||
|
- "nuxt.config.ts"
|
||||||
|
- "nuxt.config.js"
|
||||||
|
|
||||||
|
configuration:
|
||||||
|
base_url: "http://localhost:3000"
|
||||||
|
dev_server_command: "npm run dev"
|
||||||
|
|
||||||
|
# ========== DETECTION WORKFLOW ==========
|
||||||
|
detection_workflow:
|
||||||
|
steps:
|
||||||
|
- name: "Check for package.json"
|
||||||
|
action: "Read package.json if exists"
|
||||||
|
output: "dependencies and devDependencies lists"
|
||||||
|
|
||||||
|
- name: "Match framework patterns"
|
||||||
|
action: "Compare dependencies against framework patterns"
|
||||||
|
priority: "Use priority field to determine order"
|
||||||
|
output: "List of matching frameworks (highest priority first)"
|
||||||
|
|
||||||
|
- name: "Verify with config files"
|
||||||
|
action: "Check if expected config files exist"
|
||||||
|
output: "Confirm framework match"
|
||||||
|
|
||||||
|
- name: "Check additional indicators"
|
||||||
|
action: "Verify directory structure and entry points"
|
||||||
|
output: "Final framework identification"
|
||||||
|
|
||||||
|
- name: "Determine dev server"
|
||||||
|
action: "Read package.json scripts section"
|
||||||
|
fallback: "Use framework's default dev_server_command"
|
||||||
|
output: "Command to start dev server"
|
||||||
|
|
||||||
|
- name: "Detect port"
|
||||||
|
action: "Check if server is already running"
|
||||||
|
fallback: "Use framework's default base_url port"
|
||||||
|
output: "Base URL for tests"
|
||||||
|
|
||||||
|
# ========== PORT DETECTION ==========
|
||||||
|
port_detection:
|
||||||
|
methods:
|
||||||
|
- name: "Check running processes"
|
||||||
|
command: "lsof -i :3000 -i :5173 -i :8080 -i :4321"
|
||||||
|
description: "See if common ports are in use"
|
||||||
|
|
||||||
|
- name: "Check package.json scripts"
|
||||||
|
pattern: "--port (\\d+)"
|
||||||
|
description: "Extract port from dev script"
|
||||||
|
|
||||||
|
- name: "Check config files"
|
||||||
|
files:
|
||||||
|
- "vite.config.ts": "server.port"
|
||||||
|
- "next.config.js": "devServer.port"
|
||||||
|
- ".env": "PORT"
|
||||||
|
|
||||||
|
- name: "Attempt connection"
|
||||||
|
ports: [3000, 5173, 8080, 4321, 8000, 4200]
|
||||||
|
description: "Try common ports in order"
|
||||||
|
|
||||||
|
# ========== MULTI-APP DETECTION ==========
|
||||||
|
fullstack_detection:
|
||||||
|
patterns:
|
||||||
|
monorepo:
|
||||||
|
indicators:
|
||||||
|
- "package.json with workspaces field"
|
||||||
|
- "packages/ or apps/ directory"
|
||||||
|
- "Lerna or Nx configuration"
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
- "Detect each package separately"
|
||||||
|
- "Generate tests for each app"
|
||||||
|
- "Configure multiple web servers if needed"
|
||||||
|
|
||||||
|
separate_frontend_backend:
|
||||||
|
indicators:
|
||||||
|
- "client/ and server/ directories"
|
||||||
|
- "frontend/ and backend/ directories"
|
||||||
|
- "Multiple package.json files"
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
- "Detect frontend framework"
|
||||||
|
- "Detect backend framework"
|
||||||
|
- "Start both servers for e2e tests"
|
||||||
|
- "Configure API proxy if needed"
|
||||||
|
|
||||||
|
# ========== FALLBACK STRATEGY ==========
|
||||||
|
fallback:
|
||||||
|
when_no_match:
|
||||||
|
- "Prompt user to specify framework manually"
|
||||||
|
- "List detected files and ask for clarification"
|
||||||
|
- "Suggest generic static server approach"
|
||||||
|
|
||||||
|
generic_config:
|
||||||
|
base_url: "http://localhost:8080"
|
||||||
|
dev_server: "npx serve . -l 8080"
|
||||||
|
timeout: 30000
|
||||||
|
|
||||||
|
# ========== VALIDATION ==========
|
||||||
|
validation:
|
||||||
|
checks:
|
||||||
|
- name: "Server starts successfully"
|
||||||
|
test: "Run dev server command, wait for port to open"
|
||||||
|
timeout: 120000
|
||||||
|
failure: "Ask user to start server manually"
|
||||||
|
|
||||||
|
- name: "Base URL accessible"
|
||||||
|
test: "HTTP GET to base_url returns 200"
|
||||||
|
timeout: 30000
|
||||||
|
failure: "Check if port is different, ask user"
|
||||||
|
|
||||||
|
- name: "Page renders content"
|
||||||
|
test: "Page has visible content (not blank)"
|
||||||
|
timeout: 10000
|
||||||
|
failure: "Possible SPA routing issue, check entry point"
|
||||||
303
skills/playwright-e2e-automation/data/framework-versions.yaml
Normal file
303
skills/playwright-e2e-automation/data/framework-versions.yaml
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# Framework Version Compatibility Database
|
||||||
|
# Used for version-aware template selection and configuration generation
|
||||||
|
|
||||||
|
# This file maps framework versions to their syntax, configuration requirements,
|
||||||
|
# and breaking changes. When the skill detects installed package versions, it
|
||||||
|
# consults this database to select appropriate templates and warn about issues.
|
||||||
|
|
||||||
|
# ========== TAILWIND CSS ==========
|
||||||
|
tailwindcss:
|
||||||
|
v3:
|
||||||
|
version_range: ">=3.0.0 <4.0.0"
|
||||||
|
detection_priority: 2
|
||||||
|
|
||||||
|
syntax:
|
||||||
|
css_directives:
|
||||||
|
- "@tailwind base;"
|
||||||
|
- "@tailwind components;"
|
||||||
|
- "@tailwind utilities;"
|
||||||
|
postcss_plugin: "tailwindcss"
|
||||||
|
config_file: "tailwind.config.js"
|
||||||
|
|
||||||
|
templates:
|
||||||
|
css: "templates/css/tailwind-v3.css"
|
||||||
|
postcss_config: "templates/configs/postcss-tailwind-v3.js"
|
||||||
|
tailwind_config: "templates/configs/tailwind-v3.config.js"
|
||||||
|
|
||||||
|
notes: "Stable version with @tailwind directive syntax"
|
||||||
|
|
||||||
|
v4:
|
||||||
|
version_range: ">=4.0.0"
|
||||||
|
detection_priority: 1 # Check first (latest)
|
||||||
|
|
||||||
|
syntax:
|
||||||
|
css_directives:
|
||||||
|
- "@import \"tailwindcss\";"
|
||||||
|
postcss_plugin: "@tailwindcss/postcss"
|
||||||
|
config_file: "tailwind.config.ts" # Now TypeScript by default
|
||||||
|
|
||||||
|
templates:
|
||||||
|
css: "templates/css/tailwind-v4.css"
|
||||||
|
postcss_config: "templates/configs/postcss-tailwind-v4.js"
|
||||||
|
tailwind_config: "templates/configs/tailwind-v4.config.ts"
|
||||||
|
|
||||||
|
breaking_changes:
|
||||||
|
- "CSS syntax changed from @tailwind directives to @import"
|
||||||
|
- "PostCSS plugin renamed from 'tailwindcss' to '@tailwindcss/postcss'"
|
||||||
|
- "Configuration file now TypeScript by default"
|
||||||
|
- "Some utility classes restructured (check migration guide)"
|
||||||
|
|
||||||
|
migration_guide: "https://tailwindcss.com/docs/upgrade-guide"
|
||||||
|
|
||||||
|
notes: "Major rewrite with new @import syntax and improved PostCSS integration"
|
||||||
|
|
||||||
|
# ========== REACT ==========
|
||||||
|
react:
|
||||||
|
v17:
|
||||||
|
version_range: ">=17.0.0 <18.0.0"
|
||||||
|
detection_priority: 2
|
||||||
|
|
||||||
|
features:
|
||||||
|
jsx_transform: "classic" # Requires React import
|
||||||
|
concurrent_features: false
|
||||||
|
automatic_batching: false
|
||||||
|
|
||||||
|
templates:
|
||||||
|
component: "templates/react/component-v17.tsx"
|
||||||
|
|
||||||
|
notes: "Classic JSX transform, requires 'import React from react'"
|
||||||
|
|
||||||
|
v18:
|
||||||
|
version_range: ">=18.0.0 <19.0.0"
|
||||||
|
detection_priority: 1
|
||||||
|
|
||||||
|
features:
|
||||||
|
jsx_transform: "automatic" # No React import needed
|
||||||
|
concurrent_features: true
|
||||||
|
automatic_batching: true
|
||||||
|
use_client_directive: false # Not yet (that's React 19)
|
||||||
|
|
||||||
|
templates:
|
||||||
|
component: "templates/react/component-v18.tsx"
|
||||||
|
|
||||||
|
breaking_changes:
|
||||||
|
- "Automatic batching may affect state update timing"
|
||||||
|
- "Concurrent features require opt-in (via createRoot)"
|
||||||
|
- "IE11 no longer supported"
|
||||||
|
|
||||||
|
migration_guide: "https://react.dev/blog/2022/03/08/react-18-upgrade-guide"
|
||||||
|
|
||||||
|
notes: "New JSX transform, concurrent features, automatic batching"
|
||||||
|
|
||||||
|
v19:
|
||||||
|
version_range: ">=19.0.0"
|
||||||
|
detection_priority: 1
|
||||||
|
|
||||||
|
features:
|
||||||
|
jsx_transform: "automatic"
|
||||||
|
concurrent_features: true
|
||||||
|
automatic_batching: true
|
||||||
|
use_client_directive: true # Server Components
|
||||||
|
actions: true # Server Actions
|
||||||
|
|
||||||
|
templates:
|
||||||
|
component: "templates/react/component-v19.tsx"
|
||||||
|
|
||||||
|
breaking_changes:
|
||||||
|
- "'use client' directive required for client components in RSC apps"
|
||||||
|
- "ref is now a regular prop (no forwardRef needed)"
|
||||||
|
- "Context.Provider shorthand removed (use <Context> directly)"
|
||||||
|
|
||||||
|
migration_guide: "https://react.dev/blog/2024/04/25/react-19-upgrade-guide"
|
||||||
|
|
||||||
|
notes: "Server Components, Actions, ref as prop, improved performance"
|
||||||
|
|
||||||
|
# ========== NEXT.JS ==========
|
||||||
|
nextjs:
|
||||||
|
v13:
|
||||||
|
version_range: ">=13.0.0 <14.0.0"
|
||||||
|
detection_priority: 2
|
||||||
|
|
||||||
|
features:
|
||||||
|
app_router: true # Optional
|
||||||
|
pages_router: true
|
||||||
|
turbopack: "beta"
|
||||||
|
server_actions: "alpha"
|
||||||
|
|
||||||
|
router_detection:
|
||||||
|
app_router: "app/ directory exists"
|
||||||
|
pages_router: "pages/ directory exists"
|
||||||
|
|
||||||
|
templates:
|
||||||
|
config: "templates/nextjs/next-v13.config.js"
|
||||||
|
page_app: "templates/nextjs/page-v13-app.tsx"
|
||||||
|
page_pages: "templates/nextjs/page-v13-pages.tsx"
|
||||||
|
|
||||||
|
notes: "App Router introduced alongside Pages Router"
|
||||||
|
|
||||||
|
v14:
|
||||||
|
version_range: ">=14.0.0 <15.0.0"
|
||||||
|
detection_priority: 1
|
||||||
|
|
||||||
|
features:
|
||||||
|
app_router: true
|
||||||
|
pages_router: true
|
||||||
|
turbopack: "stable"
|
||||||
|
server_actions: "stable"
|
||||||
|
partial_prerendering: "experimental"
|
||||||
|
|
||||||
|
router_detection:
|
||||||
|
app_router: "app/ directory exists"
|
||||||
|
pages_router: "pages/ directory exists"
|
||||||
|
|
||||||
|
templates:
|
||||||
|
config: "templates/nextjs/next-v14.config.js"
|
||||||
|
page_app: "templates/nextjs/page-v14-app.tsx"
|
||||||
|
page_pages: "templates/nextjs/page-v14-pages.tsx"
|
||||||
|
|
||||||
|
breaking_changes:
|
||||||
|
- "Server Actions stable (syntax changes from v13)"
|
||||||
|
- "Turbopack stable for dev (replaces webpack in dev mode)"
|
||||||
|
- "Minimum Node.js version: 18.17"
|
||||||
|
|
||||||
|
migration_guide: "https://nextjs.org/docs/app/building-your-application/upgrading/version-14"
|
||||||
|
|
||||||
|
notes: "Stable Server Actions and Turbopack"
|
||||||
|
|
||||||
|
# ========== VITE ==========
|
||||||
|
vite:
|
||||||
|
v4:
|
||||||
|
version_range: ">=4.0.0 <5.0.0"
|
||||||
|
detection_priority: 2
|
||||||
|
|
||||||
|
features:
|
||||||
|
default_port: 5173
|
||||||
|
rollup_version: 3
|
||||||
|
css_code_split: true
|
||||||
|
|
||||||
|
templates:
|
||||||
|
config: "templates/vite/vite-v4.config.ts"
|
||||||
|
|
||||||
|
notes: "Stable Vite 4 with Rollup 3"
|
||||||
|
|
||||||
|
v5:
|
||||||
|
version_range: ">=5.0.0 <6.0.0"
|
||||||
|
detection_priority: 1
|
||||||
|
|
||||||
|
features:
|
||||||
|
default_port: 5173
|
||||||
|
rollup_version: 4
|
||||||
|
css_code_split: true
|
||||||
|
improved_hmr: true
|
||||||
|
|
||||||
|
templates:
|
||||||
|
config: "templates/vite/vite-v5.config.ts"
|
||||||
|
|
||||||
|
breaking_changes:
|
||||||
|
- "Rollup 4 (plugin compatibility check needed)"
|
||||||
|
- "Minimum Node.js version: 18.0"
|
||||||
|
- "Some deprecated options removed"
|
||||||
|
|
||||||
|
migration_guide: "https://vitejs.dev/guide/migration"
|
||||||
|
|
||||||
|
notes: "Vite 5 with Rollup 4 and improved HMR"
|
||||||
|
|
||||||
|
# ========== PLAYWRIGHT ==========
|
||||||
|
playwright:
|
||||||
|
v1_40_plus:
|
||||||
|
version_range: ">=1.40.0"
|
||||||
|
detection_priority: 1
|
||||||
|
|
||||||
|
features:
|
||||||
|
component_testing: true
|
||||||
|
trace_viewer: true
|
||||||
|
codegen: true
|
||||||
|
auto_waiting: true
|
||||||
|
|
||||||
|
templates:
|
||||||
|
config: "templates/playwright.config.template.ts"
|
||||||
|
test_spec: "templates/test-spec.template.ts"
|
||||||
|
page_object: "templates/page-object.template.ts"
|
||||||
|
|
||||||
|
notes: "Modern Playwright with full feature set"
|
||||||
|
|
||||||
|
# ========== POSTCSS ==========
|
||||||
|
postcss:
|
||||||
|
v8:
|
||||||
|
version_range: ">=8.0.0"
|
||||||
|
detection_priority: 1
|
||||||
|
|
||||||
|
notes: "Standard PostCSS v8 - most common version"
|
||||||
|
|
||||||
|
# ========== VERSION DETECTION STRATEGIES ==========
|
||||||
|
detection_strategies:
|
||||||
|
priority_order:
|
||||||
|
description: "Check versions in priority order (higher priority first)"
|
||||||
|
example: "For Tailwind: check v4 rules before v3 rules"
|
||||||
|
|
||||||
|
semver_matching:
|
||||||
|
description: "Use semver.satisfies() for version range matching"
|
||||||
|
library: "semver npm package"
|
||||||
|
|
||||||
|
fallback:
|
||||||
|
description: "If no version matches, warn and use sensible defaults"
|
||||||
|
default_strategy: "Use latest stable templates with warning"
|
||||||
|
|
||||||
|
multi_framework:
|
||||||
|
description: "Detect multiple frameworks simultaneously"
|
||||||
|
example: "React 18 + Vite 5 + Tailwind 4 + Playwright 1.40"
|
||||||
|
|
||||||
|
# ========== COMMON BREAKING CHANGE PATTERNS ==========
|
||||||
|
breaking_change_categories:
|
||||||
|
syntax_changes:
|
||||||
|
examples:
|
||||||
|
- "Tailwind v3→v4: @tailwind → @import"
|
||||||
|
- "React v17→v18: Optional JSX import"
|
||||||
|
impact: "Critical - app won't build/run"
|
||||||
|
detection: "Parse error, syntax error in build output"
|
||||||
|
|
||||||
|
configuration_changes:
|
||||||
|
examples:
|
||||||
|
- "Tailwind v4: PostCSS plugin rename"
|
||||||
|
- "Next.js v14: next.config.js options"
|
||||||
|
impact: "High - build fails or misconfigured"
|
||||||
|
detection: "Build error, plugin not found"
|
||||||
|
|
||||||
|
api_changes:
|
||||||
|
examples:
|
||||||
|
- "React v19: ref as prop"
|
||||||
|
- "Next.js v13: getServerSideProps in App Router"
|
||||||
|
impact: "Medium - runtime errors or warnings"
|
||||||
|
detection: "TypeScript errors, runtime warnings"
|
||||||
|
|
||||||
|
deprecation_warnings:
|
||||||
|
examples:
|
||||||
|
- "React v18: ReactDOM.render → createRoot"
|
||||||
|
- "Vite: Legacy options removed"
|
||||||
|
impact: "Low - works but will break in future"
|
||||||
|
detection: "Console warnings, deprecation notices"
|
||||||
|
|
||||||
|
# ========== USAGE NOTES ==========
|
||||||
|
usage:
|
||||||
|
detection_flow:
|
||||||
|
- "Read package.json dependencies and devDependencies"
|
||||||
|
- "For each framework, iterate through versions in priority order"
|
||||||
|
- "Use semver.satisfies(installedVersion, versionRange)"
|
||||||
|
- "First match wins (highest priority)"
|
||||||
|
- "If no match, use fallback and warn"
|
||||||
|
|
||||||
|
template_selection:
|
||||||
|
- "Based on detected version, select appropriate template files"
|
||||||
|
- "Templates use version-specific syntax and best practices"
|
||||||
|
- "Combine multiple framework templates (e.g., React + Tailwind + Vite)"
|
||||||
|
|
||||||
|
error_prevention:
|
||||||
|
- "Check for known breaking changes before generating config"
|
||||||
|
- "Warn user if mixing incompatible versions"
|
||||||
|
- "Provide migration guides for major version differences"
|
||||||
|
|
||||||
|
maintenance:
|
||||||
|
- "Add new versions as they're released"
|
||||||
|
- "Update breaking_changes based on real-world issues"
|
||||||
|
- "Keep migration_guide links current"
|
||||||
|
- "Test template compatibility regularly"
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
# Playwright Best Practices
|
||||||
|
|
||||||
|
Official best practices for Playwright test automation, optimized for LLM-assisted development.
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
### Use Page Object Models (POM)
|
||||||
|
|
||||||
|
**Why**: Separates page structure from test logic, improves maintainability
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good: Page Object Model
|
||||||
|
// pages/login.page.ts
|
||||||
|
export class LoginPage {
|
||||||
|
constructor(private page: Page) {}
|
||||||
|
|
||||||
|
async goto() {
|
||||||
|
await this.page.goto('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(username: string, password: string) {
|
||||||
|
await this.page.getByLabel('Username').fill(username);
|
||||||
|
await this.page.getByLabel('Password').fill(password);
|
||||||
|
await this.page.getByRole('button', { name: 'Sign in' }).click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// specs/login.spec.ts
|
||||||
|
test('user can login', async ({ page }) => {
|
||||||
|
const loginPage = new LoginPage(page);
|
||||||
|
await loginPage.goto();
|
||||||
|
await loginPage.login('user@example.com', 'password123');
|
||||||
|
await expect(page).toHaveURL('/dashboard');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Semantic Selectors
|
||||||
|
|
||||||
|
**Priority order** (most stable → least stable):
|
||||||
|
|
||||||
|
1. **getByRole** - Accessible role (button, heading, textbox, etc.)
|
||||||
|
2. **getByLabel** - Form inputs with associated labels
|
||||||
|
3. **getByPlaceholder** - Input placeholder text
|
||||||
|
4. **getByText** - User-visible text content
|
||||||
|
5. **getByTestId** - data-testid attributes (last resort)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Best: Role-based (accessible and stable)
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
// Good: Label-based (for forms)
|
||||||
|
await page.getByLabel('Email address').fill('user@example.com');
|
||||||
|
|
||||||
|
// Acceptable: Text-based
|
||||||
|
await page.getByText('Continue to checkout').click();
|
||||||
|
|
||||||
|
// Avoid: CSS selectors (brittle)
|
||||||
|
await page.click('.btn-primary'); // ❌ Breaks if class changes
|
||||||
|
|
||||||
|
// Last resort: Test IDs (when semantic selectors don't work)
|
||||||
|
await page.getByTestId('checkout-button').click();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screenshot Best Practices
|
||||||
|
|
||||||
|
### When to Capture Screenshots
|
||||||
|
|
||||||
|
1. **Initial page load** - Baseline visual state
|
||||||
|
2. **Before interaction** - Pre-state for comparison
|
||||||
|
3. **After interaction** - Result of user action
|
||||||
|
4. **Error states** - When validation fails or errors occur
|
||||||
|
5. **Success states** - Confirmation screens, success messages
|
||||||
|
6. **Test failures** - Automatic capture for debugging
|
||||||
|
|
||||||
|
### Screenshot Naming Convention
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Pattern: {test-name}-{viewport}-{state}-{timestamp}.png
|
||||||
|
|
||||||
|
await page.screenshot({
|
||||||
|
path: `screenshots/current/login-desktop-initial-${Date.now()}.png`,
|
||||||
|
fullPage: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.screenshot({
|
||||||
|
path: `screenshots/current/checkout-mobile-error-${Date.now()}.png`,
|
||||||
|
fullPage: true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full-Page vs Element Screenshots
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Full-page: For layout and overall UI analysis
|
||||||
|
await page.screenshot({
|
||||||
|
path: 'homepage-full.png',
|
||||||
|
fullPage: true // Captures entire scrollable page
|
||||||
|
});
|
||||||
|
|
||||||
|
// Element-specific: For component testing
|
||||||
|
const button = page.getByRole('button', { name: 'Submit' });
|
||||||
|
await button.screenshot({
|
||||||
|
path: 'submit-button.png'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Waiting and Timing
|
||||||
|
|
||||||
|
### Auto-Waiting
|
||||||
|
|
||||||
|
Playwright automatically waits for:
|
||||||
|
- Element to be attached to DOM
|
||||||
|
- Element to be visible
|
||||||
|
- Element to be stable (not animating)
|
||||||
|
- Element to receive events (not obscured)
|
||||||
|
- Element to be enabled
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// This automatically waits for button to be clickable
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Explicit Waits (when needed)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Wait for navigation
|
||||||
|
await page.waitForURL('/dashboard');
|
||||||
|
|
||||||
|
// Wait for network idle (good before screenshots)
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Wait for specific element
|
||||||
|
await page.waitForSelector('img[alt="Profile picture"]');
|
||||||
|
|
||||||
|
// Wait for custom condition
|
||||||
|
await page.waitForFunction(() => window.scrollY === 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Avoid Fixed Timeouts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad: Arbitrary delays
|
||||||
|
await page.waitForTimeout(3000); // ❌ Flaky, slow
|
||||||
|
|
||||||
|
// Good: Wait for specific condition
|
||||||
|
await expect(page.getByText('Success')).toBeVisible(); // ✅ Fast and reliable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Isolation
|
||||||
|
|
||||||
|
### Independent Tests
|
||||||
|
|
||||||
|
Each test should be completely independent:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good: Test is self-contained
|
||||||
|
test('user can add item to cart', async ({ page }) => {
|
||||||
|
// Set up: Create user, log in
|
||||||
|
await page.goto('/');
|
||||||
|
await login(page, 'user@example.com', 'password');
|
||||||
|
|
||||||
|
// Action: Add to cart
|
||||||
|
await page.getByRole('button', { name: 'Add to cart' }).click();
|
||||||
|
|
||||||
|
// Assert: Item in cart
|
||||||
|
await expect(page.getByTestId('cart-count')).toHaveText('1');
|
||||||
|
|
||||||
|
// Cleanup happens automatically with new page context
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bad: Depends on previous test state
|
||||||
|
test('user can checkout', async ({ page }) => {
|
||||||
|
// ❌ Assumes cart already has items from previous test
|
||||||
|
await page.goto('/checkout');
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use test.beforeEach for Common Setup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
test.describe('Shopping cart', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await login(page, 'user@example.com', 'password');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can add item to cart', async ({ page }) => {
|
||||||
|
// Setup already done
|
||||||
|
await page.getByRole('button', { name: 'Add to cart' }).click();
|
||||||
|
await expect(page.getByTestId('cart-count')).toHaveText('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can remove item from cart', async ({ page }) => {
|
||||||
|
// Setup already done, fresh state
|
||||||
|
await page.getByRole('button', { name: 'Add to cart' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Remove' }).click();
|
||||||
|
await expect(page.getByTestId('cart-count')).toHaveText('0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Visual Regression Testing
|
||||||
|
|
||||||
|
### Snapshot Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic snapshot
|
||||||
|
await expect(page).toHaveScreenshot('homepage.png');
|
||||||
|
|
||||||
|
// With threshold (allow minor differences)
|
||||||
|
await expect(page).toHaveScreenshot('homepage.png', {
|
||||||
|
maxDiffPixelRatio: 0.05 // Allow 5% difference
|
||||||
|
});
|
||||||
|
|
||||||
|
// Element snapshot
|
||||||
|
const card = page.getByRole('article').first();
|
||||||
|
await expect(card).toHaveScreenshot('product-card.png');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating Baselines
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update all snapshots
|
||||||
|
npx playwright test --update-snapshots
|
||||||
|
|
||||||
|
# Update specific test
|
||||||
|
npx playwright test login.spec.ts --update-snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
### Baseline Management
|
||||||
|
|
||||||
|
- **Store baselines in git** - Commit to repository for consistency
|
||||||
|
- **Review diffs carefully** - Not all changes are bugs
|
||||||
|
- **Update deliberately** - Only update when changes are intentional
|
||||||
|
- **Use CI checks** - Fail pipeline on unexpected visual changes
|
||||||
|
|
||||||
|
## Configuration Best Practices
|
||||||
|
|
||||||
|
### playwright.config.ts Essentials
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
|
||||||
|
// Timeout for each test
|
||||||
|
timeout: 30 * 1000,
|
||||||
|
|
||||||
|
// Global setup/teardown
|
||||||
|
globalSetup: require.resolve('./tests/setup/global-setup.ts'),
|
||||||
|
|
||||||
|
// Fail fast on CI, retry locally
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
|
||||||
|
// Parallel execution
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
|
||||||
|
// Reporter
|
||||||
|
reporter: process.env.CI ? 'github' : 'html',
|
||||||
|
|
||||||
|
use: {
|
||||||
|
// Base URL
|
||||||
|
baseURL: 'http://localhost:5173',
|
||||||
|
|
||||||
|
// Screenshot on failure
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
|
||||||
|
// Trace on first retry
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
|
||||||
|
// Video on failure
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Projects for multi-browser testing
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mobile-chrome',
|
||||||
|
use: { ...devices['Pixel 5'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mobile-safari',
|
||||||
|
use: { ...devices['iPhone 13'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Web server for dev
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev',
|
||||||
|
url: 'http://localhost:5173',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
### Playwright Inspector
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debug specific test
|
||||||
|
npx playwright test --debug login.spec.ts
|
||||||
|
|
||||||
|
# Debug from specific line
|
||||||
|
npx playwright test --debug --grep "user can login"
|
||||||
|
```
|
||||||
|
|
||||||
|
### VS Code Debugger
|
||||||
|
|
||||||
|
```json
|
||||||
|
// .vscode/launch.json
|
||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug Playwright Tests",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/node_modules/@playwright/test/cli.js",
|
||||||
|
"args": ["test", "--headed", "${file}"],
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trace Viewer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with trace
|
||||||
|
npx playwright test --trace on
|
||||||
|
|
||||||
|
# View trace
|
||||||
|
npx playwright show-trace trace.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Parallel Execution
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Run tests in parallel (default)
|
||||||
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
|
// Run tests serially (when needed)
|
||||||
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reuse Authentication State
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// global-setup.ts
|
||||||
|
import { chromium } from '@playwright/test';
|
||||||
|
|
||||||
|
export default async function globalSetup() {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
await page.goto('http://localhost:5173/login');
|
||||||
|
await page.getByLabel('Username').fill('admin');
|
||||||
|
await page.getByLabel('Password').fill('password');
|
||||||
|
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||||
|
|
||||||
|
// Save authentication state
|
||||||
|
await page.context().storageState({ path: 'auth.json' });
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use in tests
|
||||||
|
test.use({ storageState: 'auth.json' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Pitfalls to Avoid
|
||||||
|
|
||||||
|
### 1. Not Waiting for Network Idle Before Screenshots
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad: Screenshot may capture loading state
|
||||||
|
await page.goto('/dashboard');
|
||||||
|
await page.screenshot({ path: 'dashboard.png' });
|
||||||
|
|
||||||
|
// Good: Wait for content to load
|
||||||
|
await page.goto('/dashboard');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.screenshot({ path: 'dashboard.png' });
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Using Non-Stable Selectors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad: Position-based (breaks if order changes)
|
||||||
|
await page.locator('button').nth(2).click();
|
||||||
|
|
||||||
|
// Good: Content-based
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Not Handling Dynamic Content
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad: Assumes content is already loaded
|
||||||
|
const text = await page.getByTestId('user-name').textContent();
|
||||||
|
|
||||||
|
// Good: Wait for element first
|
||||||
|
await expect(page.getByTestId('user-name')).toBeVisible();
|
||||||
|
const text = await page.getByTestId('user-name').textContent();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Overly Broad Assertions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad: Fails on any minor change
|
||||||
|
await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0 });
|
||||||
|
|
||||||
|
// Good: Allow reasonable tolerance
|
||||||
|
await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0.02 });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary Checklist
|
||||||
|
|
||||||
|
- [ ] Use Page Object Models for test organization
|
||||||
|
- [ ] Prefer semantic selectors (getByRole, getByLabel)
|
||||||
|
- [ ] Capture screenshots at key interaction points
|
||||||
|
- [ ] Wait for network idle before screenshots
|
||||||
|
- [ ] Use auto-waiting instead of fixed timeouts
|
||||||
|
- [ ] Make tests independent and isolated
|
||||||
|
- [ ] Configure proper retry logic (2-3 retries in CI)
|
||||||
|
- [ ] Store authentication state for reuse
|
||||||
|
- [ ] Use trace viewer for debugging
|
||||||
|
- [ ] Review visual diffs before updating baselines
|
||||||
|
- [ ] Run tests in parallel for performance
|
||||||
|
- [ ] Enable screenshot/video on failure
|
||||||
|
- [ ] Store baselines in version control
|
||||||
|
- [ ] Use meaningful screenshot names with timestamps
|
||||||
|
- [ ] Configure appropriate visual diff thresholds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**References:**
|
||||||
|
- [Playwright Official Docs](https://playwright.dev/)
|
||||||
|
- [Best Practices Guide](https://playwright.dev/docs/best-practices)
|
||||||
|
- [Locators Guide](https://playwright.dev/docs/locators)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { Page, Locator } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HomePage Page Object Model
|
||||||
|
*
|
||||||
|
* Example POM for a React + Vite application homepage
|
||||||
|
* Demonstrates best practices for locator selection
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class HomePage {
|
||||||
|
readonly page: Page;
|
||||||
|
|
||||||
|
// Locators - Using semantic selectors (priority: getByRole > getByLabel > getByText > getByTestId)
|
||||||
|
readonly welcomeMessage: Locator;
|
||||||
|
readonly aboutLink: Locator;
|
||||||
|
readonly contactLink: Locator;
|
||||||
|
readonly navbar: Locator;
|
||||||
|
readonly heroSection: Locator;
|
||||||
|
readonly ctaButton: Locator;
|
||||||
|
readonly featureCards: Locator;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
|
||||||
|
// Initialize locators with semantic selectors
|
||||||
|
this.navbar = page.getByRole('navigation');
|
||||||
|
this.welcomeMessage = page.getByRole('heading', { name: /welcome/i });
|
||||||
|
this.aboutLink = page.getByRole('link', { name: /about/i });
|
||||||
|
this.contactLink = page.getByRole('link', { name: /contact/i });
|
||||||
|
this.heroSection = page.getByRole('banner');
|
||||||
|
this.ctaButton = page.getByRole('button', { name: /get started/i });
|
||||||
|
this.featureCards = page.getByRole('article');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to homepage
|
||||||
|
*/
|
||||||
|
async goto() {
|
||||||
|
await this.page.goto('/');
|
||||||
|
await this.page.waitForLoadState('networkidle');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for page to be fully loaded and ready
|
||||||
|
*/
|
||||||
|
async waitForReady() {
|
||||||
|
await this.welcomeMessage.waitFor({ state: 'visible' });
|
||||||
|
await this.navbar.waitFor({ state: 'visible' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to About page
|
||||||
|
*/
|
||||||
|
async goToAbout() {
|
||||||
|
await this.aboutLink.click();
|
||||||
|
await this.page.waitForURL('**/about');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to Contact page
|
||||||
|
*/
|
||||||
|
async goToContact() {
|
||||||
|
await this.contactLink.click();
|
||||||
|
await this.page.waitForURL('**/contact');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click the main CTA button
|
||||||
|
*/
|
||||||
|
async clickCTA() {
|
||||||
|
await this.ctaButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of feature cards
|
||||||
|
*/
|
||||||
|
async getFeatureCardCount(): Promise<number> {
|
||||||
|
return await this.featureCards.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take screenshot of homepage
|
||||||
|
*/
|
||||||
|
async screenshot(name: string = 'homepage') {
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
await this.page.screenshot({
|
||||||
|
path: `screenshots/current/${name}-${timestamp}.png`,
|
||||||
|
fullPage: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { HomePage } from '../pages/home.page';
|
||||||
|
import { captureWithContext } from '../utils/screenshot-helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example Playwright Test for React + Vite Application
|
||||||
|
*
|
||||||
|
* This demonstrates best practices for e2e testing with screenshot capture
|
||||||
|
*/
|
||||||
|
|
||||||
|
test.describe('Homepage', () => {
|
||||||
|
let homePage: HomePage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
homePage = new HomePage(page);
|
||||||
|
await homePage.goto();
|
||||||
|
|
||||||
|
// Capture initial page load
|
||||||
|
await captureWithContext(page, 'homepage-initial-load', 'Homepage loaded successfully');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display welcome message', async ({ page }) => {
|
||||||
|
// Arrange: Page is already loaded in beforeEach
|
||||||
|
|
||||||
|
// Act: No action needed, just checking initial state
|
||||||
|
await captureWithContext(page, 'homepage-welcome-check', 'Checking for welcome message');
|
||||||
|
|
||||||
|
// Assert: Welcome message is visible
|
||||||
|
await expect(homePage.welcomeMessage).toBeVisible();
|
||||||
|
await expect(homePage.welcomeMessage).toContainText('Welcome');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to about page when clicking About link', async ({ page }) => {
|
||||||
|
// Arrange: Page loaded
|
||||||
|
await captureWithContext(page, 'homepage-before-nav', 'Before clicking About link');
|
||||||
|
|
||||||
|
// Act: Click About link
|
||||||
|
await homePage.aboutLink.click();
|
||||||
|
|
||||||
|
// Capture after navigation
|
||||||
|
await page.waitForURL('**/about');
|
||||||
|
await captureWithContext(page, 'about-page-loaded', 'About page after navigation');
|
||||||
|
|
||||||
|
// Assert: URL changed and about page content visible
|
||||||
|
expect(page.url()).toContain('/about');
|
||||||
|
await expect(page.getByRole('heading', { name: 'About' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should submit contact form successfully', async ({ page }) => {
|
||||||
|
// Arrange: Navigate to contact page
|
||||||
|
await page.goto('/contact');
|
||||||
|
await captureWithContext(page, 'contact-form-initial', 'Contact form initial state');
|
||||||
|
|
||||||
|
// Act: Fill out form
|
||||||
|
await page.getByLabel('Name').fill('John Doe');
|
||||||
|
await page.getByLabel('Email').fill('john@example.com');
|
||||||
|
await page.getByLabel('Message').fill('This is a test message');
|
||||||
|
|
||||||
|
await captureWithContext(page, 'contact-form-filled', 'Form filled before submission');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Send Message' }).click();
|
||||||
|
|
||||||
|
// Wait for success message
|
||||||
|
await page.waitForSelector('[data-testid="success-message"]', { state: 'visible' });
|
||||||
|
|
||||||
|
await captureWithContext(page, 'contact-form-success', 'Success message displayed');
|
||||||
|
|
||||||
|
// Assert: Success message appears
|
||||||
|
await expect(page.getByTestId('success-message')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('success-message')).toContainText('Message sent successfully');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should validate required fields', async ({ page }) => {
|
||||||
|
// Arrange: Navigate to contact page
|
||||||
|
await page.goto('/contact');
|
||||||
|
await captureWithContext(page, 'contact-form-validation-init', 'Before validation check');
|
||||||
|
|
||||||
|
// Act: Try to submit empty form
|
||||||
|
await page.getByRole('button', { name: 'Send Message' }).click();
|
||||||
|
|
||||||
|
await captureWithContext(page, 'contact-form-validation-errors', 'Validation errors displayed');
|
||||||
|
|
||||||
|
// Assert: Error messages appear
|
||||||
|
await expect(page.getByText('Name is required')).toBeVisible();
|
||||||
|
await expect(page.getByText('Email is required')).toBeVisible();
|
||||||
|
await expect(page.getByText('Message is required')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not have accessibility violations', async ({ page }) => {
|
||||||
|
const AxeBuilder = (await import('@axe-core/playwright')).default;
|
||||||
|
|
||||||
|
const accessibilityScanResults = await new AxeBuilder({ page })
|
||||||
|
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
||||||
|
.analyze();
|
||||||
|
|
||||||
|
await captureWithContext(
|
||||||
|
page,
|
||||||
|
'homepage-accessibility-check',
|
||||||
|
`Found ${accessibilityScanResults.violations.length} accessibility violations`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log violations for review
|
||||||
|
if (accessibilityScanResults.violations.length > 0) {
|
||||||
|
console.log('\n⚠️ Accessibility Violations:');
|
||||||
|
accessibilityScanResults.violations.forEach((violation) => {
|
||||||
|
console.log(`\n- ${violation.id}: ${violation.description}`);
|
||||||
|
console.log(` Impact: ${violation.impact}`);
|
||||||
|
console.log(` Nodes: ${violation.nodes.length}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail on critical violations only (for this example)
|
||||||
|
const criticalViolations = accessibilityScanResults.violations.filter(
|
||||||
|
(v) => v.impact === 'critical' || v.impact === 'serious'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(criticalViolations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display correctly across viewports', async ({ page }) => {
|
||||||
|
const viewports = [
|
||||||
|
{ name: 'desktop', width: 1280, height: 720 },
|
||||||
|
{ name: 'tablet', width: 768, height: 1024 },
|
||||||
|
{ name: 'mobile', width: 375, height: 667 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const viewport of viewports) {
|
||||||
|
await page.setViewportSize(viewport);
|
||||||
|
await page.waitForTimeout(500); // Let responsive changes settle
|
||||||
|
|
||||||
|
await captureWithContext(
|
||||||
|
page,
|
||||||
|
`homepage-responsive-${viewport.name}`,
|
||||||
|
`${viewport.width}x${viewport.height} viewport`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify no horizontal scroll on mobile/tablet
|
||||||
|
if (viewport.name !== 'desktop') {
|
||||||
|
const scrollWidth = await page.evaluate(() => document.body.scrollWidth);
|
||||||
|
const clientWidth = await page.evaluate(() => document.body.clientWidth);
|
||||||
|
expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 1); // Allow 1px tolerance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify main navigation is accessible
|
||||||
|
const nav = page.getByRole('navigation');
|
||||||
|
await expect(nav).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,592 @@
|
|||||||
|
# Fix Recommendations
|
||||||
|
|
||||||
|
**Generated**: 2025-11-01 16:35:22
|
||||||
|
**Based On**: visual-analysis-report.md
|
||||||
|
**Issues Addressed**: 8
|
||||||
|
**Estimated Effort**: 4-6 hours
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Use This Report
|
||||||
|
|
||||||
|
Each fix includes:
|
||||||
|
- **File location** with line numbers (when identifiable)
|
||||||
|
- **Current code** showing the problematic implementation
|
||||||
|
- **Recommended fix** with specific code changes
|
||||||
|
- **Reasoning** explaining why this fix works
|
||||||
|
- **Testing steps** to validate the fix
|
||||||
|
|
||||||
|
Apply fixes in priority order: Critical → High → Medium → Low
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Fixes (Implement Immediately)
|
||||||
|
|
||||||
|
### Fix #1: Increase Form Label Contrast
|
||||||
|
|
||||||
|
**Issue**: Insufficient color contrast on form labels (2.6:1, requires 4.5:1)
|
||||||
|
|
||||||
|
**Location**: `src/components/ContactForm.tsx:45-52`
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```tsx
|
||||||
|
<label htmlFor="name" className="block text-gray-400 text-sm mb-1">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded"
|
||||||
|
placeholder="Enter your name"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Fix**:
|
||||||
|
```tsx
|
||||||
|
<label htmlFor="name" className="block text-gray-700 text-sm font-medium mb-1">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded"
|
||||||
|
placeholder="Enter your name"
|
||||||
|
aria-required="true"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Changes Made**:
|
||||||
|
- `text-gray-400` → `text-gray-700` (changes color from #AAAAAA to #374151)
|
||||||
|
- Added `font-medium` for improved readability
|
||||||
|
- Added `aria-required="true"` for accessibility
|
||||||
|
|
||||||
|
**Reasoning**:
|
||||||
|
- `text-gray-700` (#374151) on white (#FFFFFF) = 9.7:1 contrast ratio ✅
|
||||||
|
- Exceeds WCAG 2.1 AA requirement (4.5:1)
|
||||||
|
- `font-medium` improves readability without affecting contrast
|
||||||
|
- `aria-required` helps screen reader users identify required fields
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
1. Visual check: Labels should be clearly readable
|
||||||
|
2. Contrast tool: Verify 9.7:1 ratio at https://webaim.org/resources/contrastchecker/
|
||||||
|
3. Accessibility audit: Run axe-core, verify no contrast violations
|
||||||
|
4. Screen reader: Test with NVDA/VoiceOver, verify required field announcement
|
||||||
|
|
||||||
|
**Impact**: Fixes critical WCAG 2.1 violation, improves usability for low-vision users
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix #2: Responsive Button Text Sizing
|
||||||
|
|
||||||
|
**Issue**: Button text truncated on mobile (shows "Send Mes...")
|
||||||
|
|
||||||
|
**Location**: `src/components/ContactForm.tsx:78`
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```tsx
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full px-6 py-3 text-xl font-bold bg-blue-600 text-white rounded"
|
||||||
|
>
|
||||||
|
Send Message
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Fix**:
|
||||||
|
```tsx
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full px-4 py-2 text-sm sm:text-base md:text-lg font-bold bg-blue-600 text-white rounded whitespace-nowrap overflow-visible"
|
||||||
|
>
|
||||||
|
Send Message
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Changes Made**:
|
||||||
|
- `px-6` → `px-4` (reduced padding to allow more text space)
|
||||||
|
- `py-3` → `py-2` (slightly reduced vertical padding)
|
||||||
|
- `text-xl` → `text-sm sm:text-base md:text-lg` (responsive text sizing)
|
||||||
|
- Added `whitespace-nowrap` (prevent text wrapping)
|
||||||
|
- Added `overflow-visible` (ensure text isn't hidden)
|
||||||
|
|
||||||
|
**Reasoning**:
|
||||||
|
- Mobile (375px): 14px font (text-sm) fits comfortably
|
||||||
|
- Tablet (768px): 16px font (text-base) for better readability
|
||||||
|
- Desktop (1280px): 18px font (text-lg) for prominence
|
||||||
|
- Reduced padding provides more space for text
|
||||||
|
- `whitespace-nowrap` prevents awkward line breaks
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
1. Mobile (375px viewport): Verify full text "Send Message" visible
|
||||||
|
2. Tablet (768px): Check font size scales appropriately
|
||||||
|
3. Desktop (1280px): Ensure button looks proportional
|
||||||
|
4. Accessibility: Verify button is tappable (min 44x44px)
|
||||||
|
|
||||||
|
**Impact**: Fixes broken user experience on mobile, ensures button purpose is clear
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## High Priority Fixes
|
||||||
|
|
||||||
|
### Fix #3: Prevent Navigation Overlap on Tablet
|
||||||
|
|
||||||
|
**Issue**: Nav items overlap on tablet breakpoint (768px)
|
||||||
|
|
||||||
|
**Location**: `src/components/Header.tsx:32-45`
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```tsx
|
||||||
|
<nav className="flex space-x-6">
|
||||||
|
<a href="/" className="text-gray-700 hover:text-blue-600">
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
<a href="/about" className="text-gray-700 hover:text-blue-600">
|
||||||
|
About
|
||||||
|
</a>
|
||||||
|
<a href="/contact" className="text-gray-700 hover:text-blue-600">
|
||||||
|
Contact
|
||||||
|
</a>
|
||||||
|
<a href="/blog" className="text-gray-700 hover:text-blue-600">
|
||||||
|
Blog
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Fix**:
|
||||||
|
```tsx
|
||||||
|
<nav className="flex flex-col md:flex-row md:space-x-6 space-y-2 md:space-y-0">
|
||||||
|
<a href="/" className="text-gray-700 hover:text-blue-600 py-2 md:py-0">
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
<a href="/about" className="text-gray-700 hover:text-blue-600 py-2 md:py-0">
|
||||||
|
About
|
||||||
|
</a>
|
||||||
|
<a href="/contact" className="text-gray-700 hover:text-blue-600 py-2 md:py-0">
|
||||||
|
Contact
|
||||||
|
</a>
|
||||||
|
<a href="/blog" className="text-gray-700 hover:text-blue-600 py-2 md:py-0">
|
||||||
|
Blog
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative Fix** (if horizontal menu required):
|
||||||
|
```tsx
|
||||||
|
<nav className="flex space-x-3 md:space-x-6 text-sm md:text-base">
|
||||||
|
<a href="/" className="text-gray-700 hover:text-blue-600 whitespace-nowrap">
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
<a href="/about" className="text-gray-700 hover:text-blue-600 whitespace-nowrap">
|
||||||
|
About
|
||||||
|
</a>
|
||||||
|
<a href="/contact" className="text-gray-700 hover:text-blue-600 whitespace-nowrap">
|
||||||
|
Contact
|
||||||
|
</a>
|
||||||
|
<a href="/blog" className="text-gray-700 hover:text-blue-600 whitespace-nowrap">
|
||||||
|
Blog
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reasoning**:
|
||||||
|
- **Option 1**: Stack links vertically on tablet/mobile, horizontal on desktop
|
||||||
|
- More reliable, works with any link text length
|
||||||
|
- Better for mobile usability
|
||||||
|
- **Option 2**: Reduce spacing and font size on smaller screens
|
||||||
|
- Maintains horizontal layout
|
||||||
|
- Risk: May still overflow with longer link text
|
||||||
|
|
||||||
|
**Recommendation**: Use Option 1 for reliability
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
1. Tablet (768px): Verify links stack vertically or have adequate spacing
|
||||||
|
2. Desktop (1024px+): Verify links display horizontally
|
||||||
|
3. Check all breakpoints: 640px, 768px, 1024px, 1280px
|
||||||
|
4. Test with longer link text (e.g., "Our Services" instead of "Blog")
|
||||||
|
|
||||||
|
**Impact**: Fixes navigation usability on tablet devices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix #4: Prevent Hero Image Distortion
|
||||||
|
|
||||||
|
**Issue**: Hero background image stretched on mobile viewport
|
||||||
|
|
||||||
|
**Location**: `src/components/Hero.tsx:15-25`
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```tsx
|
||||||
|
<div
|
||||||
|
className="hero-section h-96 bg-cover bg-center"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/images/hero-bg.jpg')",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="container mx-auto h-full flex items-center">
|
||||||
|
<h1 className="text-4xl font-bold text-white">Welcome to Our Site</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Fix**:
|
||||||
|
```tsx
|
||||||
|
<div className="hero-section h-96 relative overflow-hidden">
|
||||||
|
<img
|
||||||
|
src="/images/hero-bg.jpg"
|
||||||
|
alt=""
|
||||||
|
className="absolute inset-0 w-full h-full object-cover object-center"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<div className="container mx-auto h-full flex items-center relative z-10">
|
||||||
|
<h1 className="text-4xl font-bold text-white drop-shadow-lg">
|
||||||
|
Welcome to Our Site
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Changes Made**:
|
||||||
|
- Replaced CSS background image with `<img>` tag
|
||||||
|
- Added `object-cover` to maintain aspect ratio while filling container
|
||||||
|
- Added `object-center` for centered focal point
|
||||||
|
- Made container `relative` with image `absolute` for layering
|
||||||
|
- Added `drop-shadow-lg` to h1 for better text visibility
|
||||||
|
- Added `aria-hidden="true"` since image is decorative
|
||||||
|
|
||||||
|
**Reasoning**:
|
||||||
|
- `object-cover` scales image proportionally to fill container
|
||||||
|
- Crops excess rather than stretching to fit
|
||||||
|
- Maintains image quality and recognizability
|
||||||
|
- Works consistently across all viewport sizes
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
1. Mobile (375px): Verify image not stretched, focal point visible
|
||||||
|
2. Tablet (768px): Check image scales appropriately
|
||||||
|
3. Desktop (1280px): Ensure full image coverage
|
||||||
|
4. Test with different aspect ratio images (16:9, 4:3, 1:1)
|
||||||
|
|
||||||
|
**Impact**: Professional appearance maintained across all devices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix #5: Add Visible Error Messages
|
||||||
|
|
||||||
|
**Issue**: Form validation errors indicated only by red border (no text)
|
||||||
|
|
||||||
|
**Location**: `src/components/ContactForm.tsx:55-95`
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```tsx
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
className={`w-full px-4 py-2 border rounded ${
|
||||||
|
errors.email ? 'border-red-500' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Fix**:
|
||||||
|
```tsx
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="email" className="block text-gray-700 text-sm font-medium mb-1">
|
||||||
|
Email Address <span className="text-red-600" aria-label="required">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
className={`w-full px-4 py-2 border rounded ${
|
||||||
|
errors.email
|
||||||
|
? 'border-red-500 focus:ring-red-500'
|
||||||
|
: 'border-gray-300 focus:ring-blue-500'
|
||||||
|
}`}
|
||||||
|
aria-invalid={errors.email ? 'true' : 'false'}
|
||||||
|
aria-describedby={errors.email ? 'email-error' : undefined}
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<div
|
||||||
|
id="email-error"
|
||||||
|
className="mt-1 text-sm text-red-600 flex items-center"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mr-1 flex-shrink-0"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{errors.email}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Changes Made**:
|
||||||
|
- Added required indicator (*) with `aria-label`
|
||||||
|
- Added `aria-invalid` attribute for screen readers
|
||||||
|
- Added `aria-describedby` linking to error message
|
||||||
|
- Added visible error message below input
|
||||||
|
- Added error icon for visual reinforcement
|
||||||
|
- Added `role="alert"` to announce errors to screen readers
|
||||||
|
|
||||||
|
**Reasoning**:
|
||||||
|
- Error text provides specific guidance (not just "there's an error")
|
||||||
|
- Icon + color + text = multiple indicators (not color alone)
|
||||||
|
- ARIA attributes ensure screen reader compatibility
|
||||||
|
- Error message ID allows programmatic association with input
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
1. Visual: Submit empty form, verify error text appears below inputs
|
||||||
|
2. Screen reader: Verify error messages are announced
|
||||||
|
3. Keyboard: Tab to input, verify error is read aloud
|
||||||
|
4. Contrast: Verify error text meets 4.5:1 ratio (red-600 on white)
|
||||||
|
|
||||||
|
**Impact**: Makes form errors accessible to all users, improves error recovery
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Medium Priority Fixes
|
||||||
|
|
||||||
|
### Fix #6: Standardize Feature Card Heights
|
||||||
|
|
||||||
|
**Issue**: Feature cards have inconsistent heights
|
||||||
|
|
||||||
|
**Location**: `src/components/FeatureSection.tsx:28-42`
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```tsx
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{features.map((feature) => (
|
||||||
|
<div key={feature.id} className="bg-white p-6 rounded shadow">
|
||||||
|
<h3 className="text-xl font-bold mb-2">{feature.title}</h3>
|
||||||
|
<p className="text-gray-600">{feature.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Fix**:
|
||||||
|
```tsx
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 items-start">
|
||||||
|
{features.map((feature) => (
|
||||||
|
<div key={feature.id} className="bg-white p-6 rounded shadow flex flex-col h-full">
|
||||||
|
<h3 className="text-xl font-bold mb-2">{feature.title}</h3>
|
||||||
|
<p className="text-gray-600 flex-grow">{feature.description}</p>
|
||||||
|
{feature.link && (
|
||||||
|
<a
|
||||||
|
href={feature.link}
|
||||||
|
className="mt-4 text-blue-600 hover:text-blue-700 font-medium"
|
||||||
|
>
|
||||||
|
Learn more →
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Changes Made**:
|
||||||
|
- Added `items-start` to grid (align cards to top)
|
||||||
|
- Added `flex flex-col h-full` to card (flexbox layout)
|
||||||
|
- Added `flex-grow` to description (fills available space)
|
||||||
|
- Positioned link at bottom with `mt-4` (consistent spacing)
|
||||||
|
|
||||||
|
**Reasoning**:
|
||||||
|
- `h-full` makes all cards same height (tallest card determines height)
|
||||||
|
- `flex-grow` on description pushes "Learn more" link to bottom
|
||||||
|
- Creates visual consistency across grid
|
||||||
|
- Maintains readability while looking polished
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
1. Desktop (3 columns): Verify all cards same height
|
||||||
|
2. Test with varying description lengths
|
||||||
|
3. Ensure "Learn more" links align at bottom
|
||||||
|
4. Mobile (1 column): Verify cards still look good stacked
|
||||||
|
|
||||||
|
**Impact**: More professional, polished appearance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix #7: Increase Footer Link Spacing on Mobile
|
||||||
|
|
||||||
|
**Issue**: Footer links only 4-6px apart on mobile, difficult to tap
|
||||||
|
|
||||||
|
**Location**: `src/components/Footer.tsx:45-58`
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```tsx
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<a href="/about" className="text-gray-600 hover:text-gray-900">
|
||||||
|
About
|
||||||
|
</a>
|
||||||
|
<a href="/contact" className="text-gray-600 hover:text-gray-900">
|
||||||
|
Contact
|
||||||
|
</a>
|
||||||
|
<a href="/privacy" className="text-gray-600 hover:text-gray-900">
|
||||||
|
Privacy Policy
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Fix**:
|
||||||
|
```tsx
|
||||||
|
<div className="flex flex-col space-y-3">
|
||||||
|
<a
|
||||||
|
href="/about"
|
||||||
|
className="text-gray-600 hover:text-gray-900 py-2 -my-2 inline-block"
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/contact"
|
||||||
|
className="text-gray-600 hover:text-gray-900 py-2 -my-2 inline-block"
|
||||||
|
>
|
||||||
|
Contact
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/privacy"
|
||||||
|
className="text-gray-600 hover:text-gray-900 py-2 -my-2 inline-block"
|
||||||
|
>
|
||||||
|
Privacy Policy
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Changes Made**:
|
||||||
|
- `space-y-1` → `space-y-3` (increased spacing from ~4px to ~12px)
|
||||||
|
- Added `py-2` (8px vertical padding, expanding tap area)
|
||||||
|
- Added `-my-2` (negative margin to maintain visual spacing)
|
||||||
|
- Added `inline-block` (allow vertical padding on inline element)
|
||||||
|
|
||||||
|
**Reasoning**:
|
||||||
|
- `space-y-3` provides minimum 8px spacing (WCAG recommendation)
|
||||||
|
- `py-2` creates 44px minimum tap target height (8px padding + ~28px text)
|
||||||
|
- Negative margin prevents excessive visual spacing
|
||||||
|
- Easier to tap accurately on mobile devices
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
1. Mobile (375px): Verify 44x44px minimum tap target
|
||||||
|
2. Test tapping each link with finger (not stylus)
|
||||||
|
3. Ensure no accidental mis-taps to adjacent links
|
||||||
|
4. Check visual spacing looks appropriate
|
||||||
|
|
||||||
|
**Impact**: Improved mobile usability, reduces user frustration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Low Priority Fixes
|
||||||
|
|
||||||
|
### Fix #8: Improve Heading Size Hierarchy
|
||||||
|
|
||||||
|
**Issue**: H2 and H3 appear same size, reducing visual hierarchy
|
||||||
|
|
||||||
|
**Location**: `src/styles/globals.css:15-25` OR Tailwind config
|
||||||
|
|
||||||
|
**Current Code** (CSS):
|
||||||
|
```css
|
||||||
|
h2, h3 {
|
||||||
|
font-size: 1.25rem; /* 20px */
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Fix** (CSS):
|
||||||
|
```css
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem; /* 24px */
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.25rem; /* 20px */
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**OR** (Tailwind utility classes):
|
||||||
|
|
||||||
|
Replace `text-xl` on H2s with `text-2xl`, keep `text-xl` on H3s:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<h2 className="text-2xl font-bold mb-3">Section Heading</h2>
|
||||||
|
<h3 className="text-xl font-semibold mb-2">Subsection Heading</h3>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reasoning**:
|
||||||
|
- H2 (24px) → H3 (20px) creates clear hierarchy
|
||||||
|
- Progressively lighter font weights reinforce hierarchy
|
||||||
|
- Proper heading sizes aid content scanning
|
||||||
|
- Improves semantic structure perception
|
||||||
|
|
||||||
|
**Testing**:
|
||||||
|
1. Visual check: H1 > H2 > H3 size progression
|
||||||
|
2. Compare before/after screenshots
|
||||||
|
3. Test with screen reader: Verify heading navigation still works
|
||||||
|
4. Check across different pages for consistency
|
||||||
|
|
||||||
|
**Impact**: Improved content scanability and professional appearance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
### Critical (Do First)
|
||||||
|
- [ ] Fix #1: Form label contrast
|
||||||
|
- [ ] Fix #2: Button text sizing
|
||||||
|
|
||||||
|
### High (This Sprint)
|
||||||
|
- [ ] Fix #3: Navigation overlap
|
||||||
|
- [ ] Fix #4: Hero image aspect ratio
|
||||||
|
- [ ] Fix #5: Visible error messages
|
||||||
|
|
||||||
|
### Medium (Next Iteration)
|
||||||
|
- [ ] Fix #6: Card height consistency
|
||||||
|
- [ ] Fix #7: Footer link spacing
|
||||||
|
|
||||||
|
### Low (Backlog)
|
||||||
|
- [ ] Fix #8: Heading hierarchy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing After Implementation
|
||||||
|
|
||||||
|
1. **Re-run Playwright tests**:
|
||||||
|
```bash
|
||||||
|
npm run test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Capture new screenshots**:
|
||||||
|
```bash
|
||||||
|
npm run test:e2e -- --update-snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run accessibility audit**:
|
||||||
|
```bash
|
||||||
|
npm run test:e2e -- accessibility.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Manual testing**:
|
||||||
|
- Test on real devices (iPhone, Android, iPad)
|
||||||
|
- Test with screen reader (VoiceOver on iOS, TalkBack on Android)
|
||||||
|
- Verify color contrast with browser DevTools
|
||||||
|
|
||||||
|
5. **Compare screenshots**:
|
||||||
|
- Before: `screenshots/baselines/`
|
||||||
|
- After: `screenshots/current/`
|
||||||
|
- Ensure visual improvements visible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Generated by**: playwright-e2e-automation skill
|
||||||
|
**Estimated Total Time**: 4-6 hours
|
||||||
|
**Confidence Level**: High (fixes based on standard patterns)
|
||||||
|
|
||||||
|
All fixes follow React + Tailwind CSS best practices and maintain existing code structure.
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
# Visual Analysis Report
|
||||||
|
|
||||||
|
**Generated**: 2025-11-01 16:30:45
|
||||||
|
**Test Run**: Homepage e2e tests
|
||||||
|
**Screenshots Analyzed**: 23
|
||||||
|
**Issues Found**: 8 (2 Critical, 3 High, 2 Medium, 1 Low)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Analyzed 23 screenshots across 3 viewports (desktop 1280x720, tablet 768x1024, mobile 375x667). Found 8 UI/UX issues requiring attention, including 2 critical accessibility violations and 3 high-priority layout bugs.
|
||||||
|
|
||||||
|
### Issue Breakdown by Category
|
||||||
|
|
||||||
|
- **Layout Issues**: 4
|
||||||
|
- **Accessibility Violations**: 2
|
||||||
|
- **Typography Problems**: 1
|
||||||
|
- **Responsive Design Issues**: 1
|
||||||
|
|
||||||
|
### Issue Breakdown by Severity
|
||||||
|
|
||||||
|
- **Critical** (P0): 2 issues - Fix immediately
|
||||||
|
- **High** (P1): 3 issues - Fix within sprint
|
||||||
|
- **Medium** (P2): 2 issues - Address in next iteration
|
||||||
|
- **Low** (P3): 1 issue - Polish/enhancement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Issues (P0)
|
||||||
|
|
||||||
|
### 1. Insufficient Color Contrast on Form Labels
|
||||||
|
|
||||||
|
**Severity**: Critical
|
||||||
|
**Category**: Accessibility
|
||||||
|
**Viewport**: All viewports
|
||||||
|
**Screenshot**: `screenshots/current/contact-form-initial-2025-11-01T16-28-32.png`
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
Form input labels use light gray (#AAAAAA) on white background (#FFFFFF), resulting in a contrast ratio of only 2.6:1. WCAG 2.1 AA requires 4.5:1 for normal text.
|
||||||
|
|
||||||
|
**User Impact**:
|
||||||
|
Users with low vision or color blindness cannot read form labels, making the contact form unusable for accessibility-dependent users. Fails WCAG 2.1 criterion 1.4.3 (Contrast Minimum).
|
||||||
|
|
||||||
|
**Visual Evidence**:
|
||||||
|
In the screenshot, the "Name", "Email", and "Message" labels appear very faint and difficult to read against the white background.
|
||||||
|
|
||||||
|
**Affected Elements**:
|
||||||
|
- Name label
|
||||||
|
- Email label
|
||||||
|
- Message label
|
||||||
|
|
||||||
|
**Recommended Fix**: See fix-recommendations.md #1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Button Text Truncated on Mobile Viewport
|
||||||
|
|
||||||
|
**Severity**: Critical
|
||||||
|
**Category**: Layout / Responsive
|
||||||
|
**Viewport**: Mobile (375x667)
|
||||||
|
**Screenshot**: `screenshots/current/contact-form-filled-mobile-2025-11-01T16-29-15.png`
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
The "Send Message" button text is cut off mid-word on mobile viewport, displaying "Send Mes..." due to fixed width and large font size.
|
||||||
|
|
||||||
|
**User Impact**:
|
||||||
|
Users cannot see the full button text, creating confusion about the button's purpose and reducing trust in the interface.
|
||||||
|
|
||||||
|
**Visual Evidence**:
|
||||||
|
The submit button shows truncated text with an ellipsis, indicating the button width is insufficient for the text content at the current font size.
|
||||||
|
|
||||||
|
**Recommended Fix**: See fix-recommendations.md #2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## High Priority Issues (P1)
|
||||||
|
|
||||||
|
### 3. Navigation Menu Items Overlap on Tablet Viewport
|
||||||
|
|
||||||
|
**Severity**: High
|
||||||
|
**Category**: Layout
|
||||||
|
**Viewport**: Tablet (768x1024)
|
||||||
|
**Screenshot**: `screenshots/current/homepage-responsive-tablet-2025-11-01T16-27-45.png`
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
Navigation menu items in the header overlap each other at tablet breakpoint (768px), causing "About" and "Contact" links to partially obscure each other.
|
||||||
|
|
||||||
|
**User Impact**:
|
||||||
|
Users cannot click on navigation links reliably, potentially clicking the wrong link or missing links entirely.
|
||||||
|
|
||||||
|
**Visual Evidence**:
|
||||||
|
Screenshot shows "About" and "Contact" link text overlapping in the header navigation bar.
|
||||||
|
|
||||||
|
**Recommended Fix**: See fix-recommendations.md #3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Hero Section Image Stretched on Mobile
|
||||||
|
|
||||||
|
**Severity**: High
|
||||||
|
**Category**: Responsive / Layout
|
||||||
|
**Viewport**: Mobile (375x667)
|
||||||
|
**Screenshot**: `screenshots/current/homepage-responsive-mobile-2025-11-01T16-27-52.png`
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
Hero section background image appears stretched and distorted on mobile viewport. The 16:9 image is forced into a narrow vertical space, causing visible distortion.
|
||||||
|
|
||||||
|
**User Impact**:
|
||||||
|
Unprofessional appearance reduces user trust and brand perception. Image content may be unrecognizable when distorted.
|
||||||
|
|
||||||
|
**Visual Evidence**:
|
||||||
|
The hero image shows obvious stretching, with circular elements appearing oval-shaped and text in the image appearing compressed vertically.
|
||||||
|
|
||||||
|
**Recommended Fix**: See fix-recommendations.md #4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Missing Error State Indication
|
||||||
|
|
||||||
|
**Severity**: High
|
||||||
|
**Category**: Accessibility / UX
|
||||||
|
**Viewport**: All viewports
|
||||||
|
**Screenshot**: `screenshots/current/contact-form-validation-errors-2025-11-01T16-29-45.png`
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
Form validation errors are indicated only by a red border around inputs. No error text is visible, and there's no icon or other non-color indicator.
|
||||||
|
|
||||||
|
**User Impact**:
|
||||||
|
Users relying on screen readers won't hear error messages. Color-blind users may not notice the red border. Error messages are essential for understanding what went wrong.
|
||||||
|
|
||||||
|
**Visual Evidence**:
|
||||||
|
Screenshot shows inputs with red borders but no visible error text below them explaining what the error is.
|
||||||
|
|
||||||
|
**Recommended Fix**: See fix-recommendations.md #5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Medium Priority Issues (P2)
|
||||||
|
|
||||||
|
### 6. Inconsistent Card Heights in Feature Section
|
||||||
|
|
||||||
|
**Severity**: Medium
|
||||||
|
**Category**: Layout
|
||||||
|
**Viewport**: Desktop (1280x720)
|
||||||
|
**Screenshot**: `screenshots/current/homepage-initial-load-2025-11-01T16-27-18.png`
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
Feature cards have varying heights due to different content lengths. The grid layout doesn't maintain consistent card heights, creating a jagged appearance.
|
||||||
|
|
||||||
|
**User Impact**:
|
||||||
|
Visually inconsistent and less professional. Makes the page feel unpolished.
|
||||||
|
|
||||||
|
**Visual Evidence**:
|
||||||
|
Three feature cards visible - first card is noticeably taller than the second, and third is somewhere in between, creating uneven rows.
|
||||||
|
|
||||||
|
**Recommended Fix**: See fix-recommendations.md #6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Footer Links Too Close Together on Mobile
|
||||||
|
|
||||||
|
**Severity**: Medium
|
||||||
|
**Category**: Responsive / Touch Targets
|
||||||
|
**Viewport**: Mobile (375x667)
|
||||||
|
**Screenshot**: `screenshots/current/homepage-responsive-mobile-2025-11-01T16-27-52.png`
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
Footer navigation links are spaced only 4-6px apart vertically on mobile, making them difficult to tap accurately. WCAG 2.1 recommends minimum 44x44px touch targets with 8px spacing.
|
||||||
|
|
||||||
|
**User Impact**:
|
||||||
|
Users frequently mis-tap links, requiring multiple attempts to navigate. Particularly frustrating for users with motor impairments or large fingers.
|
||||||
|
|
||||||
|
**Visual Evidence**:
|
||||||
|
Footer links appear very close together with minimal spacing between each link.
|
||||||
|
|
||||||
|
**Recommended Fix**: See fix-recommendations.md #7
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Low Priority Issues (P3)
|
||||||
|
|
||||||
|
### 8. Heading Sizes Not Progressively Smaller
|
||||||
|
|
||||||
|
**Severity**: Low
|
||||||
|
**Category**: Typography / Visual Hierarchy
|
||||||
|
**Viewport**: All viewports
|
||||||
|
**Screenshot**: `screenshots/current/about-page-loaded-2025-11-01T16-28-05.png`
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
H2 and H3 headings appear to be the same size (approximately 20px), reducing visual hierarchy and making it harder to scan content structure.
|
||||||
|
|
||||||
|
**User Impact**:
|
||||||
|
Minor impact on content scanability. Users may not immediately recognize the content hierarchy.
|
||||||
|
|
||||||
|
**Visual Evidence**:
|
||||||
|
Page title (H1) is clearly larger, but H2 section headings and H3 subsection headings are visually identical in size.
|
||||||
|
|
||||||
|
**Recommended Fix**: See fix-recommendations.md #8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary Statistics
|
||||||
|
|
||||||
|
### By Severity
|
||||||
|
| Severity | Count | Percentage |
|
||||||
|
|----------|-------|------------|
|
||||||
|
| Critical | 2 | 25% |
|
||||||
|
| High | 3 | 37.5% |
|
||||||
|
| Medium | 2 | 25% |
|
||||||
|
| Low | 1 | 12.5% |
|
||||||
|
|
||||||
|
### By Category
|
||||||
|
| Category | Count |
|
||||||
|
|----------------|-------|
|
||||||
|
| Layout | 4 |
|
||||||
|
| Accessibility | 2 |
|
||||||
|
| Typography | 1 |
|
||||||
|
| Responsive | 1 |
|
||||||
|
|
||||||
|
### By Viewport
|
||||||
|
| Viewport | Issues |
|
||||||
|
|----------|--------|
|
||||||
|
| Mobile | 5 |
|
||||||
|
| Tablet | 1 |
|
||||||
|
| Desktop | 1 |
|
||||||
|
| All | 3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Actions
|
||||||
|
|
||||||
|
1. **Immediate (Critical)**:
|
||||||
|
- Fix form label contrast (#1)
|
||||||
|
- Fix button text truncation on mobile (#2)
|
||||||
|
|
||||||
|
2. **This Sprint (High)**:
|
||||||
|
- Fix navigation overlap on tablet (#3)
|
||||||
|
- Fix hero image stretching (#4)
|
||||||
|
- Add visible error messages (#5)
|
||||||
|
|
||||||
|
3. **Next Iteration (Medium)**:
|
||||||
|
- Standardize feature card heights (#6)
|
||||||
|
- Increase footer link spacing on mobile (#7)
|
||||||
|
|
||||||
|
4. **Backlog (Low)**:
|
||||||
|
- Adjust heading size hierarchy (#8)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Recommendations
|
||||||
|
|
||||||
|
After fixes are implemented:
|
||||||
|
|
||||||
|
1. Re-run Playwright test suite to capture updated screenshots
|
||||||
|
2. Compare new screenshots with current baseline
|
||||||
|
3. Run accessibility audit with axe-core
|
||||||
|
4. Test on real devices (iOS Safari, Android Chrome)
|
||||||
|
5. Validate color contrast with WebAIM tool
|
||||||
|
6. Test with screen reader (VoiceOver, NVDA)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Generated by**: playwright-e2e-automation skill
|
||||||
|
**Analysis Method**: LLM-powered visual screenshot analysis
|
||||||
|
**Reference Guides**: accessibility-checks.md, common-ui-bugs.md
|
||||||
134
skills/playwright-e2e-automation/reference/ci-cd-integration.md
Normal file
134
skills/playwright-e2e-automation/reference/ci-cd-integration.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# CI/CD Integration
|
||||||
|
|
||||||
|
## GitHub Actions Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Playwright E2E Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: npm run test:e2e
|
||||||
|
|
||||||
|
- name: Upload screenshots
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: playwright-screenshots
|
||||||
|
path: screenshots/
|
||||||
|
|
||||||
|
- name: Upload HTML report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Baseline Management in CI
|
||||||
|
|
||||||
|
### 1. Store baselines in repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add screenshots/baselines/
|
||||||
|
git commit -m "chore: update visual regression baselines"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Update baselines on approval
|
||||||
|
|
||||||
|
1. Run tests locally: `npm run test:e2e`
|
||||||
|
2. Review diffs: `npx playwright show-report`
|
||||||
|
3. Update baselines: `npm run test:e2e:update-snapshots`
|
||||||
|
4. Commit updated baselines
|
||||||
|
|
||||||
|
### 3. Fail CI on visual regressions
|
||||||
|
|
||||||
|
- Configure threshold in playwright.config.ts
|
||||||
|
- Tests fail if diffs exceed threshold
|
||||||
|
- Review in CI artifacts before merging
|
||||||
|
|
||||||
|
## GitLab CI Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
e2e-tests:
|
||||||
|
image: mcr.microsoft.com/playwright:v1.40.0-jammy
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- npm ci
|
||||||
|
- npm run test:e2e
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
paths:
|
||||||
|
- screenshots/
|
||||||
|
- playwright-report/
|
||||||
|
expire_in: 7 days
|
||||||
|
```
|
||||||
|
|
||||||
|
## CircleCI Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: 2.1
|
||||||
|
jobs:
|
||||||
|
e2e-tests:
|
||||||
|
docker:
|
||||||
|
- image: mcr.microsoft.com/playwright:v1.40.0-jammy
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run test:e2e
|
||||||
|
- store_artifacts:
|
||||||
|
path: screenshots
|
||||||
|
- store_artifacts:
|
||||||
|
path: playwright-report
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Screenshot Artifact Storage
|
||||||
|
|
||||||
|
- Always upload screenshots as artifacts
|
||||||
|
- Keep artifacts for at least 7 days
|
||||||
|
- Consider cloud storage for long-term retention
|
||||||
|
|
||||||
|
### Parallel Execution
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Run tests across multiple shards
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: npx playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
shard: [1, 2, 3, 4]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Cache Playwright browsers
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cache/ms-playwright
|
||||||
|
key: playwright-${{ hashFiles('package-lock.json') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
Configure notifications for test failures:
|
||||||
|
- Slack integration
|
||||||
|
- Email alerts
|
||||||
|
- PR comments with screenshot diffs
|
||||||
186
skills/playwright-e2e-automation/reference/troubleshooting.md
Normal file
186
skills/playwright-e2e-automation/reference/troubleshooting.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# Troubleshooting Guide
|
||||||
|
|
||||||
|
## Framework Version Errors
|
||||||
|
|
||||||
|
### Tailwind CSS v4 Syntax Mismatch
|
||||||
|
|
||||||
|
**Symptom**: Console error "Cannot apply unknown utility class" or "Utilities must be known at build time"
|
||||||
|
|
||||||
|
**Cause**: Tailwind v4 installed but CSS uses old v3 `@tailwind` directive syntax
|
||||||
|
|
||||||
|
**Root Cause**: Breaking change in Tailwind v4 - changed from `@tailwind` to `@import` syntax
|
||||||
|
|
||||||
|
**Detection**: Pre-flight health check catches this before running tests
|
||||||
|
|
||||||
|
**Auto-fix Available**: Yes - skill detects version and uses correct template
|
||||||
|
|
||||||
|
**Manual Fix**:
|
||||||
|
```css
|
||||||
|
// Old (v3):
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
// New (v4):
|
||||||
|
@import "tailwindcss";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Also Update**: `postcss.config.js` - change `tailwindcss: {}` to `'@tailwindcss/postcss': {}`
|
||||||
|
|
||||||
|
**Prevention**: Skill consults `data/framework-versions.yaml` and selects appropriate template
|
||||||
|
|
||||||
|
**Documentation**: https://tailwindcss.com/docs/upgrade-guide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PostCSS Plugin Not Found
|
||||||
|
|
||||||
|
**Symptom**: Build error "Plugin tailwindcss not found" or "Cannot find module 'tailwindcss'"
|
||||||
|
|
||||||
|
**Cause**: Tailwind v4 renamed PostCSS plugin but config uses old name
|
||||||
|
|
||||||
|
**Root Cause**: PostCSS plugin changed from `tailwindcss` to `@tailwindcss/postcss` in v4
|
||||||
|
|
||||||
|
**Detection**: Pre-flight check or build error
|
||||||
|
|
||||||
|
**Auto-fix Available**: Yes - version detection selects correct PostCSS template
|
||||||
|
|
||||||
|
**Manual Fix**:
|
||||||
|
```javascript
|
||||||
|
// postcss.config.js
|
||||||
|
// Old (v3):
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// New (v4):
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verification**: Run `npm list @tailwindcss/postcss` to confirm installation
|
||||||
|
|
||||||
|
**Prevention**: Skill uses `templates/configs/postcss-tailwind-v4.js` for Tailwind v4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Version Incompatibility Warning
|
||||||
|
|
||||||
|
**Symptom**: Skill warns "Unknown version detected" or "Version outside known ranges"
|
||||||
|
|
||||||
|
**Cause**: Framework version not in compatibility database
|
||||||
|
|
||||||
|
**Impact**: Skill may use outdated templates or incorrect syntax
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check `data/framework-versions.yaml` for supported versions
|
||||||
|
2. If version is newer, skill uses latest known template (may need manual adjustment)
|
||||||
|
3. If version is older, skill may suggest upgrading
|
||||||
|
|
||||||
|
**Reporting**: Please report unknown versions as GitHub issues to improve skill
|
||||||
|
|
||||||
|
**Workaround**: Manually specify template paths if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
### Application not detected
|
||||||
|
|
||||||
|
**Cause**: Unrecognized framework or missing package.json
|
||||||
|
|
||||||
|
**Solution**: Ask user to specify app type and dev server command manually
|
||||||
|
|
||||||
|
**Fallback**: Use generic static site configuration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Dev server not running
|
||||||
|
|
||||||
|
**Cause**: Application not started before running tests
|
||||||
|
|
||||||
|
**Solution**: Attempt to start server automatically using detected script (npm run dev)
|
||||||
|
|
||||||
|
**Fallback**: Prompt user to start server manually
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Playwright installation fails
|
||||||
|
|
||||||
|
**Cause**: Network issues, permissions, incompatible Node version
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Check Node version (>=16)
|
||||||
|
- Retry with --force
|
||||||
|
- Suggest manual installation
|
||||||
|
|
||||||
|
**Debugging**: Show full error output, check npm logs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Screenshot capture fails
|
||||||
|
|
||||||
|
**Cause**: Timeout waiting for page load, element not found, navigation error
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Increase timeout
|
||||||
|
- Add explicit waits
|
||||||
|
- Capture partial screenshot on failure
|
||||||
|
|
||||||
|
**Recovery**: Continue with other tests, report failure with details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### No baselines exist for comparison
|
||||||
|
|
||||||
|
**Cause**: First test run, baselines deleted
|
||||||
|
|
||||||
|
**Solution**: Current screenshots become baselines automatically
|
||||||
|
|
||||||
|
**Message**: "No baselines found. Current screenshots saved as baselines."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Visual analysis fails
|
||||||
|
|
||||||
|
**Cause**: LLM API error, screenshot file corruption, unsupported format
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Retry analysis
|
||||||
|
- Skip corrupted images
|
||||||
|
- Validate PNG format
|
||||||
|
|
||||||
|
**Fallback**: Provide raw screenshots for manual inspection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
### Execution Times (Typical React App)
|
||||||
|
|
||||||
|
| Phase | Time |
|
||||||
|
|-------|------|
|
||||||
|
| Application detection | ~5 seconds |
|
||||||
|
| Playwright installation | ~2-3 minutes (one-time) |
|
||||||
|
| Configuration generation | ~10 seconds |
|
||||||
|
| Test generation | ~30 seconds |
|
||||||
|
| Test execution (5 tests) | ~30-60 seconds |
|
||||||
|
| Screenshot capture | ~1-2 seconds per screenshot |
|
||||||
|
| Visual analysis (10 screenshots) | ~1-2 minutes |
|
||||||
|
| Regression comparison | ~10 seconds |
|
||||||
|
| Fix generation | ~30 seconds |
|
||||||
|
|
||||||
|
**Total end-to-end time**: ~5-8 minutes (excluding Playwright install)
|
||||||
|
|
||||||
|
### Resource Usage
|
||||||
|
|
||||||
|
- **Disk space**: ~500MB (Playwright browsers)
|
||||||
|
- **Memory**: ~500MB during test execution
|
||||||
|
- **Screenshots**: ~1-2MB per full-page screenshot
|
||||||
294
skills/playwright-e2e-automation/scripts/README.md
Normal file
294
skills/playwright-e2e-automation/scripts/README.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
# Helper Scripts
|
||||||
|
|
||||||
|
These scripts provide reference implementations for automating Playwright e2e testing workflows. They are meant to guide LLM implementation, not to be executed directly.
|
||||||
|
|
||||||
|
## Script Overview
|
||||||
|
|
||||||
|
### 1. `setup-playwright.sh`
|
||||||
|
**Purpose**: Automate Playwright installation and initial configuration
|
||||||
|
|
||||||
|
**What it does**:
|
||||||
|
- Checks Node.js version compatibility
|
||||||
|
- Installs Playwright via npm
|
||||||
|
- Installs browser binaries (Chromium, Firefox, WebKit)
|
||||||
|
- Generates initial playwright.config.ts
|
||||||
|
- Creates test directory structure
|
||||||
|
|
||||||
|
**Usage Pattern** (for LLM to implement):
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Detect if Playwright is already installed
|
||||||
|
# If not, run: npm init playwright@latest -- --yes
|
||||||
|
# Create directory structure
|
||||||
|
# Generate config based on detected framework
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `detect-framework.ts`
|
||||||
|
**Purpose**: Identify application framework and configuration
|
||||||
|
|
||||||
|
**What it does**:
|
||||||
|
- Reads package.json dependencies
|
||||||
|
- Checks for config files (vite.config.ts, next.config.js, etc.)
|
||||||
|
- Determines dev server command and port
|
||||||
|
- Returns framework metadata for test generation
|
||||||
|
|
||||||
|
**Usage Pattern**:
|
||||||
|
```typescript
|
||||||
|
// Read package.json
|
||||||
|
// Match against patterns in framework-detection-patterns.yaml
|
||||||
|
// Return framework object with baseURL, devCommand, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. `generate-tests.ts`
|
||||||
|
**Purpose**: Generate Playwright test specifications from templates
|
||||||
|
|
||||||
|
**What it does**:
|
||||||
|
- Takes framework metadata and user journeys as input
|
||||||
|
- Populates test templates with appropriate selectors
|
||||||
|
- Adds screenshot capture points
|
||||||
|
- Generates Page Object Models
|
||||||
|
- Creates test helper utilities
|
||||||
|
|
||||||
|
**Usage Pattern**:
|
||||||
|
```typescript
|
||||||
|
// Load templates from ../templates/
|
||||||
|
// Populate with framework-specific values
|
||||||
|
// Generate .spec.ts files in tests/specs/
|
||||||
|
// Generate .page.ts files in tests/pages/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. `capture-screenshots.ts`
|
||||||
|
**Purpose**: Execute tests and capture organized screenshots
|
||||||
|
|
||||||
|
**What it does**:
|
||||||
|
- Runs Playwright test suite
|
||||||
|
- Captures screenshots at defined points
|
||||||
|
- Organizes by test name, viewport, timestamp
|
||||||
|
- Generates metadata JSON for each screenshot
|
||||||
|
|
||||||
|
**Usage Pattern**:
|
||||||
|
```typescript
|
||||||
|
// Run: npx playwright test
|
||||||
|
// Hook into test lifecycle to capture screenshots
|
||||||
|
// Save to screenshots/current/{test-name}-{viewport}-{state}-{timestamp}.png
|
||||||
|
// Generate screenshots/metadata.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. `analyze-visual.ts`
|
||||||
|
**Purpose**: LLM-powered visual analysis of screenshots
|
||||||
|
|
||||||
|
**What it does**:
|
||||||
|
- Reads all screenshots from current run
|
||||||
|
- Sends each to LLM vision API with analysis prompts
|
||||||
|
- Categorizes findings (UI bugs, accessibility, layout)
|
||||||
|
- Generates structured issue reports
|
||||||
|
- Assigns severity levels
|
||||||
|
|
||||||
|
**Usage Pattern**:
|
||||||
|
```typescript
|
||||||
|
// Read screenshots/current/*.png
|
||||||
|
// For each screenshot:
|
||||||
|
// - Send to LLM with prompts from ../data/common-ui-bugs.md
|
||||||
|
// - Extract identified issues
|
||||||
|
// - Categorize and rate severity
|
||||||
|
// Generate visual-analysis-report.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. `compare-regression.ts`
|
||||||
|
**Purpose**: Compare current screenshots with baselines
|
||||||
|
|
||||||
|
**What it does**:
|
||||||
|
- Loads baseline screenshots
|
||||||
|
- Performs pixel-level comparison with current screenshots
|
||||||
|
- Generates diff images highlighting changes
|
||||||
|
// Calculates difference percentages
|
||||||
|
- Classifies changes (expected, suspicious, critical)
|
||||||
|
|
||||||
|
**Usage Pattern**:
|
||||||
|
```typescript
|
||||||
|
// Load screenshots/baselines/*.png
|
||||||
|
// Load screenshots/current/*.png
|
||||||
|
// Use Playwright's comparison utilities
|
||||||
|
// Generate screenshots/diffs/*.png
|
||||||
|
// Create regression-report.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. `generate-fixes.ts`
|
||||||
|
**Purpose**: Generate code fix recommendations from visual issues
|
||||||
|
|
||||||
|
**What it does**:
|
||||||
|
- Takes visual analysis results as input
|
||||||
|
- Maps issues to source code locations
|
||||||
|
- Generates specific fix recommendations
|
||||||
|
- Provides before/after code snippets
|
||||||
|
- Prioritizes by severity
|
||||||
|
|
||||||
|
**Usage Pattern**:
|
||||||
|
```typescript
|
||||||
|
// Read visual-analysis-report.md
|
||||||
|
// For each issue:
|
||||||
|
// - Infer likely source file (component, styles)
|
||||||
|
// - Generate fix recommendation
|
||||||
|
// - Provide file:line reference
|
||||||
|
// - Create code snippet
|
||||||
|
// Generate fix-recommendations.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. `export-test-suite.ts`
|
||||||
|
**Purpose**: Package test suite for production use
|
||||||
|
|
||||||
|
**What it does**:
|
||||||
|
- Copies generated tests to project's tests/ directory
|
||||||
|
- Adds npm scripts to package.json
|
||||||
|
- Generates README with usage instructions
|
||||||
|
- Creates CI/CD workflow examples
|
||||||
|
- Validates everything works
|
||||||
|
|
||||||
|
**Usage Pattern**:
|
||||||
|
```typescript
|
||||||
|
// Copy tests/specs/ to project/tests/e2e/
|
||||||
|
// Copy tests/pages/ to project/tests/e2e/pages/
|
||||||
|
// Update project/package.json with test scripts
|
||||||
|
// Generate project/tests/e2e/README.md
|
||||||
|
// Validate: run tests to ensure they work
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Guidelines
|
||||||
|
|
||||||
|
### For LLMs Implementing These Scripts
|
||||||
|
|
||||||
|
**Don't execute scripts directly** - Instead, implement the logic inline:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example: Instead of calling setup-playwright.sh
|
||||||
|
// Implement the logic directly in your response:
|
||||||
|
|
||||||
|
async function setupPlaywright() {
|
||||||
|
// 1. Check Node version
|
||||||
|
const nodeVersion = await execCommand('node --version');
|
||||||
|
if (!nodeVersion.startsWith('v16') && !nodeVersion.startsWith('v18')) {
|
||||||
|
throw new Error('Node 16+ required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Install Playwright
|
||||||
|
await execCommand('npm init playwright@latest -- --yes');
|
||||||
|
|
||||||
|
// 3. Create directory structure
|
||||||
|
await createDirectories([
|
||||||
|
'tests/setup',
|
||||||
|
'tests/pages',
|
||||||
|
'tests/specs',
|
||||||
|
'tests/utils',
|
||||||
|
'screenshots/baselines',
|
||||||
|
'screenshots/current',
|
||||||
|
'screenshots/diffs',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 4. Generate config (using templates)
|
||||||
|
const config = await generatePlaywrightConfig(framework);
|
||||||
|
await writeFile('playwright.config.ts', config);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Script Dependencies
|
||||||
|
|
||||||
|
All scripts should be standalone reference implementations:
|
||||||
|
- No external dependencies beyond Playwright
|
||||||
|
- Clear, commented code
|
||||||
|
- Error handling included
|
||||||
|
- TypeScript for type safety
|
||||||
|
|
||||||
|
### Data Flow Between Scripts
|
||||||
|
|
||||||
|
```
|
||||||
|
1. detect-framework.ts
|
||||||
|
↓ (framework metadata)
|
||||||
|
2. setup-playwright.sh
|
||||||
|
↓ (Playwright installed, config generated)
|
||||||
|
3. generate-tests.ts
|
||||||
|
↓ (test files created)
|
||||||
|
4. capture-screenshots.ts
|
||||||
|
↓ (screenshots captured)
|
||||||
|
5. analyze-visual.ts
|
||||||
|
↓ (issues identified)
|
||||||
|
6. compare-regression.ts
|
||||||
|
↓ (regressions detected)
|
||||||
|
7. generate-fixes.ts
|
||||||
|
↓ (fix recommendations created)
|
||||||
|
8. export-test-suite.ts
|
||||||
|
↓ (production-ready test suite)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing the Scripts
|
||||||
|
|
||||||
|
To validate script logic (for human developers):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Framework detection
|
||||||
|
npx ts-node scripts/detect-framework.ts
|
||||||
|
# Should output: { framework: 'react_vite', baseURL: 'http://localhost:5173', ... }
|
||||||
|
|
||||||
|
# 2. Test generation
|
||||||
|
npx ts-node scripts/generate-tests.ts --framework react_vite
|
||||||
|
# Should create: tests/specs/*.spec.ts
|
||||||
|
|
||||||
|
# 3. Screenshot capture
|
||||||
|
npx ts-node scripts/capture-screenshots.ts
|
||||||
|
# Should create: screenshots/current/*.png
|
||||||
|
|
||||||
|
# 4. Visual analysis
|
||||||
|
npx ts-node scripts/analyze-visual.ts
|
||||||
|
# Should create: visual-analysis-report.md
|
||||||
|
|
||||||
|
# 5. Regression comparison
|
||||||
|
npx ts-node scripts/compare-regression.ts
|
||||||
|
# Should create: screenshots/diffs/*.png, regression-report.md
|
||||||
|
|
||||||
|
# 6. Fix generation
|
||||||
|
npx ts-node scripts/generate-fixes.ts
|
||||||
|
# Should create: fix-recommendations.md
|
||||||
|
|
||||||
|
# 7. Export
|
||||||
|
npx ts-node scripts/export-test-suite.ts
|
||||||
|
# Should copy files to project/tests/e2e/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
All scripts should handle common errors:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
// Script logic
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
console.error('File not found. Check paths.');
|
||||||
|
} else if (error.message.includes('npm')) {
|
||||||
|
console.error('npm command failed. Check if npm is installed.');
|
||||||
|
} else {
|
||||||
|
console.error('Unexpected error:', error.message);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- **Parallel execution**: Run tests in parallel where possible
|
||||||
|
- **Incremental screenshots**: Only capture screenshots for changed tests
|
||||||
|
- **Caching**: Cache framework detection results
|
||||||
|
- **Batch processing**: Process multiple screenshots in batches for LLM analysis
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential scripts to add:
|
||||||
|
|
||||||
|
- `update-baselines.ts` - Interactive baseline approval
|
||||||
|
- `optimize-selectors.ts` - Suggest better selectors
|
||||||
|
- `generate-performance-tests.ts` - Add Core Web Vitals checks
|
||||||
|
- `create-ci-config.ts` - Generate GitHub Actions workflow
|
||||||
|
- `analyze-flaky-tests.ts` - Detect and fix flaky tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember**: These are reference implementations to guide LLM development. The actual execution happens through LLM-generated code, not by running these scripts directly.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* PostCSS Configuration for Tailwind CSS v3
|
||||||
|
*
|
||||||
|
* Compatible with: Tailwind CSS >=3.0.0 <4.0.0
|
||||||
|
* Generated by: playwright-e2e-automation skill
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
// Tailwind CSS v3 uses 'tailwindcss' as plugin name
|
||||||
|
tailwindcss: {},
|
||||||
|
|
||||||
|
// Autoprefixer adds vendor prefixes for browser compatibility
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* PostCSS Configuration for Tailwind CSS v4
|
||||||
|
*
|
||||||
|
* Compatible with: Tailwind CSS >=4.0.0
|
||||||
|
*
|
||||||
|
* Breaking changes from v3:
|
||||||
|
* - Plugin name changed from 'tailwindcss' to '@tailwindcss/postcss'
|
||||||
|
* - Improved performance with new architecture
|
||||||
|
* - Better integration with build tools
|
||||||
|
*
|
||||||
|
* Migration guide: https://tailwindcss.com/docs/upgrade-guide
|
||||||
|
* Generated by: playwright-e2e-automation skill
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
// Tailwind CSS v4 uses '@tailwindcss/postcss' as plugin name
|
||||||
|
// This is a BREAKING CHANGE from v3
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
|
||||||
|
// Autoprefixer adds vendor prefixes for browser compatibility
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Tailwind CSS v3 Syntax
|
||||||
|
*
|
||||||
|
* This template uses the classic @tailwind directive syntax
|
||||||
|
* Compatible with: Tailwind CSS >=3.0.0 <4.0.0
|
||||||
|
*
|
||||||
|
* Generated by: playwright-e2e-automation skill
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Base styles, CSS resets, and browser normalization */
|
||||||
|
@tailwind base;
|
||||||
|
|
||||||
|
/* Component classes and utilities */
|
||||||
|
@tailwind components;
|
||||||
|
|
||||||
|
/* Utility classes (spacing, colors, typography, etc.) */
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* ========== CUSTOM STYLES ========== */
|
||||||
|
/* Add your custom CSS below this line */
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Tailwind CSS v4 Syntax
|
||||||
|
*
|
||||||
|
* This template uses the new @import syntax introduced in Tailwind v4
|
||||||
|
* Compatible with: Tailwind CSS >=4.0.0
|
||||||
|
*
|
||||||
|
* Breaking changes from v3:
|
||||||
|
* - @tailwind directives replaced with single @import
|
||||||
|
* - PostCSS plugin changed to @tailwindcss/postcss
|
||||||
|
* - Improved performance and smaller bundle size
|
||||||
|
*
|
||||||
|
* Migration guide: https://tailwindcss.com/docs/upgrade-guide
|
||||||
|
* Generated by: playwright-e2e-automation skill
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Import Tailwind CSS (base, components, utilities all included) */
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* ========== CUSTOM STYLES ========== */
|
||||||
|
/* Add your custom CSS below this line */
|
||||||
|
|
||||||
|
/* Example: Custom utility classes using @layer */
|
||||||
|
/*
|
||||||
|
@layer utilities {
|
||||||
|
.text-balance {
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Example: Custom components using @layer */
|
||||||
|
/*
|
||||||
|
@layer components {
|
||||||
|
.btn-primary {
|
||||||
|
@apply px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
171
skills/playwright-e2e-automation/templates/css/vanilla.css
Normal file
171
skills/playwright-e2e-automation/templates/css/vanilla.css
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* Vanilla CSS Template (No CSS Framework)
|
||||||
|
*
|
||||||
|
* This template is used when no CSS framework is detected
|
||||||
|
* or when Tailwind CSS is not installed.
|
||||||
|
*
|
||||||
|
* Generated by: playwright-e2e-automation skill
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ========== CSS RESET ========== */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== BASE STYLES ========== */
|
||||||
|
:root {
|
||||||
|
/* Color palette */
|
||||||
|
--color-primary: #3b82f6;
|
||||||
|
--color-primary-dark: #2563eb;
|
||||||
|
--color-text: #1f2937;
|
||||||
|
--color-text-light: #6b7280;
|
||||||
|
--color-background: #ffffff;
|
||||||
|
--color-border: #e5e7eb;
|
||||||
|
|
||||||
|
/* Spacing scale */
|
||||||
|
--spacing-xs: 0.25rem;
|
||||||
|
--spacing-sm: 0.5rem;
|
||||||
|
--spacing-md: 1rem;
|
||||||
|
--spacing-lg: 1.5rem;
|
||||||
|
--spacing-xl: 2rem;
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-sans: system-ui, -apple-system, sans-serif;
|
||||||
|
--font-mono: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
color: var(--color-text);
|
||||||
|
background-color: var(--color-background);
|
||||||
|
line-height: 1.6;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== TYPOGRAPHY ========== */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== FORM ELEMENTS ========== */
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== UTILITY CLASSES ========== */
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-1 {
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
.mt-2 {
|
||||||
|
margin-top: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
.mt-3 {
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
}
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-1 {
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
.mb-3 {
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== CUSTOM STYLES ========== */
|
||||||
|
/* Add your custom CSS below this line */
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { chromium, FullConfig } from '@playwright/test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global Setup
|
||||||
|
* Runs once before all tests
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Ensure dev server is ready
|
||||||
|
* - Create screenshot directories
|
||||||
|
* - Perform any global authentication
|
||||||
|
* - Set up test database (if needed)
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function globalSetup(config: FullConfig) {
|
||||||
|
console.log('\n🚀 Starting global setup...\n');
|
||||||
|
|
||||||
|
// 1. Create screenshot directories
|
||||||
|
createScreenshotDirectories();
|
||||||
|
|
||||||
|
// 2. Verify dev server is accessible (webServer config handles startup)
|
||||||
|
const baseURL = config.projects[0].use.baseURL || 'http://localhost:{{PORT}}';
|
||||||
|
await verifyServer(baseURL);
|
||||||
|
|
||||||
|
// 3. Optional: Perform global authentication
|
||||||
|
// await performAuthentication(config);
|
||||||
|
|
||||||
|
console.log('✅ Global setup complete\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create necessary screenshot directories
|
||||||
|
*/
|
||||||
|
function createScreenshotDirectories() {
|
||||||
|
const directories = [
|
||||||
|
'screenshots/current',
|
||||||
|
'screenshots/baselines',
|
||||||
|
'screenshots/diffs',
|
||||||
|
'test-results',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dir of directories) {
|
||||||
|
const dirPath = path.join(process.cwd(), dir);
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
console.log(`📁 Created directory: ${dir}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify dev server is accessible
|
||||||
|
*/
|
||||||
|
async function verifyServer(baseURL: string, maxRetries = 30) {
|
||||||
|
console.log(`🔍 Verifying server at ${baseURL}...`);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
|
try {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
const response = await page.goto(baseURL, { timeout: 5000 });
|
||||||
|
|
||||||
|
if (response && response.ok()) {
|
||||||
|
console.log(`✅ Server is ready at ${baseURL}`);
|
||||||
|
await browser.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
} catch (error) {
|
||||||
|
// Server not ready yet, wait and retry
|
||||||
|
if (i < maxRetries - 1) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Server at ${baseURL} is not accessible after ${maxRetries} attempts`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional: Perform global authentication
|
||||||
|
* Saves authentication state to be reused across all tests
|
||||||
|
*/
|
||||||
|
async function performAuthentication(config: FullConfig) {
|
||||||
|
// Only run if authentication is needed
|
||||||
|
if (!process.env.AUTH_USERNAME || !process.env.AUTH_PASSWORD) {
|
||||||
|
console.log('⏭️ Skipping authentication (no credentials provided)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔐 Performing global authentication...');
|
||||||
|
|
||||||
|
const baseURL = config.projects[0].use.baseURL || 'http://localhost:{{PORT}}';
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const context = await browser.newContext();
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Navigate to login page
|
||||||
|
await page.goto(`${baseURL}/login`);
|
||||||
|
|
||||||
|
// Fill in credentials
|
||||||
|
await page.getByLabel('Username').fill(process.env.AUTH_USERNAME);
|
||||||
|
await page.getByLabel('Password').fill(process.env.AUTH_PASSWORD);
|
||||||
|
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||||
|
|
||||||
|
// Wait for successful login (adjust selector as needed)
|
||||||
|
await page.waitForURL(`${baseURL}/dashboard`, { timeout: 10000 });
|
||||||
|
|
||||||
|
// Save authentication state
|
||||||
|
await context.storageState({ path: 'auth.json' });
|
||||||
|
|
||||||
|
console.log('✅ Authentication successful, state saved to auth.json');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Authentication failed:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default globalSetup;
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { FullConfig } from '@playwright/test';
|
||||||
|
import { generateManifest } from '../utils/screenshot-helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global Teardown
|
||||||
|
* Runs once after all tests complete
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Generate screenshot manifest
|
||||||
|
* - Clean up temporary files (if needed)
|
||||||
|
* - Generate summary report
|
||||||
|
* - Perform any cleanup tasks
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function globalTeardown(config: FullConfig) {
|
||||||
|
console.log('\n🏁 Starting global teardown...\n');
|
||||||
|
|
||||||
|
// 1. Generate screenshot manifest
|
||||||
|
try {
|
||||||
|
generateManifest();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('⚠️ Failed to generate screenshot manifest:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Generate test summary
|
||||||
|
generateSummary();
|
||||||
|
|
||||||
|
// 3. Optional: Clean up temporary files
|
||||||
|
// cleanupTempFiles();
|
||||||
|
|
||||||
|
console.log('\n✅ Global teardown complete\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate test execution summary
|
||||||
|
*/
|
||||||
|
function generateSummary() {
|
||||||
|
console.log('\n📊 Test Execution Summary:');
|
||||||
|
console.log('─'.repeat(50));
|
||||||
|
|
||||||
|
// Test results are available through Playwright's built-in reporters
|
||||||
|
// This is just a placeholder for custom summary logic
|
||||||
|
|
||||||
|
console.log('✅ Check playwright-report/ for detailed results');
|
||||||
|
console.log('✅ Screenshots available in screenshots/current/');
|
||||||
|
console.log('✅ Test results available in test-results/');
|
||||||
|
|
||||||
|
console.log('\n💡 Next steps:');
|
||||||
|
console.log(' 1. Review screenshots for visual issues');
|
||||||
|
console.log(' 2. Compare with baselines if available');
|
||||||
|
console.log(' 3. Run visual analysis: npm run analyze:visual');
|
||||||
|
console.log(' 4. Generate fix recommendations if issues found');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional: Clean up temporary files
|
||||||
|
*/
|
||||||
|
function cleanupTempFiles() {
|
||||||
|
// Add cleanup logic here if needed
|
||||||
|
// For example: remove old screenshots, clear cache, etc.
|
||||||
|
console.log('🧹 Cleaning up temporary files...');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default globalTeardown;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Page, Locator } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {{PAGE_NAME}} Page Object Model
|
||||||
|
*
|
||||||
|
* Represents: {{PAGE_DESCRIPTION}}
|
||||||
|
* URL: {{PAGE_URL}}
|
||||||
|
* Generated: {{GENERATED_DATE}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class {{PAGE_CLASS_NAME}} {
|
||||||
|
readonly page: Page;
|
||||||
|
|
||||||
|
// Locators - Using semantic selectors (getByRole, getByLabel, getByText)
|
||||||
|
{{#LOCATORS}}
|
||||||
|
readonly {{LOCATOR_NAME}}: Locator;
|
||||||
|
{{/LOCATORS}}
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
|
||||||
|
// Initialize locators
|
||||||
|
{{#LOCATORS}}
|
||||||
|
this.{{LOCATOR_NAME}} = page.{{SELECTOR}};
|
||||||
|
{{/LOCATORS}}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to this page
|
||||||
|
*/
|
||||||
|
async goto() {
|
||||||
|
await this.page.goto('{{PAGE_URL}}');
|
||||||
|
await this.page.waitForLoadState('networkidle');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for page to be ready
|
||||||
|
*/
|
||||||
|
async waitForReady() {
|
||||||
|
await this.page.waitForLoadState('domcontentloaded');
|
||||||
|
{{#READY_INDICATORS}}
|
||||||
|
await this.{{INDICATOR}}.waitFor({ state: 'visible' });
|
||||||
|
{{/READY_INDICATORS}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{#METHODS}}
|
||||||
|
/**
|
||||||
|
* {{METHOD_DESCRIPTION}}
|
||||||
|
{{#PARAMS}}
|
||||||
|
* @param {{PARAM_NAME}} - {{PARAM_DESCRIPTION}}
|
||||||
|
{{/PARAMS}}
|
||||||
|
*/
|
||||||
|
async {{METHOD_NAME}}({{PARAMS_SIGNATURE}}) {
|
||||||
|
{{METHOD_BODY}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/METHODS}}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get page title
|
||||||
|
*/
|
||||||
|
async getTitle(): Promise<string> {
|
||||||
|
return await this.page.title();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current URL
|
||||||
|
*/
|
||||||
|
async getCurrentUrl(): Promise<string> {
|
||||||
|
return this.page.url();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take screenshot of this page
|
||||||
|
* @param name - Screenshot filename
|
||||||
|
*/
|
||||||
|
async screenshot(name: string) {
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
await this.page.screenshot({
|
||||||
|
path: `screenshots/current/{{PAGE_NAME_KEBAB}}-${name}-${timestamp}.png`,
|
||||||
|
fullPage: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playwright Configuration
|
||||||
|
* Generated by playwright-e2e-automation skill
|
||||||
|
*
|
||||||
|
* Framework: {{FRAMEWORK_NAME}}
|
||||||
|
* Base URL: {{BASE_URL}}
|
||||||
|
* Generated: {{GENERATED_DATE}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests/specs',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum time one test can run for
|
||||||
|
*/
|
||||||
|
timeout: {{TIMEOUT}},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test execution settings
|
||||||
|
*/
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reporter configuration
|
||||||
|
* CI: GitHub Actions reporter
|
||||||
|
* Local: HTML reporter with screenshots
|
||||||
|
*/
|
||||||
|
reporter: process.env.CI
|
||||||
|
? 'github'
|
||||||
|
: [
|
||||||
|
['html', { outputFolder: 'playwright-report' }],
|
||||||
|
['json', { outputFile: 'test-results/results.json' }],
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared settings for all projects
|
||||||
|
*/
|
||||||
|
use: {
|
||||||
|
/* Base URL for navigation */
|
||||||
|
baseURL: '{{BASE_URL}}',
|
||||||
|
|
||||||
|
/* Collect trace on first retry */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
|
||||||
|
/* Screenshot settings */
|
||||||
|
screenshot: {
|
||||||
|
mode: 'only-on-failure',
|
||||||
|
fullPage: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Video settings */
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
|
||||||
|
/* Maximum time each action can take */
|
||||||
|
actionTimeout: 10000,
|
||||||
|
|
||||||
|
/* Navigation timeout */
|
||||||
|
navigationTimeout: 30000,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browser and device configurations
|
||||||
|
*/
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium-desktop',
|
||||||
|
use: {
|
||||||
|
...devices['Desktop Chrome'],
|
||||||
|
viewport: { width: 1280, height: 720 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'firefox-desktop',
|
||||||
|
use: {
|
||||||
|
...devices['Desktop Firefox'],
|
||||||
|
viewport: { width: 1280, height: 720 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'webkit-desktop',
|
||||||
|
use: {
|
||||||
|
...devices['Desktop Safari'],
|
||||||
|
viewport: { width: 1280, height: 720 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'mobile-chrome',
|
||||||
|
use: {
|
||||||
|
...devices['Pixel 5'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'mobile-safari',
|
||||||
|
use: {
|
||||||
|
...devices['iPhone 13'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'tablet',
|
||||||
|
use: {
|
||||||
|
...devices['iPad Pro'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web server configuration
|
||||||
|
* Starts dev server before running tests
|
||||||
|
*/
|
||||||
|
webServer: {
|
||||||
|
command: '{{DEV_SERVER_COMMAND}}',
|
||||||
|
url: '{{BASE_URL}}',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 120 * 1000,
|
||||||
|
stdout: 'pipe',
|
||||||
|
stderr: 'pipe',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output directories
|
||||||
|
*/
|
||||||
|
outputDir: 'test-results',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global setup/teardown
|
||||||
|
*/
|
||||||
|
globalSetup: require.resolve('./tests/setup/global-setup.ts'),
|
||||||
|
globalTeardown: require.resolve('./tests/setup/global-teardown.ts'),
|
||||||
|
});
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
import { Page } from '@playwright/test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screenshot Helper Utilities
|
||||||
|
* Provides consistent screenshot capture with metadata
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ScreenshotMetadata {
|
||||||
|
path: string;
|
||||||
|
context: string;
|
||||||
|
timestamp: string;
|
||||||
|
viewport: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
url: string;
|
||||||
|
testName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture screenshot with context metadata
|
||||||
|
*
|
||||||
|
* @param page - Playwright page object
|
||||||
|
* @param name - Screenshot name (will be kebab-cased)
|
||||||
|
* @param context - Description of what the screenshot shows
|
||||||
|
* @returns Metadata about the captured screenshot
|
||||||
|
*/
|
||||||
|
export async function captureWithContext(
|
||||||
|
page: Page,
|
||||||
|
name: string,
|
||||||
|
context: string
|
||||||
|
): Promise<ScreenshotMetadata> {
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const viewport = page.viewportSize() || { width: 1280, height: 720 };
|
||||||
|
const url = page.url();
|
||||||
|
|
||||||
|
// Ensure screenshots directory exists
|
||||||
|
const screenshotDir = path.join(process.cwd(), 'screenshots', 'current');
|
||||||
|
if (!fs.existsSync(screenshotDir)) {
|
||||||
|
fs.mkdirSync(screenshotDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate filename
|
||||||
|
const filename = `${name}-${timestamp}.png`;
|
||||||
|
const screenshotPath = path.join(screenshotDir, filename);
|
||||||
|
|
||||||
|
// Wait for network idle before capturing
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Capture screenshot
|
||||||
|
await page.screenshot({
|
||||||
|
path: screenshotPath,
|
||||||
|
fullPage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create metadata
|
||||||
|
const metadata: ScreenshotMetadata = {
|
||||||
|
path: screenshotPath,
|
||||||
|
context,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
viewport,
|
||||||
|
url,
|
||||||
|
testName: process.env.PLAYWRIGHT_TEST_NAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save metadata alongside screenshot
|
||||||
|
const metadataPath = screenshotPath.replace('.png', '.json');
|
||||||
|
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
||||||
|
|
||||||
|
console.log(`📸 Screenshot captured: ${filename}`);
|
||||||
|
console.log(` Context: ${context}`);
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture element screenshot with context
|
||||||
|
*
|
||||||
|
* @param page - Playwright page object
|
||||||
|
* @param selector - Element selector
|
||||||
|
* @param name - Screenshot name
|
||||||
|
* @param context - Description
|
||||||
|
*/
|
||||||
|
export async function captureElement(
|
||||||
|
page: Page,
|
||||||
|
selector: string,
|
||||||
|
name: string,
|
||||||
|
context: string
|
||||||
|
): Promise<ScreenshotMetadata> {
|
||||||
|
const element = page.locator(selector);
|
||||||
|
await element.waitFor({ state: 'visible' });
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const viewport = page.viewportSize() || { width: 1280, height: 720 };
|
||||||
|
const url = page.url();
|
||||||
|
|
||||||
|
const screenshotDir = path.join(process.cwd(), 'screenshots', 'current');
|
||||||
|
if (!fs.existsSync(screenshotDir)) {
|
||||||
|
fs.mkdirSync(screenshotDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = `${name}-element-${timestamp}.png`;
|
||||||
|
const screenshotPath = path.join(screenshotDir, filename);
|
||||||
|
|
||||||
|
await element.screenshot({
|
||||||
|
path: screenshotPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata: ScreenshotMetadata = {
|
||||||
|
path: screenshotPath,
|
||||||
|
context: `${context} (element: ${selector})`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
viewport,
|
||||||
|
url,
|
||||||
|
testName: process.env.PLAYWRIGHT_TEST_NAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
const metadataPath = screenshotPath.replace('.png', '.json');
|
||||||
|
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
||||||
|
|
||||||
|
console.log(`📸 Element screenshot captured: ${filename}`);
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture comparison screenshots (before/after)
|
||||||
|
*
|
||||||
|
* @param page - Playwright page object
|
||||||
|
* @param name - Base name for screenshots
|
||||||
|
* @param actionCallback - Action to perform between screenshots
|
||||||
|
*/
|
||||||
|
export async function captureComparison(
|
||||||
|
page: Page,
|
||||||
|
name: string,
|
||||||
|
actionCallback: () => Promise<void>
|
||||||
|
): Promise<{ before: ScreenshotMetadata; after: ScreenshotMetadata }> {
|
||||||
|
const before = await captureWithContext(page, `${name}-before`, 'State before action');
|
||||||
|
|
||||||
|
await actionCallback();
|
||||||
|
|
||||||
|
const after = await captureWithContext(page, `${name}-after`, 'State after action');
|
||||||
|
|
||||||
|
return { before, after };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture screenshots across multiple viewports
|
||||||
|
*
|
||||||
|
* @param page - Playwright page object
|
||||||
|
* @param name - Base name for screenshots
|
||||||
|
* @param viewports - Array of viewport configurations
|
||||||
|
*/
|
||||||
|
export async function captureViewports(
|
||||||
|
page: Page,
|
||||||
|
name: string,
|
||||||
|
viewports: Array<{ name: string; width: number; height: number }>
|
||||||
|
): Promise<ScreenshotMetadata[]> {
|
||||||
|
const screenshots: ScreenshotMetadata[] = [];
|
||||||
|
|
||||||
|
for (const viewport of viewports) {
|
||||||
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
||||||
|
|
||||||
|
// Wait for responsive changes to settle
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
const metadata = await captureWithContext(
|
||||||
|
page,
|
||||||
|
`${name}-${viewport.name}`,
|
||||||
|
`${viewport.width}x${viewport.height} viewport`
|
||||||
|
);
|
||||||
|
|
||||||
|
screenshots.push(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
return screenshots;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate screenshot manifest
|
||||||
|
* Collects all screenshots and their metadata into a single manifest file
|
||||||
|
*/
|
||||||
|
export function generateManifest(): void {
|
||||||
|
const screenshotDir = path.join(process.cwd(), 'screenshots', 'current');
|
||||||
|
|
||||||
|
if (!fs.existsSync(screenshotDir)) {
|
||||||
|
console.log('No screenshots directory found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(screenshotDir);
|
||||||
|
const metadataFiles = files.filter((f) => f.endsWith('.json'));
|
||||||
|
|
||||||
|
const manifest = metadataFiles.map((file) => {
|
||||||
|
const content = fs.readFileSync(path.join(screenshotDir, file), 'utf-8');
|
||||||
|
return JSON.parse(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifestPath = path.join(screenshotDir, 'manifest.json');
|
||||||
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
||||||
|
|
||||||
|
console.log(`\n📋 Screenshot manifest generated: ${manifestPath}`);
|
||||||
|
console.log(` Total screenshots: ${manifest.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare screenshot with baseline
|
||||||
|
*
|
||||||
|
* @param currentPath - Path to current screenshot
|
||||||
|
* @param baselinePath - Path to baseline screenshot
|
||||||
|
* @param diffPath - Path to save diff image
|
||||||
|
* @param threshold - Difference threshold (0-1, default 0.2 = 20%)
|
||||||
|
*/
|
||||||
|
export async function compareWithBaseline(
|
||||||
|
currentPath: string,
|
||||||
|
baselinePath: string,
|
||||||
|
diffPath: string,
|
||||||
|
threshold: number = 0.2
|
||||||
|
): Promise<{ match: boolean; diffPercentage: number }> {
|
||||||
|
// Note: This requires pixelmatch or Playwright's built-in comparison
|
||||||
|
// For now, this is a placeholder showing the interface
|
||||||
|
|
||||||
|
console.log(`🔍 Comparing screenshots:`);
|
||||||
|
console.log(` Current: ${currentPath}`);
|
||||||
|
console.log(` Baseline: ${baselinePath}`);
|
||||||
|
|
||||||
|
// Implementation would use Playwright's toHaveScreenshot comparison
|
||||||
|
// or a library like pixelmatch for pixel-level comparison
|
||||||
|
|
||||||
|
return {
|
||||||
|
match: true,
|
||||||
|
diffPercentage: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
120
skills/playwright-e2e-automation/templates/test-spec.template.ts
Normal file
120
skills/playwright-e2e-automation/templates/test-spec.template.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { {{PAGE_OBJECT_CLASS}} } from '../pages/{{PAGE_OBJECT_FILE}}';
|
||||||
|
import { captureWithContext } from '../utils/screenshot-helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {{TEST_SUITE_NAME}}
|
||||||
|
*
|
||||||
|
* Tests: {{TEST_DESCRIPTION}}
|
||||||
|
* Generated: {{GENERATED_DATE}}
|
||||||
|
*/
|
||||||
|
|
||||||
|
test.describe('{{TEST_SUITE_NAME}}', () => {
|
||||||
|
let page: {{PAGE_OBJECT_CLASS}};
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page: testPage }) => {
|
||||||
|
page = new {{PAGE_OBJECT_CLASS}}(testPage);
|
||||||
|
await page.goto();
|
||||||
|
|
||||||
|
// Capture initial page load
|
||||||
|
await captureWithContext(
|
||||||
|
testPage,
|
||||||
|
'{{TEST_SUITE_NAME_KEBAB}}-initial-load',
|
||||||
|
'Page loaded successfully'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
{{#TESTS}}
|
||||||
|
test('{{TEST_NAME}}', async ({ page: testPage }) => {
|
||||||
|
// Arrange: {{ARRANGE_DESCRIPTION}}
|
||||||
|
{{#ARRANGE_STEPS}}
|
||||||
|
{{STEP}}
|
||||||
|
{{/ARRANGE_STEPS}}
|
||||||
|
|
||||||
|
// Capture pre-action state
|
||||||
|
await captureWithContext(
|
||||||
|
testPage,
|
||||||
|
'{{TEST_SUITE_NAME_KEBAB}}-{{TEST_NAME_KEBAB}}-before',
|
||||||
|
'Before {{ACTION_DESCRIPTION}}'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act: {{ACTION_DESCRIPTION}}
|
||||||
|
{{ACTION_CODE}}
|
||||||
|
|
||||||
|
// Capture post-action state
|
||||||
|
await captureWithContext(
|
||||||
|
testPage,
|
||||||
|
'{{TEST_SUITE_NAME_KEBAB}}-{{TEST_NAME_KEBAB}}-after',
|
||||||
|
'After {{ACTION_DESCRIPTION}}'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert: {{ASSERT_DESCRIPTION}}
|
||||||
|
{{#ASSERTIONS}}
|
||||||
|
await expect({{SELECTOR}}).{{MATCHER}};
|
||||||
|
{{/ASSERTIONS}}
|
||||||
|
|
||||||
|
// Final state screenshot
|
||||||
|
await captureWithContext(
|
||||||
|
testPage,
|
||||||
|
'{{TEST_SUITE_NAME_KEBAB}}-{{TEST_NAME_KEBAB}}-final',
|
||||||
|
'{{FINAL_STATE_DESCRIPTION}}'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
{{/TESTS}}
|
||||||
|
|
||||||
|
test('should not have accessibility violations', async ({ page: testPage }) => {
|
||||||
|
// Run accessibility audit
|
||||||
|
const AxeBuilder = (await import('@axe-core/playwright')).default;
|
||||||
|
|
||||||
|
const accessibilityScanResults = await new AxeBuilder({ page: testPage })
|
||||||
|
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
||||||
|
.analyze();
|
||||||
|
|
||||||
|
// Capture page with any violations highlighted
|
||||||
|
await captureWithContext(
|
||||||
|
testPage,
|
||||||
|
'{{TEST_SUITE_NAME_KEBAB}}-accessibility-check',
|
||||||
|
`Found ${accessibilityScanResults.violations.length} accessibility violations`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fail if there are critical violations
|
||||||
|
const criticalViolations = accessibilityScanResults.violations.filter(
|
||||||
|
(v) => v.impact === 'critical' || v.impact === 'serious'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(criticalViolations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display correctly across viewports', async ({ page: testPage }) => {
|
||||||
|
const viewports = [
|
||||||
|
{ name: 'desktop', width: 1280, height: 720 },
|
||||||
|
{ name: 'tablet', width: 768, height: 1024 },
|
||||||
|
{ name: 'mobile', width: 375, height: 667 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const viewport of viewports) {
|
||||||
|
await testPage.setViewportSize(viewport);
|
||||||
|
|
||||||
|
// Wait for any responsive changes to settle
|
||||||
|
await testPage.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Capture screenshot for each viewport
|
||||||
|
await captureWithContext(
|
||||||
|
testPage,
|
||||||
|
`{{TEST_SUITE_NAME_KEBAB}}-responsive-${viewport.name}`,
|
||||||
|
`${viewport.width}x${viewport.height} viewport`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Basic responsive checks
|
||||||
|
await expect(testPage.locator('body')).toBeVisible();
|
||||||
|
|
||||||
|
// No horizontal scroll on mobile/tablet
|
||||||
|
if (viewport.name !== 'desktop') {
|
||||||
|
const scrollWidth = await testPage.evaluate(() => document.body.scrollWidth);
|
||||||
|
const clientWidth = await testPage.evaluate(() => document.body.clientWidth);
|
||||||
|
expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 1); // Allow 1px tolerance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Phase 1: Application Discovery & Version Detection
|
||||||
|
|
||||||
|
**Purpose**: Understand the application architecture, detect framework versions, and determine optimal Playwright setup
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Detect application type and versions
|
||||||
|
- Read package.json to identify frameworks (React, Vite, Next.js, Express, etc.)
|
||||||
|
- Check for common files (vite.config.ts, next.config.js, app.js, index.html)
|
||||||
|
- Identify build tools and dev server configuration
|
||||||
|
- Extract installed package versions for version-aware configuration
|
||||||
|
|
||||||
|
### 2. Consult version compatibility database
|
||||||
|
- Load `data/framework-versions.yaml` compatibility rules
|
||||||
|
- Match installed versions against version ranges using semver
|
||||||
|
- Determine appropriate templates for each framework version
|
||||||
|
- Identify potential breaking changes or incompatibilities
|
||||||
|
- **Example**: Tailwind v4 detected → use `@import` syntax, not `@tailwind`
|
||||||
|
|
||||||
|
### 3. Validate application access
|
||||||
|
- Check if dev server is running (ports 3000, 5173, 8080, etc.)
|
||||||
|
- If not running, determine how to start it (npm run dev, npm start, etc.)
|
||||||
|
- Verify application loads successfully
|
||||||
|
|
||||||
|
### 4. Map critical user journeys
|
||||||
|
- Identify key pages/routes from routing configuration
|
||||||
|
- Detect authentication flows
|
||||||
|
- Find form submissions and interactive elements
|
||||||
|
- Locate API integrations
|
||||||
|
|
||||||
|
## Version Detection Logic
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Load compatibility database
|
||||||
|
const versionDb = parseYAML('data/framework-versions.yaml');
|
||||||
|
|
||||||
|
// Detect versions
|
||||||
|
const detectedVersions = {
|
||||||
|
tailwind: detectVersion(deps.tailwindcss, versionDb.tailwindcss),
|
||||||
|
react: detectVersion(deps.react, versionDb.react),
|
||||||
|
vite: detectVersion(deps.vite, versionDb.vite),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Select appropriate templates
|
||||||
|
const templates = {
|
||||||
|
css: detectedVersions.tailwind?.templates.css || 'templates/css/vanilla.css',
|
||||||
|
postcss: detectedVersions.tailwind?.templates.postcss_config,
|
||||||
|
playwright: 'templates/playwright.config.template.ts',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Application profile with:
|
||||||
|
- Framework type and versions
|
||||||
|
- URLs and ports
|
||||||
|
- Test targets
|
||||||
|
- Selected templates
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
**Unrecognized framework**
|
||||||
|
- Ask user to specify app type and dev server command manually
|
||||||
|
- Use generic static site configuration as fallback
|
||||||
|
|
||||||
|
**Missing package.json**
|
||||||
|
- Check for other indicators (index.html, etc.)
|
||||||
|
- Prompt user for application details
|
||||||
|
|
||||||
|
## Transition
|
||||||
|
|
||||||
|
Proceed to Phase 2 (Playwright Installation) with version-aware configuration
|
||||||
72
skills/playwright-e2e-automation/workflow/phase-2-setup.md
Normal file
72
skills/playwright-e2e-automation/workflow/phase-2-setup.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Phase 2: Playwright Installation & Setup
|
||||||
|
|
||||||
|
**Purpose**: Install Playwright and generate optimal configuration
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Install Playwright
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm init playwright@latest -- --yes
|
||||||
|
# Installs Playwright, test runners, and browsers (Chromium, Firefox, WebKit)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Generate playwright.config.ts
|
||||||
|
|
||||||
|
Configure based on app type:
|
||||||
|
- Set base URL (http://localhost:5173 for Vite, etc.)
|
||||||
|
- Configure viewport sizes:
|
||||||
|
- Desktop: 1280x720
|
||||||
|
- Tablet: 768x1024
|
||||||
|
- Mobile: 375x667
|
||||||
|
- Set screenshot directory: `screenshots/{test-name}/{timestamp}/`
|
||||||
|
- Enable trace on failure for debugging
|
||||||
|
- Configure retries (2 attempts) and timeout (30s)
|
||||||
|
|
||||||
|
### 3. Set up directory structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── setup/
|
||||||
|
│ └── global-setup.ts # Start dev server
|
||||||
|
├── pages/
|
||||||
|
│ └── *.page.ts # Page object models
|
||||||
|
├── specs/
|
||||||
|
│ └── *.spec.ts # Test specifications
|
||||||
|
└── utils/
|
||||||
|
└── screenshot-helper.ts
|
||||||
|
|
||||||
|
screenshots/
|
||||||
|
├── baselines/ # Reference images
|
||||||
|
├── current/ # Latest test run
|
||||||
|
└── diffs/ # Visual comparisons
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Integrate with existing test setup
|
||||||
|
|
||||||
|
- Add playwright scripts to package.json
|
||||||
|
- Configure alongside Vitest/Jest (no conflicts)
|
||||||
|
- Set up TypeScript types for Playwright
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Fully configured Playwright environment with version-appropriate templates
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
~2-3 minutes for installation and setup (one-time)
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
**Installation fails**
|
||||||
|
- Check Node version (>=16)
|
||||||
|
- Retry with --force
|
||||||
|
- Suggest manual installation if network issues
|
||||||
|
|
||||||
|
**Browser download fails**
|
||||||
|
- Check disk space (~500MB needed)
|
||||||
|
- Try installing specific browser: `npx playwright install chromium`
|
||||||
|
|
||||||
|
## Transition
|
||||||
|
|
||||||
|
Proceed to Phase 2.5 (Pre-flight Health Check)
|
||||||
113
skills/playwright-e2e-automation/workflow/phase-2.5-preflight.md
Normal file
113
skills/playwright-e2e-automation/workflow/phase-2.5-preflight.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Phase 2.5: Pre-flight Health Check
|
||||||
|
|
||||||
|
**Purpose**: Validate app loads correctly before running full test suite - catches configuration errors early
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Launch browser and attempt to load app
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await page.goto(baseURL, { timeout: 30000 });
|
||||||
|
|
||||||
|
if (!response || !response.ok()) {
|
||||||
|
throw new Error(`App returned ${response?.status()}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Analyze error and provide guidance
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Monitor console for critical errors
|
||||||
|
|
||||||
|
- Listen for console errors during page load
|
||||||
|
- Collect all error messages for pattern analysis
|
||||||
|
- Wait 2-3 seconds to let errors surface
|
||||||
|
|
||||||
|
### 3. Analyze errors against known patterns
|
||||||
|
|
||||||
|
Load `data/error-patterns.yaml` error database and match against known patterns:
|
||||||
|
|
||||||
|
**Example patterns detected**:
|
||||||
|
- Tailwind v4 syntax mismatch: "Cannot apply unknown utility class"
|
||||||
|
- PostCSS plugin error: "Plugin tailwindcss not found"
|
||||||
|
- Missing dependencies: "Module not found"
|
||||||
|
|
||||||
|
### 4. Provide actionable diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Pre-flight check failed: Critical errors detected
|
||||||
|
|
||||||
|
Issue: Tailwind CSS v4 syntax mismatch
|
||||||
|
Root cause: CSS file uses @tailwind directives but v4 requires @import
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
1. Update src/index.css (or globals.css):
|
||||||
|
Change from: @tailwind base; @tailwind components; @tailwind utilities;
|
||||||
|
Change to: @import "tailwindcss";
|
||||||
|
|
||||||
|
2. Update postcss.config.js:
|
||||||
|
Change from: plugins: { tailwindcss: {} }
|
||||||
|
Change to: plugins: { '@tailwindcss/postcss': {} }
|
||||||
|
|
||||||
|
3. Restart dev server: npm run dev
|
||||||
|
|
||||||
|
Documentation: https://tailwindcss.com/docs/upgrade-guide
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Auto-fix if possible, otherwise halt with guidance
|
||||||
|
|
||||||
|
- For known issues with clear fixes, offer to fix automatically
|
||||||
|
- For ambiguous issues, halt and require user intervention
|
||||||
|
- Prevent running 10+ tests that will all fail due to one config issue
|
||||||
|
|
||||||
|
## Error Pattern Analysis
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function analyzeErrors(consoleErrors) {
|
||||||
|
const errorPatterns = parseYAML('data/error-patterns.yaml');
|
||||||
|
const issues = [];
|
||||||
|
|
||||||
|
for (const error of consoleErrors) {
|
||||||
|
for (const [name, pattern] of Object.entries(errorPatterns.css_errors)) {
|
||||||
|
if (pattern.pattern.test(error) ||
|
||||||
|
pattern.alternative_patterns?.some(alt => alt.test(error))) {
|
||||||
|
issues.push({
|
||||||
|
name,
|
||||||
|
severity: pattern.severity,
|
||||||
|
diagnosis: pattern.diagnosis,
|
||||||
|
recovery_steps: pattern.recovery_steps,
|
||||||
|
documentation: pattern.documentation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
critical: issues.filter(i => i.severity === 'critical'),
|
||||||
|
allIssues: issues,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
- **Fast feedback**: 2-3 seconds vs 30+ seconds for full test suite
|
||||||
|
- **Clear guidance**: Specific fix steps, not generic "tests failed"
|
||||||
|
- **Prevents cascade failures**: One config error won't fail all 10 tests
|
||||||
|
- **Educational**: Explains what went wrong and why
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Health check passed, or detailed error diagnostics with fix steps
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
~2-5 seconds
|
||||||
|
|
||||||
|
## Transition
|
||||||
|
|
||||||
|
If health check passes, proceed to Phase 3 (Test Generation). If fails, provide fixes and halt.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Phase 3: Test Generation
|
||||||
|
|
||||||
|
**Purpose**: Create screenshot-enabled test suite covering critical workflows
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Generate page object models
|
||||||
|
|
||||||
|
- Create POM classes for each major page/component
|
||||||
|
- Define locators using best practices (getByRole, getByLabel, getByText)
|
||||||
|
- Add screenshot capture methods to each POM
|
||||||
|
|
||||||
|
### 2. Create test specifications
|
||||||
|
|
||||||
|
Generate tests for each critical user journey with screenshot capture at key points:
|
||||||
|
- Initial page load
|
||||||
|
- Before interaction (button click, form fill)
|
||||||
|
- After interaction
|
||||||
|
- Error states
|
||||||
|
- Success states
|
||||||
|
|
||||||
|
### 3. Add accessibility checks
|
||||||
|
|
||||||
|
- Integrate axe-core for automated a11y testing
|
||||||
|
- Capture accessibility violations in screenshots
|
||||||
|
- Generate accessibility reports
|
||||||
|
|
||||||
|
### 4. Set up screenshot helpers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// templates/screenshot-helper.ts
|
||||||
|
export async function captureWithContext(
|
||||||
|
page: Page,
|
||||||
|
name: string,
|
||||||
|
context?: string
|
||||||
|
) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const path = `screenshots/current/${name}-${timestamp}.png`;
|
||||||
|
await page.screenshot({ path, fullPage: true });
|
||||||
|
return { path, context, timestamp };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Complete test suite with screenshot automation
|
||||||
|
|
||||||
|
## Test Coverage
|
||||||
|
|
||||||
|
Aim for critical user journeys (80/20 rule):
|
||||||
|
- Core functionality tests
|
||||||
|
- Authentication flows
|
||||||
|
- Form submissions
|
||||||
|
- Key interactions
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
**Too many tests generated**
|
||||||
|
- Focus on critical paths
|
||||||
|
- Prioritize user journeys over edge cases
|
||||||
|
|
||||||
|
**Locators not found**
|
||||||
|
- Use semantic locators (getByRole, getByLabel)
|
||||||
|
- Add test IDs as last resort
|
||||||
|
|
||||||
|
## Transition
|
||||||
|
|
||||||
|
Proceed to Phase 4 (Screenshot Capture & Execution)
|
||||||
65
skills/playwright-e2e-automation/workflow/phase-4-capture.md
Normal file
65
skills/playwright-e2e-automation/workflow/phase-4-capture.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Phase 4: Screenshot Capture & Execution
|
||||||
|
|
||||||
|
**Purpose**: Run tests and capture comprehensive visual data
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Execute test suite
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx playwright test --project=chromium --headed=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Capture screenshots systematically
|
||||||
|
|
||||||
|
- Full-page screenshots for layout analysis
|
||||||
|
- Element-specific screenshots for component testing
|
||||||
|
- Different viewports (desktop, tablet, mobile)
|
||||||
|
- Different states (hover, focus, active, disabled)
|
||||||
|
|
||||||
|
### 3. Organize screenshot artifacts
|
||||||
|
|
||||||
|
- Group by test name
|
||||||
|
- Add timestamp and viewport metadata
|
||||||
|
- Generate index file for easy navigation
|
||||||
|
|
||||||
|
### 4. Handle failures gracefully
|
||||||
|
|
||||||
|
On test failure:
|
||||||
|
- Capture additional debug screenshots
|
||||||
|
- Save page HTML snapshot
|
||||||
|
- Record network activity
|
||||||
|
- Generate Playwright trace for replay
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Organized screenshot directory with metadata:
|
||||||
|
|
||||||
|
```
|
||||||
|
screenshots/
|
||||||
|
├── current/
|
||||||
|
│ ├── home-page-load-2024-01-15T10-30-00.png
|
||||||
|
│ ├── home-page-after-click-2024-01-15T10-30-05.png
|
||||||
|
│ └── index.json (metadata)
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
~30-60 seconds for typical app (5-10 tests)
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
**Screenshot capture fails**
|
||||||
|
- Increase timeout
|
||||||
|
- Add explicit waits
|
||||||
|
- Capture partial screenshot on failure
|
||||||
|
|
||||||
|
**Tests timeout**
|
||||||
|
- Check dev server is running
|
||||||
|
- Increase test timeout
|
||||||
|
- Add explicit wait conditions
|
||||||
|
|
||||||
|
## Transition
|
||||||
|
|
||||||
|
Proceed to Phase 5 (Visual Analysis)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Phase 5: Visual Analysis
|
||||||
|
|
||||||
|
**Purpose**: Use LLM vision capabilities to analyze screenshots and identify issues
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Batch screenshot analysis
|
||||||
|
|
||||||
|
Read all captured screenshots and ask LLM to identify:
|
||||||
|
- UI bugs (broken layouts, overlapping elements, cut-off text)
|
||||||
|
- Accessibility issues (low contrast, missing labels, improper heading hierarchy)
|
||||||
|
- Responsive problems (elements not scaling, overflow issues)
|
||||||
|
- Missing or misaligned elements
|
||||||
|
- Unexpected visual artifacts
|
||||||
|
|
||||||
|
### 2. Categorize findings
|
||||||
|
|
||||||
|
- **Critical**: App is broken/unusable (crashes, white screen, no content)
|
||||||
|
- **High**: Major UI bugs affecting core functionality
|
||||||
|
- **Medium**: Visual inconsistencies that impact UX
|
||||||
|
- **Low**: Minor alignment or styling issues
|
||||||
|
|
||||||
|
### 3. Generate issue descriptions
|
||||||
|
|
||||||
|
For each issue:
|
||||||
|
- Natural language description
|
||||||
|
- Screenshot reference with highlighted problem area
|
||||||
|
- Affected viewport/browser if relevant
|
||||||
|
- User impact assessment
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Structured list of visual issues with severity ratings:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Visual Issues Found
|
||||||
|
|
||||||
|
### Critical (1)
|
||||||
|
- White screen on mobile viewport - App fails to render
|
||||||
|
|
||||||
|
### High (2)
|
||||||
|
- Button text cut off at 375px width
|
||||||
|
- Form labels overlap input fields
|
||||||
|
|
||||||
|
### Medium (3)
|
||||||
|
- Header alignment inconsistent
|
||||||
|
- ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
~5-10 seconds per screenshot for LLM analysis
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
**Analysis fails**
|
||||||
|
- Retry analysis
|
||||||
|
- Skip corrupted images
|
||||||
|
- Validate PNG format
|
||||||
|
|
||||||
|
**Too many false positives**
|
||||||
|
- Adjust analysis prompts
|
||||||
|
- Focus on critical issues first
|
||||||
|
|
||||||
|
## Transition
|
||||||
|
|
||||||
|
Proceed to Phase 6 (Regression Detection)
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Phase 6: Regression Detection
|
||||||
|
|
||||||
|
**Purpose**: Compare current screenshots against baselines to detect changes
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Load baseline images
|
||||||
|
|
||||||
|
- Check if baselines exist in screenshots/baselines/
|
||||||
|
- If first run, current screenshots become baselines
|
||||||
|
- If baselines exist, proceed to comparison
|
||||||
|
|
||||||
|
### 2. Perform pixel-level comparison
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { compareScreenshots } from 'playwright-core/lib/utils';
|
||||||
|
|
||||||
|
const diff = await compareScreenshots(
|
||||||
|
baselinePath,
|
||||||
|
currentPath,
|
||||||
|
diffPath,
|
||||||
|
{ threshold: 0.2 } // 20% difference threshold
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Generate visual diff reports
|
||||||
|
|
||||||
|
- Create side-by-side comparison images
|
||||||
|
- Highlight changed regions in red
|
||||||
|
- Calculate difference percentage
|
||||||
|
- Classify changes:
|
||||||
|
- **Expected**: Intentional changes (new features, fixes)
|
||||||
|
- **Suspicious**: Unintended changes requiring review
|
||||||
|
- **Critical**: Major regressions (broken features)
|
||||||
|
|
||||||
|
### 4. Update baselines if approved
|
||||||
|
|
||||||
|
Ask user: "Accept these changes as new baseline?"
|
||||||
|
- If yes, copy current → baselines
|
||||||
|
- If no, flag as regressions needing fixes
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Visual regression report with diff images:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Regression Report
|
||||||
|
|
||||||
|
### Changed Screenshots (3)
|
||||||
|
|
||||||
|
| Screenshot | Diff % | Classification |
|
||||||
|
|------------|--------|----------------|
|
||||||
|
| home-page | 5% | Expected |
|
||||||
|
| form-page | 25% | Suspicious |
|
||||||
|
| mobile-nav | 45% | Critical |
|
||||||
|
|
||||||
|
See screenshots/diffs/ for visual comparisons.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
~1-2 seconds per image pair
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
**No baselines exist**
|
||||||
|
- Current screenshots become baselines automatically
|
||||||
|
- Message: "No baselines found. Current screenshots saved as baselines."
|
||||||
|
|
||||||
|
**False positive diffs**
|
||||||
|
- Adjust threshold (default 20%)
|
||||||
|
- Ignore dynamic content areas
|
||||||
|
- Use stable test data
|
||||||
|
|
||||||
|
## Transition
|
||||||
|
|
||||||
|
Proceed to Phase 7 (Fix Recommendation Generation)
|
||||||
72
skills/playwright-e2e-automation/workflow/phase-7-fixes.md
Normal file
72
skills/playwright-e2e-automation/workflow/phase-7-fixes.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Phase 7: Fix Recommendation Generation
|
||||||
|
|
||||||
|
**Purpose**: Map visual issues to source code and generate actionable fixes
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Correlate issues with source code
|
||||||
|
|
||||||
|
- Use test file metadata to identify component under test
|
||||||
|
- Search codebase for relevant files (component, styles, layout)
|
||||||
|
- Match visual issues to likely code locations
|
||||||
|
|
||||||
|
### 2. Generate fix recommendations
|
||||||
|
|
||||||
|
For each issue, provide:
|
||||||
|
- **Issue description**: Natural language explanation
|
||||||
|
- **File location**: `src/components/Button.tsx:45`
|
||||||
|
- **Current code**: Snippet showing problematic code
|
||||||
|
- **Recommended fix**: Specific code change
|
||||||
|
- **Reasoning**: Why this fix addresses the issue
|
||||||
|
|
||||||
|
### 3. Prioritize fixes
|
||||||
|
|
||||||
|
- Sort by severity (critical → low)
|
||||||
|
- Group related fixes (same component, same file)
|
||||||
|
- Estimate complexity (simple CSS tweak vs. complex refactor)
|
||||||
|
|
||||||
|
### 4. Format as actionable report
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Visual Bug Fix Recommendations
|
||||||
|
|
||||||
|
## Critical Issues (2)
|
||||||
|
|
||||||
|
### 1. Button text cut off on mobile viewport
|
||||||
|
**Location**: `src/components/Button.tsx:45`
|
||||||
|
**Screenshot**: `screenshots/current/button-mobile-1234.png`
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```tsx
|
||||||
|
<button className="px-4 py-2 text-lg">
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Fix**:
|
||||||
|
```tsx
|
||||||
|
<button className="px-4 py-2 text-sm sm:text-lg truncate max-w-full">
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reasoning**: Fixed width and font size cause overflow on narrow viewports. Added responsive text sizing and truncation.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
fix-recommendations.md with prioritized, actionable fixes
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
**Can't find source file**
|
||||||
|
- Ask user for component location
|
||||||
|
- Search by component name patterns
|
||||||
|
|
||||||
|
**Multiple possible fixes**
|
||||||
|
- Present options with trade-offs
|
||||||
|
- Recommend simplest solution
|
||||||
|
|
||||||
|
## Transition
|
||||||
|
|
||||||
|
Proceed to Phase 8 (Test Suite Export)
|
||||||
82
skills/playwright-e2e-automation/workflow/phase-8-export.md
Normal file
82
skills/playwright-e2e-automation/workflow/phase-8-export.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Phase 8: Test Suite Export
|
||||||
|
|
||||||
|
**Purpose**: Provide production-ready test suite for ongoing use
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Export test files
|
||||||
|
|
||||||
|
- Copy generated tests to project's tests/ directory
|
||||||
|
- Ensure proper TypeScript types and imports
|
||||||
|
- Add comments explaining test purpose
|
||||||
|
|
||||||
|
### 2. Create README documentation
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Playwright E2E Test Suite
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
```bash
|
||||||
|
npm run test:e2e # Run all e2e tests
|
||||||
|
npm run test:e2e:headed # Run with browser UI
|
||||||
|
npm run test:e2e:debug # Run with Playwright Inspector
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screenshot Management
|
||||||
|
- Baselines: `screenshots/baselines/`
|
||||||
|
- Current: `screenshots/current/`
|
||||||
|
- Diffs: `screenshots/diffs/`
|
||||||
|
|
||||||
|
## Updating Baselines
|
||||||
|
```bash
|
||||||
|
npm run test:e2e:update-snapshots
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add npm scripts
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:headed": "playwright test --headed",
|
||||||
|
"test:e2e:debug": "playwright test --debug",
|
||||||
|
"test:e2e:update-snapshots": "playwright test --update-snapshots"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Document CI/CD integration
|
||||||
|
|
||||||
|
- Provide GitHub Actions workflow example
|
||||||
|
- Explain screenshot artifact storage
|
||||||
|
- Show how to update baselines in CI
|
||||||
|
- Configure Playwright HTML reporter for CI
|
||||||
|
|
||||||
|
See `reference/ci-cd-integration.md` for complete examples.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Complete, documented test suite ready for development workflow:
|
||||||
|
- Tests in `tests/` directory
|
||||||
|
- README with usage instructions
|
||||||
|
- npm scripts configured
|
||||||
|
- CI/CD documentation
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
**Tests don't run after export**
|
||||||
|
- Check TypeScript types
|
||||||
|
- Verify imports are correct
|
||||||
|
- Ensure Playwright is in dependencies
|
||||||
|
|
||||||
|
**CI/CD integration issues**
|
||||||
|
- See `reference/ci-cd-integration.md`
|
||||||
|
- Check browser installation in CI
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] Test suite exported to project
|
||||||
|
- [ ] All tests executable via npm run test:e2e
|
||||||
|
- [ ] README includes usage instructions
|
||||||
|
- [ ] CI/CD guidance documented
|
||||||
14
skills/tdd-automation/CHANGELOG.md
Normal file
14
skills/tdd-automation/CHANGELOG.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
- Refactored to Anthropic progressive disclosure pattern
|
||||||
|
- Updated description with "Use PROACTIVELY when..." format
|
||||||
|
- Removed version/author/category/tags from frontmatter
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
- Initial release with RED-GREEN-REFACTOR cycle enforcement
|
||||||
|
- Automated TDD workflow with git hooks and npm scripts
|
||||||
|
- CPU usage prevention safeguards
|
||||||
|
- Integration with Testing Trophy methodology
|
||||||
340
skills/tdd-automation/README.md
Normal file
340
skills/tdd-automation/README.md
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
# TDD Red-Green-Refactor Automation
|
||||||
|
|
||||||
|
Automated TDD enforcement for LLM-assisted development. This skill installs infrastructure that guides Claude Code to automatically follow TDD workflow without manual intervention.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
**0.2.0** - Stable release with comprehensive safety features
|
||||||
|
|
||||||
|
## What This Skill Does
|
||||||
|
|
||||||
|
Transforms your project so that when you ask Claude to implement features, it **automatically**:
|
||||||
|
|
||||||
|
1. 🔴 **RED Phase** - Writes failing tests FIRST
|
||||||
|
2. 🟢 **GREEN Phase** - Implements minimal code to pass tests
|
||||||
|
3. 🔵 **REFACTOR Phase** - Improves code quality while keeping tests green
|
||||||
|
|
||||||
|
**No slash commands. No manual prompting. Just say "implement X" and TDD happens.**
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From claudex marketplace
|
||||||
|
/plugin install tdd-automation
|
||||||
|
|
||||||
|
# Or directly invoke the skill
|
||||||
|
tdd-automation
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Gets Installed
|
||||||
|
|
||||||
|
### 1. CLAUDE.md Configuration
|
||||||
|
- Adds TDD requirements to project's `.claude/CLAUDE.md`
|
||||||
|
- **Safety**: Backs up existing files, merges cleanly, fully reversible
|
||||||
|
- Configures LLM to automatically follow TDD workflow
|
||||||
|
|
||||||
|
### 2. npm Scripts
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"test:tdd": "vitest --run",
|
||||||
|
"test:tdd:watch": "vitest",
|
||||||
|
"test:red": "vitest --run --reporter=verbose",
|
||||||
|
"test:green": "vitest --run --reporter=verbose",
|
||||||
|
"validate:tdd": "node .tdd-automation/scripts/validate-tdd.js",
|
||||||
|
"generate:test": "node .tdd-automation/scripts/generate-test.js"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Git Hooks
|
||||||
|
- **Pre-commit hook**: Validates tests exist before allowing commits
|
||||||
|
- Prevents TDD violations at the version control level
|
||||||
|
- Can be bypassed with `--no-verify` if needed
|
||||||
|
|
||||||
|
### 4. Helper Scripts
|
||||||
|
```
|
||||||
|
.tdd-automation/
|
||||||
|
├── scripts/
|
||||||
|
│ ├── generate-test.js # Create test templates
|
||||||
|
│ ├── validate-tdd.js # Check compliance
|
||||||
|
│ ├── rollback-tdd.js # Restore previous state
|
||||||
|
│ └── remove-tdd-section.js # Clean uninstall
|
||||||
|
└── templates/
|
||||||
|
└── test-template.js # Test file template
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Before Installation
|
||||||
|
```
|
||||||
|
You: "Add user authentication"
|
||||||
|
Claude: *writes implementation code directly* ❌
|
||||||
|
```
|
||||||
|
|
||||||
|
### After Installation
|
||||||
|
```
|
||||||
|
You: "Add user authentication"
|
||||||
|
|
||||||
|
Claude: "I'll implement user authentication following TDD red-green-refactor.
|
||||||
|
|
||||||
|
Let me create a todo list to track the phases:
|
||||||
|
- [ ] RED: Write failing test for user authentication
|
||||||
|
- [ ] Verify test fails with expected error
|
||||||
|
- [ ] GREEN: Implement minimal authentication logic
|
||||||
|
- [ ] Verify test passes
|
||||||
|
- [ ] REFACTOR: Improve code quality
|
||||||
|
|
||||||
|
Starting RED phase - I'll create the test file first..."
|
||||||
|
|
||||||
|
*Creates src/auth/authenticate.test.ts*
|
||||||
|
*Writes failing test*
|
||||||
|
*Runs: npm run test:red*
|
||||||
|
*Verifies failure*
|
||||||
|
|
||||||
|
"Test is failing as expected. Moving to GREEN phase..."
|
||||||
|
|
||||||
|
*Implements minimal code*
|
||||||
|
*Runs: npm run test:green*
|
||||||
|
*Verifies pass*
|
||||||
|
|
||||||
|
"All tests passing. Moving to REFACTOR phase..."
|
||||||
|
|
||||||
|
*Improves code structure*
|
||||||
|
*Runs: npm run test:tdd*
|
||||||
|
|
||||||
|
"✅ Feature complete with TDD workflow followed."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
```bash
|
||||||
|
# Check if project follows TDD practices
|
||||||
|
npm run validate:tdd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Generation
|
||||||
|
```bash
|
||||||
|
# Generate test template for implementation file
|
||||||
|
npm run generate:test src/features/auth/login.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
```bash
|
||||||
|
# Rollback to previous CLAUDE.md
|
||||||
|
node .tdd-automation/scripts/rollback-tdd.js
|
||||||
|
|
||||||
|
# Remove only TDD section from CLAUDE.md
|
||||||
|
node .tdd-automation/scripts/remove-tdd-section.js
|
||||||
|
|
||||||
|
# View available backups
|
||||||
|
ls -lh .claude/CLAUDE.md.backup.*
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety Features
|
||||||
|
|
||||||
|
### ✅ Non-Destructive Installation
|
||||||
|
- **Automatic backups** before any file modification
|
||||||
|
- **Merge strategy** preserves all existing content
|
||||||
|
- **Clear markers** for easy identification of additions
|
||||||
|
- **Rollback capability** to restore previous state
|
||||||
|
|
||||||
|
### ✅ Validation Before Installation
|
||||||
|
- Detects existing TDD automation (skips if present)
|
||||||
|
- Checks for existing CLAUDE.md (merges safely)
|
||||||
|
- Validates project structure
|
||||||
|
- Reports warnings for missing dependencies
|
||||||
|
|
||||||
|
### ✅ Clean Uninstallation
|
||||||
|
- Remove TDD section without affecting other content
|
||||||
|
- Restore from timestamped backups
|
||||||
|
- No orphaned files or configurations
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Enforcement Level
|
||||||
|
|
||||||
|
By default, TDD is **strictly enforced**. To customize:
|
||||||
|
|
||||||
|
Edit `.claude/CLAUDE.md` and modify:
|
||||||
|
```yaml
|
||||||
|
tdd-enforcement-level: strict # or: advisory, optional
|
||||||
|
```
|
||||||
|
|
||||||
|
- **strict**: LLM must follow TDD (default)
|
||||||
|
- **advisory**: LLM suggests TDD but allows flexibility
|
||||||
|
- **optional**: TDD is available but not required
|
||||||
|
|
||||||
|
### Test Framework
|
||||||
|
|
||||||
|
The skill auto-detects your test framework:
|
||||||
|
- Vitest (default, recommended)
|
||||||
|
- Jest
|
||||||
|
- Mocha
|
||||||
|
- AVA
|
||||||
|
|
||||||
|
To override, edit `.tdd-automation/config.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"testCommand": "jest",
|
||||||
|
"testExtension": ".test.ts"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Issue: LLM not following TDD
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Check CLAUDE.md has TDD configuration:
|
||||||
|
```bash
|
||||||
|
grep -A 5 "TDD_AUTOMATION" .claude/CLAUDE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Verify npm scripts installed:
|
||||||
|
```bash
|
||||||
|
npm run test:tdd
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Validate installation:
|
||||||
|
```bash
|
||||||
|
npm run validate:tdd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Git hook blocking commits
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Ensure tests exist for implementation files
|
||||||
|
2. Commit tests before implementation:
|
||||||
|
```bash
|
||||||
|
git add src/**/*.test.ts
|
||||||
|
git commit -m "Add tests for feature X"
|
||||||
|
git add src/**/!(*test).ts
|
||||||
|
git commit -m "Implement feature X"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Or bypass hook temporarily:
|
||||||
|
```bash
|
||||||
|
git commit --no-verify
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Want to remove TDD automation
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# Option 1: Remove only CLAUDE.md section
|
||||||
|
node .tdd-automation/scripts/remove-tdd-section.js
|
||||||
|
|
||||||
|
# Option 2: Rollback to previous CLAUDE.md
|
||||||
|
node .tdd-automation/scripts/rollback-tdd.js
|
||||||
|
|
||||||
|
# Option 3: Manual removal
|
||||||
|
# 1. Edit .claude/CLAUDE.md
|
||||||
|
# 2. Delete section between <!-- TDD_AUTOMATION_START --> and <!-- TDD_AUTOMATION_END -->
|
||||||
|
# 3. Remove .tdd-automation/ directory
|
||||||
|
# 4. Remove npm scripts from package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ User Request │
|
||||||
|
│ "Implement feature X" │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ CLAUDE.md (LLM Configuration) │
|
||||||
|
│ • TDD workflow requirements │
|
||||||
|
│ • Phase-by-phase instructions │
|
||||||
|
│ • TodoWrite templates │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Claude Code (LLM) │
|
||||||
|
│ 1. Creates test file │
|
||||||
|
│ 2. Writes failing test (RED) │
|
||||||
|
│ 3. Runs: npm run test:red │
|
||||||
|
│ 4. Implements code (GREEN) │
|
||||||
|
│ 5. Runs: npm run test:green │
|
||||||
|
│ 6. Refactors (REFACTOR) │
|
||||||
|
│ 7. Runs: npm run test:tdd │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Git Pre-Commit Hook │
|
||||||
|
│ • Validates tests exist │
|
||||||
|
│ • Blocks commits without tests │
|
||||||
|
│ • Ensures test-first compliance │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
project/
|
||||||
|
├── .claude/
|
||||||
|
│ ├── CLAUDE.md # TDD configuration (modified)
|
||||||
|
│ ├── CLAUDE.md.backup.* # Safety backups
|
||||||
|
│ └── hooks/
|
||||||
|
│ └── tdd-auto-enforcer.sh # Pre-prompt hook (optional)
|
||||||
|
├── .git/
|
||||||
|
│ └── hooks/
|
||||||
|
│ └── pre-commit # TDD validation (modified)
|
||||||
|
├── .tdd-automation/ # Installed by skill
|
||||||
|
│ ├── scripts/
|
||||||
|
│ │ ├── generate-test.js
|
||||||
|
│ │ ├── validate-tdd.js
|
||||||
|
│ │ ├── rollback-tdd.js
|
||||||
|
│ │ └── remove-tdd-section.js
|
||||||
|
│ ├── templates/
|
||||||
|
│ │ └── test-template.js
|
||||||
|
│ └── README.md
|
||||||
|
└── package.json # npm scripts added
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
Track TDD adherence:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run validate:tdd
|
||||||
|
```
|
||||||
|
|
||||||
|
**Target Metrics:**
|
||||||
|
- Test-first adherence: >95%
|
||||||
|
- RED-GREEN-REFACTOR cycles: >90%
|
||||||
|
- Defect escape rate: <2%
|
||||||
|
- Test coverage: >80%
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
This skill was auto-generated by the Pattern Suggestion Pipeline from 37 successful TDD applications with 86% success rate.
|
||||||
|
|
||||||
|
**Issues or improvements:**
|
||||||
|
- GitHub: https://github.com/cskiro/claudex/issues
|
||||||
|
- Pattern source: Issue #13
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **0.2.0** (2025-11-02)
|
||||||
|
- Stable release with full implementation
|
||||||
|
- Comprehensive safety features (backup, rollback, merge)
|
||||||
|
- Automatic LLM TDD enforcement via CLAUDE.md
|
||||||
|
- Git hooks for pre-commit validation
|
||||||
|
- Helper scripts (generate-test, validate-tdd)
|
||||||
|
- Multi-framework support (Vitest, Jest, Mocha, AVA)
|
||||||
|
- Complete documentation and troubleshooting guide
|
||||||
|
|
||||||
|
- **0.1.0** (2025-11-02)
|
||||||
|
- Initial proof-of-concept
|
||||||
|
- Auto-generated by Pattern Pipeline
|
||||||
|
- Basic skill structure and manifest
|
||||||
235
skills/tdd-automation/SKILL.md
Normal file
235
skills/tdd-automation/SKILL.md
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
---
|
||||||
|
name: tdd-automation
|
||||||
|
description: Use PROACTIVELY when starting new projects requiring strict TDD adherence, or when user wants to enforce test-first workflow. Automates red-green-refactor cycle for LLM-assisted development with git hooks, npm scripts, and CLAUDE.md configuration. Not for prototypes or projects without test requirements.
|
||||||
|
---
|
||||||
|
|
||||||
|
# TDD Red-Green-Refactor Automation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This skill transforms any project to automatically enforce TDD (Test-Driven Development) workflow when using LLMs for code generation. Once installed, Claude Code will automatically follow the red-green-refactor cycle without manual prompting.
|
||||||
|
|
||||||
|
**Version:** 0.2.0
|
||||||
|
**Status:** Stable
|
||||||
|
**Success Rate:** 86% (based on 37 pattern applications)
|
||||||
|
**Source:** Pattern Suggestion Pipeline (Issue #13)
|
||||||
|
|
||||||
|
## What This Skill Does
|
||||||
|
|
||||||
|
**Automatic TDD Enforcement** - When you ask Claude to implement a feature, it will:
|
||||||
|
|
||||||
|
1. 🔴 **RED Phase**: Write failing test FIRST
|
||||||
|
2. 🟢 **GREEN Phase**: Implement minimal code to pass test
|
||||||
|
3. 🔵 **REFACTOR Phase**: Improve code quality while keeping tests green
|
||||||
|
|
||||||
|
**No manual steps required** - The LLM is automatically configured through CLAUDE.md to follow this workflow.
|
||||||
|
|
||||||
|
## Installation Process
|
||||||
|
|
||||||
|
When you invoke this skill, it will:
|
||||||
|
|
||||||
|
1. **Detect project configuration**
|
||||||
|
- Identify test framework (Vitest, Jest, Mocha, AVA)
|
||||||
|
- Check for TypeScript/JavaScript
|
||||||
|
- Validate git repository exists
|
||||||
|
|
||||||
|
2. **Configure CLAUDE.md safely**
|
||||||
|
- Create automatic backup before any changes
|
||||||
|
- Merge TDD requirements (preserves existing content)
|
||||||
|
- Add clear section markers for identification
|
||||||
|
|
||||||
|
3. **Install npm scripts**
|
||||||
|
- `test:tdd` - Run all tests once
|
||||||
|
- `test:tdd:watch` - Run tests in watch mode
|
||||||
|
- `test:red` - Verify test fails (RED phase)
|
||||||
|
- `test:green` - Verify test passes (GREEN phase)
|
||||||
|
- `validate:tdd` - Check TDD compliance
|
||||||
|
- `generate:test` - Create test template
|
||||||
|
|
||||||
|
4. **Install git hooks**
|
||||||
|
- Pre-commit hook validates tests exist
|
||||||
|
- Prevents commits without tests
|
||||||
|
- Enforces test-first workflow
|
||||||
|
|
||||||
|
5. **Create helper scripts**
|
||||||
|
- Test template generator
|
||||||
|
- TDD compliance validator
|
||||||
|
- Rollback utility
|
||||||
|
- Section removal utility
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
**Use this skill when:**
|
||||||
|
- Starting a new project that requires strict TDD adherence
|
||||||
|
- Migrating existing project to TDD workflow
|
||||||
|
- Training team members on TDD practices
|
||||||
|
- Ensuring consistent quality in LLM-generated code
|
||||||
|
- Working on mission-critical code that needs high test coverage
|
||||||
|
|
||||||
|
**Don't use this skill if:**
|
||||||
|
- Project doesn't have or need tests
|
||||||
|
- Working on prototypes or experiments
|
||||||
|
- TDD workflow conflicts with existing practices
|
||||||
|
|
||||||
|
## Trigger Phrases
|
||||||
|
|
||||||
|
The skill can be invoked by:
|
||||||
|
- Direct invocation: `tdd-automation`
|
||||||
|
- Natural language: "Set up TDD automation"
|
||||||
|
- Natural language: "Configure automatic TDD workflow"
|
||||||
|
- Natural language: "Install TDD enforcement"
|
||||||
|
|
||||||
|
## Behavior After Installation
|
||||||
|
|
||||||
|
Once installed, when you say something like:
|
||||||
|
```
|
||||||
|
"Implement user authentication"
|
||||||
|
```
|
||||||
|
|
||||||
|
Claude will **automatically**:
|
||||||
|
```
|
||||||
|
1. Create todo list with TDD phases:
|
||||||
|
- [ ] RED: Write failing test for user authentication
|
||||||
|
- [ ] Verify test fails with expected error
|
||||||
|
- [ ] GREEN: Implement minimal authentication logic
|
||||||
|
- [ ] Verify test passes
|
||||||
|
- [ ] REFACTOR: Improve code quality
|
||||||
|
- [ ] Verify all tests still pass
|
||||||
|
|
||||||
|
2. Create test file FIRST:
|
||||||
|
src/auth/authenticate.test.ts
|
||||||
|
|
||||||
|
3. Write failing test with clear description
|
||||||
|
|
||||||
|
4. Run test and verify RED state:
|
||||||
|
npm run test:red -- src/auth/authenticate.test.ts
|
||||||
|
|
||||||
|
5. Implement minimal code:
|
||||||
|
src/auth/authenticate.ts
|
||||||
|
|
||||||
|
6. Run test and verify GREEN state:
|
||||||
|
npm run test:green -- src/auth/authenticate.test.ts
|
||||||
|
|
||||||
|
7. Refactor if needed while keeping tests green
|
||||||
|
|
||||||
|
8. Final validation:
|
||||||
|
npm run test:tdd
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety Features
|
||||||
|
|
||||||
|
### Non-Destructive Installation
|
||||||
|
- Automatic backups before file modifications
|
||||||
|
- Merge strategy preserves all existing CLAUDE.md content
|
||||||
|
- Clear markers (`<!-- TDD_AUTOMATION_START/END -->`) for identification
|
||||||
|
- Rollback capability to restore previous state
|
||||||
|
|
||||||
|
### Validation Before Installation
|
||||||
|
- Detects if TDD automation already installed (skips if present)
|
||||||
|
- Checks for existing CLAUDE.md (merges safely)
|
||||||
|
- Validates project structure
|
||||||
|
- Reports warnings for missing dependencies
|
||||||
|
|
||||||
|
### Clean Uninstallation
|
||||||
|
- Remove only TDD section without affecting other content
|
||||||
|
- Restore from timestamped backups
|
||||||
|
- Utility scripts for easy maintenance
|
||||||
|
|
||||||
|
## Response Style
|
||||||
|
|
||||||
|
After installation, this skill guides Claude Code to:
|
||||||
|
|
||||||
|
- **Be explicit**: Always state which TDD phase is active
|
||||||
|
- **Be thorough**: Verify each phase completion before proceeding
|
||||||
|
- **Be traceable**: Use TodoWrite to track progress
|
||||||
|
- **Be safe**: Run tests to confirm RED/GREEN states
|
||||||
|
- **Be helpful**: Provide clear error messages if TDD is violated
|
||||||
|
|
||||||
|
## Files Modified/Created
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
- `.claude/CLAUDE.md` - TDD workflow configuration added (backed up)
|
||||||
|
- `package.json` - npm scripts added (backed up)
|
||||||
|
- `.git/hooks/pre-commit` - TDD validation added (backed up if exists)
|
||||||
|
|
||||||
|
### Created Files
|
||||||
|
```
|
||||||
|
.tdd-automation/
|
||||||
|
├── scripts/
|
||||||
|
│ ├── generate-test.js # Test template generator
|
||||||
|
│ ├── validate-tdd.js # Compliance checker
|
||||||
|
│ ├── rollback-tdd.js # Restore previous state
|
||||||
|
│ └── remove-tdd-section.js # Clean uninstall
|
||||||
|
├── templates/
|
||||||
|
│ └── test-template.js # Test file template
|
||||||
|
└── README.md # Usage documentation
|
||||||
|
|
||||||
|
.claude/
|
||||||
|
├── CLAUDE.md.backup.* # Timestamped backups
|
||||||
|
└── hooks/
|
||||||
|
└── tdd-auto-enforcer.sh # Pre-prompt hook (optional)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
Installation succeeds when:
|
||||||
|
1. ✅ CLAUDE.md contains TDD configuration
|
||||||
|
2. ✅ npm scripts added to package.json
|
||||||
|
3. ✅ Git pre-commit hook installed (if git repo exists)
|
||||||
|
4. ✅ Helper scripts created in .tdd-automation/
|
||||||
|
5. ✅ Backups created for all modified files
|
||||||
|
6. ✅ Validation passes: `npm run validate:tdd`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Issue: LLM not following TDD
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
```bash
|
||||||
|
# Verify CLAUDE.md has TDD configuration
|
||||||
|
grep "TDD_AUTOMATION" .claude/CLAUDE.md
|
||||||
|
|
||||||
|
# Verify npm scripts
|
||||||
|
npm run test:tdd
|
||||||
|
|
||||||
|
# Run validation
|
||||||
|
npm run validate:tdd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Git hook blocking commits
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Ensure tests exist for implementation files
|
||||||
|
2. Commit tests before implementation
|
||||||
|
3. Or bypass temporarily: `git commit --no-verify`
|
||||||
|
|
||||||
|
### Issue: Want to remove TDD automation
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
```bash
|
||||||
|
# Option 1: Remove CLAUDE.md section only
|
||||||
|
node .tdd-automation/scripts/remove-tdd-section.js
|
||||||
|
|
||||||
|
# Option 2: Rollback to previous CLAUDE.md
|
||||||
|
node .tdd-automation/scripts/rollback-tdd.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generated By
|
||||||
|
|
||||||
|
🤖 **Pattern Suggestion Pipeline**
|
||||||
|
- Detected from 37 successful TDD applications
|
||||||
|
- Success rate: 86%
|
||||||
|
- Source: GitHub Issue #13
|
||||||
|
- Generated: 2025-11-02
|
||||||
|
- Enhanced with full implementation: 2025-11-02
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **0.2.0** (2025-11-02) - Stable release with full implementation
|
||||||
|
- **0.1.0** (2025-11-02) - Initial proof-of-concept
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- Full documentation: `README.md`
|
||||||
|
- Helper scripts: `.tdd-automation/scripts/`
|
||||||
|
- Examples: See README.md "Usage Examples" section
|
||||||
0
skills/tdd-automation/examples/.gitkeep
Normal file
0
skills/tdd-automation/examples/.gitkeep
Normal file
417
skills/tdd-automation/index.js
Executable file
417
skills/tdd-automation/index.js
Executable file
@@ -0,0 +1,417 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TDD Red-Green-Refactor Automation Skill
|
||||||
|
*
|
||||||
|
* Main orchestrator that installs TDD automation infrastructure in a project.
|
||||||
|
* Configures CLAUDE.md, git hooks, npm scripts, and helper utilities to enforce
|
||||||
|
* TDD workflow automatically for LLM-assisted development.
|
||||||
|
*
|
||||||
|
* @author Pattern Suggestion Pipeline
|
||||||
|
* @version 0.2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Import utilities
|
||||||
|
const ClaudeMdValidator = require('./utils/validate-claude-md');
|
||||||
|
const ClaudeMdMerger = require('./utils/merge-claude-md');
|
||||||
|
const ProjectDetector = require('./utils/detect-project-type');
|
||||||
|
const PackageJsonUpdater = require('./utils/update-package-json');
|
||||||
|
const HookInstaller = require('./utils/install-hooks');
|
||||||
|
|
||||||
|
class TddAutomationSkill {
|
||||||
|
constructor() {
|
||||||
|
this.projectRoot = process.cwd();
|
||||||
|
this.skillRoot = __dirname;
|
||||||
|
this.version = '0.2.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main installation workflow
|
||||||
|
*/
|
||||||
|
async install() {
|
||||||
|
console.log('╔════════════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ TDD Red-Green-Refactor Automation ║');
|
||||||
|
console.log('║ Version 0.2.0 ║');
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
success: false,
|
||||||
|
steps: [],
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Detect project type
|
||||||
|
console.log('🔍 Step 1: Detecting project configuration...\n');
|
||||||
|
const projectInfo = await this.detectProject(results);
|
||||||
|
|
||||||
|
// Step 2: Validate and backup CLAUDE.md
|
||||||
|
console.log('\n🔍 Step 2: Validating CLAUDE.md...\n');
|
||||||
|
const validation = await this.validateClaudeMd(results);
|
||||||
|
|
||||||
|
if (validation.strategy === 'skip') {
|
||||||
|
console.log('✅ TDD automation already installed');
|
||||||
|
console.log(' No changes needed.\n');
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Install/merge CLAUDE.md configuration
|
||||||
|
console.log('\n📝 Step 3: Configuring CLAUDE.md...\n');
|
||||||
|
await this.configureClaudeMd(validation, results);
|
||||||
|
|
||||||
|
// Step 4: Update package.json with TDD scripts
|
||||||
|
console.log('\n📦 Step 4: Adding npm scripts...\n');
|
||||||
|
await this.updatePackageJson(projectInfo, results);
|
||||||
|
|
||||||
|
// Step 5: Install hooks
|
||||||
|
console.log('\n🪝 Step 5: Installing hooks...\n');
|
||||||
|
await this.installHooks(results);
|
||||||
|
|
||||||
|
// Step 6: Verify installation
|
||||||
|
console.log('\n✅ Step 6: Verifying installation...\n');
|
||||||
|
await this.verifyInstallation(results);
|
||||||
|
|
||||||
|
// Success summary
|
||||||
|
results.success = results.errors.length === 0;
|
||||||
|
this.printSummary(results, projectInfo);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ Installation failed:', error.message);
|
||||||
|
console.error('\n Stack trace:', error.stack);
|
||||||
|
results.errors.push(error.message);
|
||||||
|
results.success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect project type and configuration
|
||||||
|
*/
|
||||||
|
async detectProject(results) {
|
||||||
|
const detector = new ProjectDetector(this.projectRoot);
|
||||||
|
const projectInfo = detector.detect();
|
||||||
|
|
||||||
|
console.log(` Project: ${projectInfo.projectName}`);
|
||||||
|
console.log(` Type: ${projectInfo.type}`);
|
||||||
|
|
||||||
|
if (projectInfo.testFramework !== 'unknown') {
|
||||||
|
console.log(` Test Framework: ${projectInfo.testFramework}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectInfo.hasTypeScript) {
|
||||||
|
console.log(' Language: TypeScript');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectInfo.hasGit) {
|
||||||
|
console.log(' ✓ Git repository detected');
|
||||||
|
} else {
|
||||||
|
results.warnings.push('No git repository - git hooks will not be installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectInfo.recommendations.length > 0) {
|
||||||
|
console.log('\n Recommendations:');
|
||||||
|
projectInfo.recommendations.forEach(rec => {
|
||||||
|
console.log(` • ${rec}`);
|
||||||
|
results.warnings.push(rec);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
results.steps.push('Project detection completed');
|
||||||
|
return projectInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate CLAUDE.md state
|
||||||
|
*/
|
||||||
|
async validateClaudeMd(results) {
|
||||||
|
const validator = new ClaudeMdValidator(this.projectRoot);
|
||||||
|
const validation = validator.validate();
|
||||||
|
|
||||||
|
console.log(` Location: ${validation.path}`);
|
||||||
|
console.log(` Exists: ${validation.exists ? 'Yes' : 'No'}`);
|
||||||
|
|
||||||
|
if (validation.exists) {
|
||||||
|
console.log(` Size: ${this.formatBytes(validation.size)}`);
|
||||||
|
console.log(` Has Content: ${validation.hasExistingContent ? 'Yes' : 'No'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Strategy: ${validation.strategy.toUpperCase()}`);
|
||||||
|
|
||||||
|
if (validation.warnings.length > 0) {
|
||||||
|
console.log('\n Notes:');
|
||||||
|
validation.warnings.forEach(warning => {
|
||||||
|
console.log(` • ${warning}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
results.steps.push(`CLAUDE.md validation: ${validation.strategy}`);
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure CLAUDE.md with TDD automation
|
||||||
|
*/
|
||||||
|
async configureClaudeMd(validation, results) {
|
||||||
|
const validator = new ClaudeMdValidator(this.projectRoot);
|
||||||
|
|
||||||
|
if (validation.strategy === 'merge') {
|
||||||
|
// Existing content - merge safely
|
||||||
|
console.log(' Creating backup...');
|
||||||
|
const backup = validator.createBackup();
|
||||||
|
|
||||||
|
if (backup.success) {
|
||||||
|
console.log(` ✓ Backup created: ${path.basename(backup.path)}`);
|
||||||
|
results.steps.push(`Backup created: ${backup.path}`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Backup failed: ${backup.reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read existing content
|
||||||
|
const existingContent = fs.readFileSync(validator.claudeMdPath, 'utf-8');
|
||||||
|
|
||||||
|
// Merge with TDD section
|
||||||
|
console.log(' Merging TDD configuration...');
|
||||||
|
const merger = new ClaudeMdMerger(existingContent);
|
||||||
|
const mergedContent = merger.merge();
|
||||||
|
|
||||||
|
// Write merged content
|
||||||
|
fs.writeFileSync(validator.claudeMdPath, mergedContent, 'utf-8');
|
||||||
|
|
||||||
|
const newSize = mergedContent.length;
|
||||||
|
const added = newSize - validation.size;
|
||||||
|
|
||||||
|
console.log(` ✓ CLAUDE.md updated`);
|
||||||
|
console.log(` Original: ${this.formatBytes(validation.size)}`);
|
||||||
|
console.log(` New: ${this.formatBytes(newSize)} (+${this.formatBytes(added)})`);
|
||||||
|
|
||||||
|
results.steps.push('CLAUDE.md merged with TDD configuration');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// No existing content or empty - create new
|
||||||
|
console.log(' Creating new CLAUDE.md with TDD configuration...');
|
||||||
|
|
||||||
|
const claudeDir = path.join(this.projectRoot, '.claude');
|
||||||
|
if (!fs.existsSync(claudeDir)) {
|
||||||
|
fs.mkdirSync(claudeDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = ClaudeMdMerger.createNew();
|
||||||
|
fs.writeFileSync(validator.claudeMdPath, content, 'utf-8');
|
||||||
|
|
||||||
|
console.log(` ✓ CLAUDE.md created`);
|
||||||
|
console.log(` Size: ${this.formatBytes(content.length)}`);
|
||||||
|
|
||||||
|
results.steps.push('CLAUDE.md created with TDD configuration');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update package.json with TDD scripts
|
||||||
|
*/
|
||||||
|
async updatePackageJson(projectInfo, results) {
|
||||||
|
if (!projectInfo.hasPackageJson) {
|
||||||
|
console.log(' ⚠️ No package.json found - skipping npm scripts');
|
||||||
|
results.warnings.push('No package.json - npm scripts not added');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updater = new PackageJsonUpdater(this.projectRoot);
|
||||||
|
|
||||||
|
// Check if already installed
|
||||||
|
if (updater.hasTddScripts()) {
|
||||||
|
console.log(' ✓ TDD scripts already installed');
|
||||||
|
results.steps.push('TDD npm scripts already present');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine test command based on project
|
||||||
|
const testCommand = projectInfo.testFramework === 'vitest'
|
||||||
|
? 'vitest --run'
|
||||||
|
: projectInfo.testFramework === 'jest'
|
||||||
|
? 'jest'
|
||||||
|
: 'npm test';
|
||||||
|
|
||||||
|
const result = updater.addTddScripts(testCommand);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(' ✓ Added TDD scripts to package.json');
|
||||||
|
|
||||||
|
if (result.added.length > 0) {
|
||||||
|
console.log('\n Scripts added:');
|
||||||
|
result.added.forEach(script => {
|
||||||
|
console.log(` • ${script}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.skipped.length > 0) {
|
||||||
|
console.log('\n Scripts skipped (already exist):');
|
||||||
|
result.skipped.forEach(script => {
|
||||||
|
console.log(` • ${script}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
results.steps.push(`Added ${result.added.length} npm scripts`);
|
||||||
|
} else {
|
||||||
|
results.warnings.push(`Failed to update package.json: ${result.reason}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install git and Claude hooks
|
||||||
|
*/
|
||||||
|
async installHooks(results) {
|
||||||
|
const installer = new HookInstaller(this.projectRoot, this.skillRoot);
|
||||||
|
const result = installer.installAll();
|
||||||
|
|
||||||
|
if (result.installed.length > 0) {
|
||||||
|
console.log(' Installed:');
|
||||||
|
result.installed.forEach(item => {
|
||||||
|
console.log(` ✓ ${item}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.skipped.length > 0) {
|
||||||
|
console.log('\n Skipped:');
|
||||||
|
result.skipped.forEach(item => {
|
||||||
|
console.log(` • ${item}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.failed.length > 0) {
|
||||||
|
console.log('\n Failed:');
|
||||||
|
result.failed.forEach(item => {
|
||||||
|
console.log(` ✗ ${item}`);
|
||||||
|
results.warnings.push(`Hook installation failed: ${item}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
results.steps.push(`Installed ${result.installed.length} hooks`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify installation was successful
|
||||||
|
*/
|
||||||
|
async verifyInstallation(results) {
|
||||||
|
const checks = [];
|
||||||
|
|
||||||
|
// Check CLAUDE.md
|
||||||
|
const claudeMdPath = path.join(this.projectRoot, '.claude', 'CLAUDE.md');
|
||||||
|
if (fs.existsSync(claudeMdPath)) {
|
||||||
|
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
||||||
|
if (content.includes('TDD_AUTOMATION')) {
|
||||||
|
checks.push('✓ CLAUDE.md configured');
|
||||||
|
} else {
|
||||||
|
checks.push('✗ CLAUDE.md missing TDD configuration');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
checks.push('✗ CLAUDE.md not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check .tdd-automation directory
|
||||||
|
const tddDir = path.join(this.projectRoot, '.tdd-automation');
|
||||||
|
if (fs.existsSync(tddDir)) {
|
||||||
|
checks.push('✓ .tdd-automation directory created');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check package.json scripts
|
||||||
|
const packageJsonPath = path.join(this.projectRoot, 'package.json');
|
||||||
|
if (fs.existsSync(packageJsonPath)) {
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||||
|
if (packageJson.scripts && packageJson.scripts['test:tdd']) {
|
||||||
|
checks.push('✓ npm scripts added');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checks.forEach(check => console.log(` ${check}`));
|
||||||
|
results.steps.push('Installation verified');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print installation summary
|
||||||
|
*/
|
||||||
|
printSummary(results, projectInfo) {
|
||||||
|
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ Installation Summary ║');
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
if (results.success) {
|
||||||
|
console.log('✅ TDD automation installed successfully!\n');
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ TDD automation installed with warnings\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📦 What was installed:\n');
|
||||||
|
console.log(' • CLAUDE.md: TDD workflow configuration for LLM');
|
||||||
|
console.log(' • npm scripts: test:tdd, validate:tdd, generate:test');
|
||||||
|
console.log(' • Git hooks: pre-commit TDD validation');
|
||||||
|
console.log(' • Helper scripts: .tdd-automation/scripts/');
|
||||||
|
console.log(' • Templates: Test file generators\n');
|
||||||
|
|
||||||
|
console.log('🚀 Next steps:\n');
|
||||||
|
console.log(' 1. Test the automation:');
|
||||||
|
console.log(' Say: "Implement user authentication"\n');
|
||||||
|
console.log(' 2. The LLM will automatically:');
|
||||||
|
console.log(' • Follow TDD red-green-refactor workflow');
|
||||||
|
console.log(' • Create tests BEFORE implementation');
|
||||||
|
console.log(' • Run tests to verify each phase');
|
||||||
|
console.log(' • Use TodoWrite to track progress\n');
|
||||||
|
|
||||||
|
console.log('📚 Available commands:\n');
|
||||||
|
console.log(' npm run test:tdd # Run all tests');
|
||||||
|
console.log(' npm run validate:tdd # Check TDD compliance');
|
||||||
|
console.log(' npm run generate:test <file> # Create test template\n');
|
||||||
|
|
||||||
|
console.log('🔧 Maintenance:\n');
|
||||||
|
console.log(' node .tdd-automation/scripts/rollback-tdd.js');
|
||||||
|
console.log(' node .tdd-automation/scripts/remove-tdd-section.js\n');
|
||||||
|
|
||||||
|
if (results.warnings.length > 0) {
|
||||||
|
console.log('⚠️ Warnings:\n');
|
||||||
|
results.warnings.forEach(warning => {
|
||||||
|
console.log(` • ${warning}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('═══════════════════════════════════════════════════════════════\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format bytes for display
|
||||||
|
*/
|
||||||
|
formatBytes(bytes) {
|
||||||
|
if (bytes < 1024) return `${bytes} bytes`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
const skill = new TddAutomationSkill();
|
||||||
|
const results = await skill.install();
|
||||||
|
|
||||||
|
if (!results.success && results.errors.length > 0) {
|
||||||
|
console.error('Installation completed with errors');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run if called directly
|
||||||
|
if (require.main === module) {
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TddAutomationSkill;
|
||||||
0
skills/tdd-automation/reference/.gitkeep
Normal file
0
skills/tdd-automation/reference/.gitkeep
Normal file
198
skills/tdd-automation/scripts/generate-test.js
Executable file
198
skills/tdd-automation/scripts/generate-test.js
Executable file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Template Generator
|
||||||
|
*
|
||||||
|
* Generates test file templates based on implementation files.
|
||||||
|
* Analyzes project structure and test framework to create appropriate templates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function generateTest() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
console.error('Usage: npm run generate:test <implementation-file>');
|
||||||
|
console.error('');
|
||||||
|
console.error('Example:');
|
||||||
|
console.error(' npm run generate:test src/features/auth/login.ts');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const implFile = args[0];
|
||||||
|
|
||||||
|
if (!fs.existsSync(implFile)) {
|
||||||
|
console.error(`❌ Error: File not found: ${implFile}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine test file path
|
||||||
|
const ext = path.extname(implFile);
|
||||||
|
const testFile = implFile.replace(ext, `.test${ext}`);
|
||||||
|
|
||||||
|
if (fs.existsSync(testFile)) {
|
||||||
|
console.error(`❌ Error: Test file already exists: ${testFile}`);
|
||||||
|
console.error(' Use a different name or edit the existing file');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implementation file info
|
||||||
|
const filename = path.basename(implFile, ext);
|
||||||
|
const implContent = fs.readFileSync(implFile, 'utf-8');
|
||||||
|
|
||||||
|
// Detect test framework
|
||||||
|
const testFramework = detectTestFramework();
|
||||||
|
|
||||||
|
// Generate appropriate template
|
||||||
|
const template = generateTemplate(filename, implFile, implContent, testFramework);
|
||||||
|
|
||||||
|
// Write test file
|
||||||
|
fs.writeFileSync(testFile, template, 'utf-8');
|
||||||
|
|
||||||
|
console.log('✅ Test file created:', testFile);
|
||||||
|
console.log('');
|
||||||
|
console.log('📝 Next steps:');
|
||||||
|
console.log(' 1. Edit the test file to add specific test cases');
|
||||||
|
console.log(' 2. Run tests to verify RED phase:');
|
||||||
|
console.log(` npm run test:red -- ${testFile}`);
|
||||||
|
console.log(' 3. Implement the feature');
|
||||||
|
console.log(' 4. Run tests to verify GREEN phase:');
|
||||||
|
console.log(` npm run test:green -- ${testFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectTestFramework() {
|
||||||
|
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(packageJsonPath)) {
|
||||||
|
return 'vitest'; // default
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||||
|
const allDeps = {
|
||||||
|
...packageJson.dependencies,
|
||||||
|
...packageJson.devDependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
if (allDeps['vitest']) return 'vitest';
|
||||||
|
if (allDeps['jest']) return 'jest';
|
||||||
|
if (allDeps['@jest/globals']) return 'jest';
|
||||||
|
if (allDeps['mocha']) return 'mocha';
|
||||||
|
|
||||||
|
return 'vitest'; // default
|
||||||
|
} catch (error) {
|
||||||
|
return 'vitest';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTemplate(filename, implFile, implContent, testFramework) {
|
||||||
|
const relativePath = path.relative(path.dirname(implFile), implFile);
|
||||||
|
const importPath = './' + path.basename(implFile, path.extname(implFile));
|
||||||
|
|
||||||
|
// Extract potential functions/classes to test
|
||||||
|
const entities = extractEntities(implContent);
|
||||||
|
|
||||||
|
let template = '';
|
||||||
|
|
||||||
|
// Add imports based on test framework
|
||||||
|
if (testFramework === 'vitest') {
|
||||||
|
template += `import { describe, it, expect, beforeEach, afterEach } from 'vitest';\n`;
|
||||||
|
} else if (testFramework === 'jest') {
|
||||||
|
template += `import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';\n`;
|
||||||
|
} else {
|
||||||
|
template += `const { describe, it, expect, beforeEach, afterEach } = require('${testFramework}');\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add import for implementation
|
||||||
|
template += `import * as impl from '${importPath}';\n`;
|
||||||
|
template += `\n`;
|
||||||
|
|
||||||
|
// Add main describe block
|
||||||
|
template += `describe('${filename}', () => {\n`;
|
||||||
|
template += ` // TODO: Add setup and teardown if needed\n`;
|
||||||
|
template += ` // beforeEach(() => {\n`;
|
||||||
|
template += ` // // Setup before each test\n`;
|
||||||
|
template += ` // });\n`;
|
||||||
|
template += `\n`;
|
||||||
|
template += ` // afterEach(() => {\n`;
|
||||||
|
template += ` // // Cleanup after each test\n`;
|
||||||
|
template += ` // });\n`;
|
||||||
|
template += `\n`;
|
||||||
|
|
||||||
|
if (entities.length > 0) {
|
||||||
|
// Generate test stubs for each entity
|
||||||
|
entities.forEach(entity => {
|
||||||
|
template += ` describe('${entity}', () => {\n`;
|
||||||
|
template += ` it('should [behavior] when [condition]', () => {\n`;
|
||||||
|
template += ` // Arrange\n`;
|
||||||
|
template += ` // TODO: Set up test data and dependencies\n`;
|
||||||
|
template += `\n`;
|
||||||
|
template += ` // Act\n`;
|
||||||
|
template += ` // TODO: Call the function/method being tested\n`;
|
||||||
|
template += ` // const result = impl.${entity}();\n`;
|
||||||
|
template += `\n`;
|
||||||
|
template += ` // Assert\n`;
|
||||||
|
template += ` // TODO: Verify the expected behavior\n`;
|
||||||
|
template += ` expect(true).toBe(false); // Replace with actual assertion\n`;
|
||||||
|
template += ` });\n`;
|
||||||
|
template += `\n`;
|
||||||
|
template += ` // TODO: Add more test cases:\n`;
|
||||||
|
template += ` // it('should handle edge case when [condition]', () => { ... });\n`;
|
||||||
|
template += ` // it('should throw error when [invalid input]', () => { ... });\n`;
|
||||||
|
template += ` });\n`;
|
||||||
|
template += `\n`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Generic test template
|
||||||
|
template += ` it('should [behavior] when [condition]', () => {\n`;
|
||||||
|
template += ` // Arrange\n`;
|
||||||
|
template += ` // TODO: Set up test data and dependencies\n`;
|
||||||
|
template += `\n`;
|
||||||
|
template += ` // Act\n`;
|
||||||
|
template += ` // TODO: Call the function/method being tested\n`;
|
||||||
|
template += `\n`;
|
||||||
|
template += ` // Assert\n`;
|
||||||
|
template += ` // TODO: Verify the expected behavior\n`;
|
||||||
|
template += ` expect(true).toBe(false); // Replace with actual assertion\n`;
|
||||||
|
template += ` });\n`;
|
||||||
|
template += `\n`;
|
||||||
|
template += ` // TODO: Add more test cases for different scenarios\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
template += `});\n`;
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractEntities(content) {
|
||||||
|
const entities = [];
|
||||||
|
|
||||||
|
// Extract exported functions
|
||||||
|
const functionMatches = content.matchAll(/export\s+(?:async\s+)?function\s+([a-zA-Z0-9_]+)/g);
|
||||||
|
for (const match of functionMatches) {
|
||||||
|
entities.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract exported classes
|
||||||
|
const classMatches = content.matchAll(/export\s+class\s+([a-zA-Z0-9_]+)/g);
|
||||||
|
for (const match of classMatches) {
|
||||||
|
entities.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract exported const functions (arrow functions)
|
||||||
|
const constMatches = content.matchAll(/export\s+const\s+([a-zA-Z0-9_]+)\s*=\s*(?:async\s*)?\(/g);
|
||||||
|
for (const match of constMatches) {
|
||||||
|
entities.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run if called directly
|
||||||
|
if (require.main === module) {
|
||||||
|
generateTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { generateTest };
|
||||||
125
skills/tdd-automation/scripts/remove-tdd-section.js
Executable file
125
skills/tdd-automation/scripts/remove-tdd-section.js
Executable file
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TDD Section Removal Utility
|
||||||
|
*
|
||||||
|
* Cleanly removes only the TDD automation section from CLAUDE.md
|
||||||
|
* while preserving all other content.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Import utilities
|
||||||
|
const projectRoot = process.cwd();
|
||||||
|
const skillRoot = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
// Try to load ClaudeMdValidator
|
||||||
|
let ClaudeMdValidator;
|
||||||
|
try {
|
||||||
|
ClaudeMdValidator = require(path.join(skillRoot, 'utils', 'validate-claude-md.js'));
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
ClaudeMdValidator = require(path.join(projectRoot, '.tdd-automation', 'utils', 'validate-claude-md.js'));
|
||||||
|
} catch {
|
||||||
|
console.error('❌ Error: ClaudeMdValidator not found');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTddSection() {
|
||||||
|
console.log('🗑️ TDD Section Removal Utility\n');
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||||
|
|
||||||
|
const validator = new ClaudeMdValidator(projectRoot);
|
||||||
|
|
||||||
|
// Check if CLAUDE.md exists
|
||||||
|
if (!fs.existsSync(validator.claudeMdPath)) {
|
||||||
|
console.log('❌ CLAUDE.md not found');
|
||||||
|
console.log(` Expected location: ${validator.claudeMdPath}`);
|
||||||
|
console.log('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if TDD section exists
|
||||||
|
const content = fs.readFileSync(validator.claudeMdPath, 'utf-8');
|
||||||
|
|
||||||
|
if (!validator.detectTddSection(content)) {
|
||||||
|
console.log('✅ No TDD section found in CLAUDE.md');
|
||||||
|
console.log(' Nothing to remove');
|
||||||
|
console.log('');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 TDD section detected in CLAUDE.md\n');
|
||||||
|
|
||||||
|
// Show what will be removed
|
||||||
|
const startMarker = '<!-- TDD_AUTOMATION_START -->';
|
||||||
|
const endMarker = '<!-- TDD_AUTOMATION_END -->';
|
||||||
|
const startIdx = content.indexOf(startMarker);
|
||||||
|
const endIdx = content.indexOf(endMarker);
|
||||||
|
|
||||||
|
if (startIdx !== -1 && endIdx !== -1) {
|
||||||
|
const sectionSize = endIdx - startIdx + endMarker.length;
|
||||||
|
console.log(` Section size: ${formatBytes(sectionSize)}`);
|
||||||
|
console.log(` Total file size: ${formatBytes(content.length)}`);
|
||||||
|
console.log(` After removal: ${formatBytes(content.length - sectionSize)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform removal
|
||||||
|
console.log('🔒 Creating backup before removal...');
|
||||||
|
const result = validator.removeTddSection();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`✅ Backup created: ${path.basename(result.backup)}\n`);
|
||||||
|
console.log('✅ TDD section removed successfully!\n');
|
||||||
|
console.log(' Statistics:');
|
||||||
|
console.log(` • Original size: ${formatBytes(result.originalSize)}`);
|
||||||
|
console.log(` • New size: ${formatBytes(result.newSize)}`);
|
||||||
|
console.log(` • Removed: ${formatBytes(result.removed)}\n`);
|
||||||
|
console.log(' Backup available at:');
|
||||||
|
console.log(` ${result.backup}\n`);
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||||
|
console.log('⚠️ TDD automation configuration removed from CLAUDE.md\n');
|
||||||
|
console.log(' Other TDD automation components remain:');
|
||||||
|
console.log(' • .tdd-automation/ directory');
|
||||||
|
console.log(' • npm scripts (test:tdd, validate:tdd, etc.)');
|
||||||
|
console.log(' • git hooks (.git/hooks/pre-commit)');
|
||||||
|
console.log(' • Claude hooks (.claude/hooks/tdd-auto-enforcer.sh)\n');
|
||||||
|
console.log(' To remove all components:');
|
||||||
|
console.log(' node .tdd-automation/scripts/uninstall-tdd.js\n');
|
||||||
|
console.log(' To restore this section:');
|
||||||
|
console.log(` node .tdd-automation/scripts/rollback-tdd.js\n`);
|
||||||
|
} else {
|
||||||
|
console.error('❌ Removal failed:', result.reason);
|
||||||
|
console.error('');
|
||||||
|
console.error(' Possible issues:');
|
||||||
|
console.error(' • Section markers not found (manual edit may be needed)');
|
||||||
|
console.error(' • File permissions issue');
|
||||||
|
console.error(' • Disk space issue');
|
||||||
|
console.error('');
|
||||||
|
console.error(' Manual removal:');
|
||||||
|
console.error(' 1. Open .claude/CLAUDE.md in editor');
|
||||||
|
console.error(' 2. Find <!-- TDD_AUTOMATION_START -->');
|
||||||
|
console.error(' 3. Delete everything until <!-- TDD_AUTOMATION_END -->');
|
||||||
|
console.error(' 4. Save file');
|
||||||
|
console.error('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes < 1024) return `${bytes} bytes`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run if called directly
|
||||||
|
if (require.main === module) {
|
||||||
|
removeTddSection().catch(error => {
|
||||||
|
console.error('❌ Unexpected error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { removeTddSection };
|
||||||
141
skills/tdd-automation/scripts/rollback-tdd.js
Executable file
141
skills/tdd-automation/scripts/rollback-tdd.js
Executable file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TDD Automation Rollback Utility
|
||||||
|
*
|
||||||
|
* Restores previous CLAUDE.md from backup and optionally removes all TDD automation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Import utilities from parent directory
|
||||||
|
const projectRoot = process.cwd();
|
||||||
|
const skillRoot = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
// Try to load ClaudeMdValidator from skill location
|
||||||
|
let ClaudeMdValidator;
|
||||||
|
try {
|
||||||
|
ClaudeMdValidator = require(path.join(skillRoot, 'utils', 'validate-claude-md.js'));
|
||||||
|
} catch {
|
||||||
|
// If skill utils not available, use local copy in .tdd-automation
|
||||||
|
try {
|
||||||
|
ClaudeMdValidator = require(path.join(projectRoot, '.tdd-automation', 'utils', 'validate-claude-md.js'));
|
||||||
|
} catch {
|
||||||
|
console.error('❌ Error: ClaudeMdValidator not found');
|
||||||
|
console.error(' This script must be run from project root or .tdd-automation directory');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rollbackTddAutomation() {
|
||||||
|
console.log('🔄 TDD Automation Rollback Utility\n');
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||||
|
|
||||||
|
const validator = new ClaudeMdValidator(projectRoot);
|
||||||
|
|
||||||
|
// List available backups
|
||||||
|
const backups = validator.listBackups();
|
||||||
|
|
||||||
|
if (backups.length === 0) {
|
||||||
|
console.log('❌ No backup files found');
|
||||||
|
console.log(' Cannot rollback without backup');
|
||||||
|
console.log('');
|
||||||
|
console.log(' Backup files should be in: .claude/CLAUDE.md.backup.*');
|
||||||
|
console.log('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📋 Found ${backups.length} backup(s):\n`);
|
||||||
|
|
||||||
|
backups.forEach((backup, i) => {
|
||||||
|
const age = getTimeAgo(backup.created);
|
||||||
|
console.log(`${i + 1}. ${backup.name}`);
|
||||||
|
console.log(` Created: ${backup.created.toISOString()} (${age})`);
|
||||||
|
console.log(` Size: ${formatBytes(backup.size)}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use most recent backup
|
||||||
|
const latestBackup = backups[0];
|
||||||
|
console.log(`🔄 Rolling back to most recent backup:\n`);
|
||||||
|
console.log(` ${latestBackup.name}`);
|
||||||
|
console.log(` Created: ${latestBackup.created.toISOString()}\n`);
|
||||||
|
|
||||||
|
// Create backup of current state before rollback
|
||||||
|
console.log('🔒 Creating safety backup of current state...');
|
||||||
|
const currentBackup = validator.createBackup();
|
||||||
|
|
||||||
|
if (currentBackup.success) {
|
||||||
|
console.log(`✅ Current state backed up to:`);
|
||||||
|
console.log(` ${path.basename(currentBackup.path)}\n`);
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ Could not backup current state: ${currentBackup.reason}`);
|
||||||
|
console.log(` Proceeding with rollback anyway...\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform rollback
|
||||||
|
console.log('🔄 Performing rollback...');
|
||||||
|
const result = validator.rollback(latestBackup.path);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('✅ Rollback successful!\n');
|
||||||
|
console.log(' CLAUDE.md has been restored from:');
|
||||||
|
console.log(` ${path.basename(result.restoredFrom)}`);
|
||||||
|
console.log(` Size: ${formatBytes(result.size)}\n`);
|
||||||
|
|
||||||
|
if (currentBackup.success) {
|
||||||
|
console.log(' Your previous state was saved to:');
|
||||||
|
console.log(` ${path.basename(currentBackup.path)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||||
|
console.log('⚠️ TDD automation configuration has been removed from CLAUDE.md\n');
|
||||||
|
console.log(' Other TDD automation components remain:');
|
||||||
|
console.log(' • .tdd-automation/ directory');
|
||||||
|
console.log(' • npm scripts (test:tdd, validate:tdd, etc.)');
|
||||||
|
console.log(' • git hooks (.git/hooks/pre-commit)');
|
||||||
|
console.log(' • Claude hooks (.claude/hooks/tdd-auto-enforcer.sh)\n');
|
||||||
|
console.log(' To remove all TDD automation components:');
|
||||||
|
console.log(' node .tdd-automation/scripts/uninstall-tdd.js\n');
|
||||||
|
console.log(' To reinstall TDD automation:');
|
||||||
|
console.log(' Run the tdd-automation skill again\n');
|
||||||
|
} else {
|
||||||
|
console.error('❌ Rollback failed:', result.reason);
|
||||||
|
console.error('');
|
||||||
|
console.error(' You may need to manually restore CLAUDE.md from backup');
|
||||||
|
console.error(` Backup location: ${latestBackup.path}`);
|
||||||
|
console.error('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimeAgo(date) {
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now - date;
|
||||||
|
|
||||||
|
const seconds = Math.floor(diff / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
|
||||||
|
if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||||
|
if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
||||||
|
return `${seconds} second${seconds > 1 ? 's' : ''} ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes < 1024) return `${bytes} bytes`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run if called directly
|
||||||
|
if (require.main === module) {
|
||||||
|
rollbackTddAutomation().catch(error => {
|
||||||
|
console.error('❌ Unexpected error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { rollbackTddAutomation };
|
||||||
300
skills/tdd-automation/scripts/validate-tdd.js
Executable file
300
skills/tdd-automation/scripts/validate-tdd.js
Executable file
@@ -0,0 +1,300 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TDD Compliance Validator
|
||||||
|
*
|
||||||
|
* Validates that project follows TDD best practices:
|
||||||
|
* - All implementation files have corresponding tests
|
||||||
|
* - Test coverage meets minimum threshold
|
||||||
|
* - Tests are properly structured
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
async function validateTdd() {
|
||||||
|
console.log('🔍 TDD Compliance Validation\n');
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
passed: [],
|
||||||
|
failed: [],
|
||||||
|
warnings: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check 1: Verify test files exist for implementation files
|
||||||
|
console.log('📋 Check 1: Test file coverage...');
|
||||||
|
await checkTestFileCoverage(results);
|
||||||
|
|
||||||
|
// Check 2: Verify test framework is installed
|
||||||
|
console.log('\n📋 Check 2: Test framework installation...');
|
||||||
|
await checkTestFramework(results);
|
||||||
|
|
||||||
|
// Check 3: Verify TDD npm scripts exist
|
||||||
|
console.log('\n📋 Check 3: TDD npm scripts...');
|
||||||
|
await checkNpmScripts(results);
|
||||||
|
|
||||||
|
// Check 4: Verify git hooks installed
|
||||||
|
console.log('\n📋 Check 4: Git hooks...');
|
||||||
|
await checkGitHooks(results);
|
||||||
|
|
||||||
|
// Check 5: Verify CLAUDE.md has TDD configuration
|
||||||
|
console.log('\n📋 Check 5: CLAUDE.md configuration...');
|
||||||
|
await checkClaudeMd(results);
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||||
|
console.log('📊 Validation Summary\n');
|
||||||
|
|
||||||
|
if (results.passed.length > 0) {
|
||||||
|
console.log(`✅ Passed: ${results.passed.length}`);
|
||||||
|
results.passed.forEach(msg => console.log(` • ${msg}`));
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.warnings.length > 0) {
|
||||||
|
console.log(`⚠️ Warnings: ${results.warnings.length}`);
|
||||||
|
results.warnings.forEach(msg => console.log(` • ${msg}`));
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.failed.length > 0) {
|
||||||
|
console.log(`❌ Failed: ${results.failed.length}`);
|
||||||
|
results.failed.forEach(msg => console.log(` • ${msg}`));
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||||
|
|
||||||
|
if (results.failed.length > 0) {
|
||||||
|
console.log('❌ TDD compliance validation FAILED\n');
|
||||||
|
console.log(' Please address the failed checks above');
|
||||||
|
console.log(' Run: npm run generate:test <file> to create missing tests\n');
|
||||||
|
process.exit(1);
|
||||||
|
} else if (results.warnings.length > 0) {
|
||||||
|
console.log('⚠️ TDD compliance validation PASSED with warnings\n');
|
||||||
|
console.log(' Consider addressing the warnings above\n');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log('✅ TDD compliance validation PASSED\n');
|
||||||
|
console.log(' All checks passed successfully!\n');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkTestFileCoverage(results) {
|
||||||
|
const srcDir = path.join(process.cwd(), 'src');
|
||||||
|
|
||||||
|
if (!fs.existsSync(srcDir)) {
|
||||||
|
results.warnings.push('No src/ directory found - skipping test coverage check');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const implFiles = findImplementationFiles(srcDir);
|
||||||
|
const testFiles = findTestFiles(srcDir);
|
||||||
|
|
||||||
|
let missing = 0;
|
||||||
|
let covered = 0;
|
||||||
|
|
||||||
|
for (const implFile of implFiles) {
|
||||||
|
const testFile = findCorrespondingTest(implFile, testFiles);
|
||||||
|
|
||||||
|
if (!testFile) {
|
||||||
|
missing++;
|
||||||
|
if (missing <= 5) { // Only show first 5 to avoid spam
|
||||||
|
results.failed.push(`Missing test for: ${path.relative(process.cwd(), implFile)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
covered++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing > 5) {
|
||||||
|
results.failed.push(`... and ${missing - 5} more files without tests`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalFiles = implFiles.length;
|
||||||
|
const coverage = totalFiles > 0 ? ((covered / totalFiles) * 100).toFixed(1) : 100;
|
||||||
|
|
||||||
|
if (missing === 0 && totalFiles > 0) {
|
||||||
|
results.passed.push(`All ${totalFiles} implementation files have tests (100%)`);
|
||||||
|
} else if (coverage >= 80) {
|
||||||
|
results.warnings.push(`Test file coverage: ${coverage}% (${covered}/${totalFiles}) - below 100%`);
|
||||||
|
} else if (totalFiles === 0) {
|
||||||
|
results.warnings.push('No implementation files found in src/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkTestFramework(results) {
|
||||||
|
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(packageJsonPath)) {
|
||||||
|
results.failed.push('package.json not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||||
|
const allDeps = {
|
||||||
|
...packageJson.dependencies,
|
||||||
|
...packageJson.devDependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
const testFrameworks = ['vitest', 'jest', '@jest/globals', 'mocha', 'ava'];
|
||||||
|
const installed = testFrameworks.find(fw => allDeps[fw]);
|
||||||
|
|
||||||
|
if (installed) {
|
||||||
|
results.passed.push(`Test framework installed: ${installed}`);
|
||||||
|
} else {
|
||||||
|
results.failed.push('No test framework found (install vitest, jest, or mocha)');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
results.failed.push(`Error reading package.json: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkNpmScripts(results) {
|
||||||
|
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(packageJsonPath)) {
|
||||||
|
return; // Already reported in previous check
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||||
|
const scripts = packageJson.scripts || {};
|
||||||
|
|
||||||
|
const requiredScripts = ['test:tdd', 'validate:tdd', 'generate:test'];
|
||||||
|
const missingScripts = requiredScripts.filter(script => !scripts[script]);
|
||||||
|
|
||||||
|
if (missingScripts.length === 0) {
|
||||||
|
results.passed.push('All TDD npm scripts installed');
|
||||||
|
} else {
|
||||||
|
missingScripts.forEach(script => {
|
||||||
|
results.warnings.push(`Missing npm script: ${script}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
results.failed.push(`Error checking npm scripts: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkGitHooks(results) {
|
||||||
|
const preCommitPath = path.join(process.cwd(), '.git', 'hooks', 'pre-commit');
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(process.cwd(), '.git'))) {
|
||||||
|
results.warnings.push('Not a git repository - no git hooks checked');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(preCommitPath)) {
|
||||||
|
results.warnings.push('Git pre-commit hook not installed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(preCommitPath, 'utf-8');
|
||||||
|
|
||||||
|
if (content.includes('TDD_AUTOMATION') || content.includes('TDD Validation')) {
|
||||||
|
results.passed.push('Git pre-commit hook installed for TDD validation');
|
||||||
|
} else {
|
||||||
|
results.warnings.push('Git pre-commit hook exists but may not have TDD validation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkClaudeMd(results) {
|
||||||
|
const claudeMdPath = path.join(process.cwd(), '.claude', 'CLAUDE.md');
|
||||||
|
|
||||||
|
if (!fs.existsSync(claudeMdPath)) {
|
||||||
|
results.warnings.push('No .claude/CLAUDE.md found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
||||||
|
|
||||||
|
const tddMarkers = [
|
||||||
|
'TDD Red-Green-Refactor',
|
||||||
|
'tdd-automation-version',
|
||||||
|
'<!-- TDD_AUTOMATION_START -->'
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasTddConfig = tddMarkers.some(marker => content.includes(marker));
|
||||||
|
|
||||||
|
if (hasTddConfig) {
|
||||||
|
results.passed.push('CLAUDE.md has TDD configuration');
|
||||||
|
} else {
|
||||||
|
results.warnings.push('CLAUDE.md exists but missing TDD configuration');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findImplementationFiles(dir, files = []) {
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
// Skip node_modules, dist, build, etc.
|
||||||
|
if (!['node_modules', 'dist', 'build', '.git', 'coverage'].includes(entry.name)) {
|
||||||
|
findImplementationFiles(fullPath, files);
|
||||||
|
}
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
// Include .ts, .js, .tsx, .jsx files but exclude test files
|
||||||
|
if (/\.(ts|js|tsx|jsx)$/.test(entry.name) && !/\.(test|spec)\./.test(entry.name)) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTestFiles(dir, files = []) {
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (!['node_modules', 'dist', 'build', '.git', 'coverage'].includes(entry.name)) {
|
||||||
|
findTestFiles(fullPath, files);
|
||||||
|
}
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
if (/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(entry.name)) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCorrespondingTest(implFile, testFiles) {
|
||||||
|
const dir = path.dirname(implFile);
|
||||||
|
const filename = path.basename(implFile);
|
||||||
|
const base = filename.replace(/\.(ts|js|tsx|jsx)$/, '');
|
||||||
|
|
||||||
|
// Try multiple test file patterns
|
||||||
|
const patterns = [
|
||||||
|
path.join(dir, `${base}.test.ts`),
|
||||||
|
path.join(dir, `${base}.test.js`),
|
||||||
|
path.join(dir, `${base}.test.tsx`),
|
||||||
|
path.join(dir, `${base}.test.jsx`),
|
||||||
|
path.join(dir, `${base}.spec.ts`),
|
||||||
|
path.join(dir, `${base}.spec.js`),
|
||||||
|
path.join(dir, `${base}.spec.tsx`),
|
||||||
|
path.join(dir, `${base}.spec.jsx`)
|
||||||
|
];
|
||||||
|
|
||||||
|
return testFiles.find(testFile => patterns.includes(testFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run if called directly
|
||||||
|
if (require.main === module) {
|
||||||
|
validateTdd().catch(error => {
|
||||||
|
console.error('❌ Unexpected error:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { validateTdd };
|
||||||
110
skills/tdd-automation/templates/pre-commit.sh
Executable file
110
skills/tdd-automation/templates/pre-commit.sh
Executable file
@@ -0,0 +1,110 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# TDD_AUTOMATION: Git Pre-Commit Hook
|
||||||
|
#
|
||||||
|
# Validates that tests exist for implementation files before allowing commit.
|
||||||
|
# Ensures TDD workflow (test-first) is followed.
|
||||||
|
#
|
||||||
|
# Installed by: tdd-automation skill
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
echo "🔍 TDD Validation: Checking test-first compliance..."
|
||||||
|
|
||||||
|
# Get list of new implementation files being committed
|
||||||
|
IMPL_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E "^src/.*\.(ts|js|tsx|jsx)$" | grep -v ".test." | grep -v ".spec.")
|
||||||
|
|
||||||
|
if [ -z "$IMPL_FILES" ]; then
|
||||||
|
echo "✅ No new implementation files detected"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
VIOLATIONS=0
|
||||||
|
|
||||||
|
# Check each implementation file
|
||||||
|
while IFS= read -r impl_file; do
|
||||||
|
if [ -z "$impl_file" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine expected test file name
|
||||||
|
dir=$(dirname "$impl_file")
|
||||||
|
filename=$(basename "$impl_file")
|
||||||
|
base="${filename%.*}"
|
||||||
|
ext="${filename##*.}"
|
||||||
|
|
||||||
|
# Try multiple test file patterns
|
||||||
|
test_patterns=(
|
||||||
|
"$dir/$base.test.$ext"
|
||||||
|
"$dir/$base.spec.$ext"
|
||||||
|
"${impl_file%.*}.test.$ext"
|
||||||
|
"${impl_file%.*}.spec.$ext"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_exists=false
|
||||||
|
for test_file in "${test_patterns[@]}"; do
|
||||||
|
if [ -f "$test_file" ]; then
|
||||||
|
test_exists=true
|
||||||
|
|
||||||
|
# Check if test file was committed first (git log timestamps)
|
||||||
|
test_commit_time=$(git log --diff-filter=A --format="%at" --follow -- "$test_file" 2>/dev/null | head -1)
|
||||||
|
impl_commit_time=$(date +%s) # Current time for uncommitted file
|
||||||
|
|
||||||
|
# If test file exists in git history, it was committed first - good!
|
||||||
|
if [ -n "$test_commit_time" ]; then
|
||||||
|
echo "✅ $impl_file -> Test exists: $test_file"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
# Test file exists but not committed yet
|
||||||
|
if git diff --cached --name-only | grep -q "$test_file"; then
|
||||||
|
echo "✅ $impl_file -> Test being committed: $test_file"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$test_exists" = false ]; then
|
||||||
|
echo "❌ TDD VIOLATION: No test file found for $impl_file"
|
||||||
|
echo " Expected one of:"
|
||||||
|
for pattern in "${test_patterns[@]}"; do
|
||||||
|
echo " - $pattern"
|
||||||
|
done
|
||||||
|
VIOLATIONS=$((VIOLATIONS + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$IMPL_FILES"
|
||||||
|
|
||||||
|
# Check for test files in commit
|
||||||
|
TEST_FILES=$(git diff --cached --name-only | grep -E "\.(test|spec)\.(ts|js|tsx|jsx)$")
|
||||||
|
|
||||||
|
if [ -n "$TEST_FILES" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "📝 Test files in this commit:"
|
||||||
|
echo "$TEST_FILES" | while IFS= read -r test_file; do
|
||||||
|
echo " ✓ $test_file"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ $VIOLATIONS -gt 0 ]; then
|
||||||
|
echo "❌ TDD Validation Failed: $VIOLATIONS violation(s) found"
|
||||||
|
echo ""
|
||||||
|
echo "TDD requires writing tests BEFORE implementation."
|
||||||
|
echo ""
|
||||||
|
echo "To fix:"
|
||||||
|
echo " 1. Create test file(s) for the implementation"
|
||||||
|
echo " 2. Commit tests first: git add <test-files> && git commit -m 'Add tests for [feature]'"
|
||||||
|
echo " 3. Then commit implementation: git add <impl-files> && git commit -m 'Implement [feature]'"
|
||||||
|
echo ""
|
||||||
|
echo "Or use: npm run generate:test <impl-file> to create a test template"
|
||||||
|
echo ""
|
||||||
|
echo "To bypass this check (not recommended):"
|
||||||
|
echo " git commit --no-verify"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ TDD Validation Passed: All implementation files have tests"
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
78
skills/tdd-automation/templates/tdd-auto-enforcer.sh
Executable file
78
skills/tdd-automation/templates/tdd-auto-enforcer.sh
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# TDD Auto-Enforcer Hook
|
||||||
|
#
|
||||||
|
# Claude Code hook that automatically guides LLM to follow TDD workflow
|
||||||
|
# when implementing features.
|
||||||
|
#
|
||||||
|
# Installed by: tdd-automation skill
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# Get user's prompt
|
||||||
|
USER_PROMPT="$1"
|
||||||
|
|
||||||
|
# Keywords that indicate implementation work
|
||||||
|
IMPLEMENTATION_KEYWORDS="implement|add|create|build|feature|write.*code|develop|make.*function|make.*component"
|
||||||
|
|
||||||
|
# Check if prompt is about implementing something
|
||||||
|
if echo "$USER_PROMPT" | grep -qiE "$IMPLEMENTATION_KEYWORDS"; then
|
||||||
|
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
⚠️ TDD ENFORCEMENT ACTIVE
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
You are about to implement code. You MUST follow TDD red-green-refactor:
|
||||||
|
|
||||||
|
📋 Required Workflow:
|
||||||
|
|
||||||
|
1. 🔴 RED Phase (First - DO NOT SKIP)
|
||||||
|
└─ Write a FAILING test before any implementation
|
||||||
|
└─ Run test to verify it fails: npm run test:red <test-file>
|
||||||
|
└─ Use TodoWrite to track this phase
|
||||||
|
|
||||||
|
2. 🟢 GREEN Phase (After RED verified)
|
||||||
|
└─ Write MINIMAL code to make test pass
|
||||||
|
└─ Run test to verify it passes: npm run test:green <test-file>
|
||||||
|
└─ Use TodoWrite to track this phase
|
||||||
|
|
||||||
|
3. 🔵 REFACTOR Phase (After GREEN verified)
|
||||||
|
└─ Improve code quality while keeping tests green
|
||||||
|
└─ Run all tests: npm run test:tdd
|
||||||
|
└─ Use TodoWrite to track this phase
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
🚨 CRITICAL RULES
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
❌ NEVER write implementation code before writing the test
|
||||||
|
❌ NEVER skip RED phase verification (must see test fail)
|
||||||
|
❌ NEVER skip GREEN phase verification (must see test pass)
|
||||||
|
✅ ALWAYS use TodoWrite to track RED-GREEN-REFACTOR phases
|
||||||
|
✅ ALWAYS run tests to verify each phase transition
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
📝 TodoWrite Template (Use This)
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
Create a todo list with these phases before starting:
|
||||||
|
|
||||||
|
[ ] RED: Write failing test for [feature name]
|
||||||
|
[ ] Verify test fails with expected error
|
||||||
|
[ ] GREEN: Implement minimal code to pass test
|
||||||
|
[ ] Verify test passes
|
||||||
|
[ ] REFACTOR: Improve code quality
|
||||||
|
[ ] Verify all tests still pass
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
Start with RED phase. Create the test file first.
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exit successfully (allow prompt to continue)
|
||||||
|
exit 0
|
||||||
163
skills/tdd-automation/utils/detect-project-type.js
Normal file
163
skills/tdd-automation/utils/detect-project-type.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project Type Detector
|
||||||
|
*
|
||||||
|
* Detects project type and test framework to provide appropriate TDD configuration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class ProjectDetector {
|
||||||
|
constructor(projectRoot) {
|
||||||
|
this.projectRoot = projectRoot;
|
||||||
|
this.packageJsonPath = path.join(projectRoot, 'package.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect project configuration
|
||||||
|
* @returns {object} Project details
|
||||||
|
*/
|
||||||
|
detect() {
|
||||||
|
const result = {
|
||||||
|
type: 'unknown',
|
||||||
|
testFramework: 'unknown',
|
||||||
|
hasPackageJson: false,
|
||||||
|
hasGit: false,
|
||||||
|
hasTypeScript: false,
|
||||||
|
projectName: path.basename(this.projectRoot),
|
||||||
|
recommendations: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for package.json
|
||||||
|
if (fs.existsSync(this.packageJsonPath)) {
|
||||||
|
result.hasPackageJson = true;
|
||||||
|
this.analyzePackageJson(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for git
|
||||||
|
if (fs.existsSync(path.join(this.projectRoot, '.git'))) {
|
||||||
|
result.hasGit = true;
|
||||||
|
} else {
|
||||||
|
result.recommendations.push('Initialize git repository for version control');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for TypeScript
|
||||||
|
if (fs.existsSync(path.join(this.projectRoot, 'tsconfig.json'))) {
|
||||||
|
result.hasTypeScript = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect project type
|
||||||
|
result.type = this.detectProjectType(result);
|
||||||
|
|
||||||
|
// Add recommendations
|
||||||
|
if (result.testFramework === 'unknown') {
|
||||||
|
result.recommendations.push('Install a test framework (vitest recommended)');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze package.json for dependencies and scripts
|
||||||
|
* @param {object} result - Result object to populate
|
||||||
|
*/
|
||||||
|
analyzePackageJson(result) {
|
||||||
|
try {
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf-8'));
|
||||||
|
|
||||||
|
result.projectName = packageJson.name || result.projectName;
|
||||||
|
|
||||||
|
const allDeps = {
|
||||||
|
...packageJson.dependencies,
|
||||||
|
...packageJson.devDependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
// Detect test framework
|
||||||
|
if (allDeps['vitest']) {
|
||||||
|
result.testFramework = 'vitest';
|
||||||
|
} else if (allDeps['jest']) {
|
||||||
|
result.testFramework = 'jest';
|
||||||
|
} else if (allDeps['mocha']) {
|
||||||
|
result.testFramework = 'mocha';
|
||||||
|
} else if (allDeps['ava']) {
|
||||||
|
result.testFramework = 'ava';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect framework/type
|
||||||
|
if (allDeps['react']) {
|
||||||
|
result.framework = 'react';
|
||||||
|
} else if (allDeps['vue']) {
|
||||||
|
result.framework = 'vue';
|
||||||
|
} else if (allDeps['@angular/core']) {
|
||||||
|
result.framework = 'angular';
|
||||||
|
} else if (allDeps['express']) {
|
||||||
|
result.framework = 'express';
|
||||||
|
} else if (allDeps['fastify']) {
|
||||||
|
result.framework = 'fastify';
|
||||||
|
} else if (allDeps['next']) {
|
||||||
|
result.framework = 'next';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing test scripts
|
||||||
|
result.hasTestScript = packageJson.scripts && packageJson.scripts.test;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing package.json:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine primary project type
|
||||||
|
* @param {object} result - Detection results
|
||||||
|
* @returns {string} Project type
|
||||||
|
*/
|
||||||
|
detectProjectType(result) {
|
||||||
|
if (result.framework === 'react') return 'react';
|
||||||
|
if (result.framework === 'vue') return 'vue';
|
||||||
|
if (result.framework === 'angular') return 'angular';
|
||||||
|
if (result.framework === 'next') return 'nextjs';
|
||||||
|
if (result.framework === 'express' || result.framework === 'fastify') return 'nodejs-backend';
|
||||||
|
if (result.hasTypeScript) return 'typescript';
|
||||||
|
if (result.hasPackageJson) return 'nodejs';
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recommended test command for project
|
||||||
|
* @returns {string} Test command
|
||||||
|
*/
|
||||||
|
getTestCommand() {
|
||||||
|
const result = this.detect();
|
||||||
|
|
||||||
|
switch (result.testFramework) {
|
||||||
|
case 'vitest':
|
||||||
|
return 'vitest --run';
|
||||||
|
case 'jest':
|
||||||
|
return 'jest';
|
||||||
|
case 'mocha':
|
||||||
|
return 'mocha';
|
||||||
|
case 'ava':
|
||||||
|
return 'ava';
|
||||||
|
default:
|
||||||
|
return 'npm test';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recommended test file extension
|
||||||
|
* @returns {string} File extension
|
||||||
|
*/
|
||||||
|
getTestExtension() {
|
||||||
|
const result = this.detect();
|
||||||
|
|
||||||
|
if (result.hasTypeScript) {
|
||||||
|
return '.test.ts';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '.test.js';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ProjectDetector;
|
||||||
305
skills/tdd-automation/utils/install-hooks.js
Normal file
305
skills/tdd-automation/utils/install-hooks.js
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook Installer
|
||||||
|
*
|
||||||
|
* Installs git hooks and Claude hooks for TDD enforcement
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class HookInstaller {
|
||||||
|
constructor(projectRoot, skillRoot) {
|
||||||
|
this.projectRoot = projectRoot;
|
||||||
|
this.skillRoot = skillRoot;
|
||||||
|
this.gitHooksDir = path.join(projectRoot, '.git', 'hooks');
|
||||||
|
this.claudeHooksDir = path.join(projectRoot, '.claude', 'hooks');
|
||||||
|
this.tddAutomationDir = path.join(projectRoot, '.tdd-automation');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install all TDD hooks
|
||||||
|
* @returns {object} Installation result
|
||||||
|
*/
|
||||||
|
installAll() {
|
||||||
|
const results = {
|
||||||
|
success: true,
|
||||||
|
installed: [],
|
||||||
|
failed: [],
|
||||||
|
skipped: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create directories
|
||||||
|
this.ensureDirectories();
|
||||||
|
|
||||||
|
// Copy TDD automation files
|
||||||
|
this.copyTddAutomationFiles(results);
|
||||||
|
|
||||||
|
// Install git pre-commit hook
|
||||||
|
this.installGitPreCommit(results);
|
||||||
|
|
||||||
|
// Install Claude hooks (if Claude hooks directory exists)
|
||||||
|
if (fs.existsSync(path.dirname(this.claudeHooksDir))) {
|
||||||
|
this.installClaudeHooks(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.success = results.failed.length === 0;
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure required directories exist
|
||||||
|
*/
|
||||||
|
ensureDirectories() {
|
||||||
|
const dirs = [
|
||||||
|
this.tddAutomationDir,
|
||||||
|
path.join(this.tddAutomationDir, 'scripts'),
|
||||||
|
path.join(this.tddAutomationDir, 'templates'),
|
||||||
|
path.join(this.tddAutomationDir, 'hooks')
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dir of dirs) {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Claude hooks directory if .claude exists
|
||||||
|
if (fs.existsSync(path.join(this.projectRoot, '.claude'))) {
|
||||||
|
if (!fs.existsSync(this.claudeHooksDir)) {
|
||||||
|
fs.mkdirSync(this.claudeHooksDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy TDD automation files to project
|
||||||
|
* @param {object} results - Results object to update
|
||||||
|
*/
|
||||||
|
copyTddAutomationFiles(results) {
|
||||||
|
const scriptsDir = path.join(this.skillRoot, 'scripts');
|
||||||
|
const templatesDir = path.join(this.skillRoot, 'templates');
|
||||||
|
const targetScriptsDir = path.join(this.tddAutomationDir, 'scripts');
|
||||||
|
const targetTemplatesDir = path.join(this.tddAutomationDir, 'templates');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Copy scripts
|
||||||
|
if (fs.existsSync(scriptsDir)) {
|
||||||
|
this.copyDirectory(scriptsDir, targetScriptsDir);
|
||||||
|
results.installed.push('TDD automation scripts');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy templates
|
||||||
|
if (fs.existsSync(templatesDir)) {
|
||||||
|
this.copyDirectory(templatesDir, targetTemplatesDir);
|
||||||
|
results.installed.push('TDD templates');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
results.failed.push(`Copy TDD files: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install git pre-commit hook
|
||||||
|
* @param {object} results - Results object to update
|
||||||
|
*/
|
||||||
|
installGitPreCommit(results) {
|
||||||
|
if (!fs.existsSync(this.gitHooksDir)) {
|
||||||
|
results.skipped.push('Git pre-commit hook (no .git directory)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookPath = path.join(this.gitHooksDir, 'pre-commit');
|
||||||
|
const templatePath = path.join(this.skillRoot, 'templates', 'pre-commit.sh');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if hook already exists
|
||||||
|
if (fs.existsSync(hookPath)) {
|
||||||
|
const existing = fs.readFileSync(hookPath, 'utf-8');
|
||||||
|
|
||||||
|
// Check if our TDD hook is already installed
|
||||||
|
if (existing.includes('TDD_AUTOMATION')) {
|
||||||
|
results.skipped.push('Git pre-commit hook (already installed)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup existing hook
|
||||||
|
const backupPath = hookPath + '.backup';
|
||||||
|
fs.copyFileSync(hookPath, backupPath);
|
||||||
|
results.installed.push(`Git pre-commit hook backup (${backupPath})`);
|
||||||
|
|
||||||
|
// Append our hook to existing
|
||||||
|
const tddHook = fs.readFileSync(templatePath, 'utf-8');
|
||||||
|
fs.appendFileSync(hookPath, '\n\n' + tddHook);
|
||||||
|
results.installed.push('Git pre-commit hook (appended)');
|
||||||
|
} else {
|
||||||
|
// Install new hook
|
||||||
|
fs.copyFileSync(templatePath, hookPath);
|
||||||
|
fs.chmodSync(hookPath, '755');
|
||||||
|
results.installed.push('Git pre-commit hook (new)');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
results.failed.push(`Git pre-commit hook: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install Claude hooks
|
||||||
|
* @param {object} results - Results object to update
|
||||||
|
*/
|
||||||
|
installClaudeHooks(results) {
|
||||||
|
const hooksToInstall = [
|
||||||
|
{
|
||||||
|
name: 'tdd-auto-enforcer.sh',
|
||||||
|
description: 'TDD auto-enforcer hook'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const hook of hooksToInstall) {
|
||||||
|
const sourcePath = path.join(this.skillRoot, 'templates', hook.name);
|
||||||
|
const targetPath = path.join(this.claudeHooksDir, hook.name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(targetPath)) {
|
||||||
|
results.skipped.push(`${hook.description} (already exists)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(sourcePath)) {
|
||||||
|
results.failed.push(`${hook.description} (template not found)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(sourcePath, targetPath);
|
||||||
|
fs.chmodSync(targetPath, '755');
|
||||||
|
results.installed.push(hook.description);
|
||||||
|
} catch (error) {
|
||||||
|
results.failed.push(`${hook.description}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy directory recursively
|
||||||
|
* @param {string} src - Source directory
|
||||||
|
* @param {string} dest - Destination directory
|
||||||
|
*/
|
||||||
|
copyDirectory(src, dest) {
|
||||||
|
if (!fs.existsSync(dest)) {
|
||||||
|
fs.mkdirSync(dest, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const srcPath = path.join(src, entry.name);
|
||||||
|
const destPath = path.join(dest, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
this.copyDirectory(srcPath, destPath);
|
||||||
|
} else {
|
||||||
|
fs.copyFileSync(srcPath, destPath);
|
||||||
|
|
||||||
|
// Make scripts executable
|
||||||
|
if (entry.name.endsWith('.sh') || entry.name.endsWith('.js')) {
|
||||||
|
try {
|
||||||
|
fs.chmodSync(destPath, '755');
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore chmod errors on systems that don't support it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstall all TDD hooks
|
||||||
|
* @returns {object} Uninstallation result
|
||||||
|
*/
|
||||||
|
uninstallAll() {
|
||||||
|
const results = {
|
||||||
|
success: true,
|
||||||
|
removed: [],
|
||||||
|
failed: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove git pre-commit hook (only our section)
|
||||||
|
this.uninstallGitPreCommit(results);
|
||||||
|
|
||||||
|
// Remove Claude hooks
|
||||||
|
this.uninstallClaudeHooks(results);
|
||||||
|
|
||||||
|
// Remove TDD automation directory
|
||||||
|
if (fs.existsSync(this.tddAutomationDir)) {
|
||||||
|
try {
|
||||||
|
fs.rmSync(this.tddAutomationDir, { recursive: true, force: true });
|
||||||
|
results.removed.push('.tdd-automation directory');
|
||||||
|
} catch (error) {
|
||||||
|
results.failed.push(`.tdd-automation directory: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.success = results.failed.length === 0;
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstall git pre-commit hook
|
||||||
|
* @param {object} results - Results object to update
|
||||||
|
*/
|
||||||
|
uninstallGitPreCommit(results) {
|
||||||
|
const hookPath = path.join(this.gitHooksDir, 'pre-commit');
|
||||||
|
|
||||||
|
if (!fs.existsSync(hookPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(hookPath, 'utf-8');
|
||||||
|
|
||||||
|
// Check if our hook is installed
|
||||||
|
if (!content.includes('TDD_AUTOMATION')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the entire file is our hook, remove it
|
||||||
|
if (content.trim().startsWith('#!/bin/bash') && content.includes('TDD_AUTOMATION')) {
|
||||||
|
// Check if there's a backup
|
||||||
|
const backupPath = hookPath + '.backup';
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
fs.copyFileSync(backupPath, hookPath);
|
||||||
|
results.removed.push('Git pre-commit hook (restored from backup)');
|
||||||
|
} else {
|
||||||
|
fs.unlinkSync(hookPath);
|
||||||
|
results.removed.push('Git pre-commit hook (removed)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
results.failed.push(`Git pre-commit hook: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstall Claude hooks
|
||||||
|
* @param {object} results - Results object to update
|
||||||
|
*/
|
||||||
|
uninstallClaudeHooks(results) {
|
||||||
|
const hooksToRemove = ['tdd-auto-enforcer.sh'];
|
||||||
|
|
||||||
|
for (const hookName of hooksToRemove) {
|
||||||
|
const hookPath = path.join(this.claudeHooksDir, hookName);
|
||||||
|
|
||||||
|
if (fs.existsSync(hookPath)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(hookPath);
|
||||||
|
results.removed.push(`Claude hook: ${hookName}`);
|
||||||
|
} catch (error) {
|
||||||
|
results.failed.push(`Claude hook ${hookName}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HookInstaller;
|
||||||
286
skills/tdd-automation/utils/merge-claude-md.js
Normal file
286
skills/tdd-automation/utils/merge-claude-md.js
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLAUDE.md Merger
|
||||||
|
*
|
||||||
|
* Safely merges TDD automation configuration into existing CLAUDE.md files
|
||||||
|
* while preserving all existing content.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class ClaudeMdMerger {
|
||||||
|
constructor(existingContent = '') {
|
||||||
|
this.existingContent = existingContent;
|
||||||
|
this.version = '0.2.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge TDD section with existing content
|
||||||
|
* @returns {string} Merged CLAUDE.md content
|
||||||
|
*/
|
||||||
|
merge() {
|
||||||
|
// Strategy: Append TDD section with clear delimiters
|
||||||
|
// This preserves ALL existing content
|
||||||
|
|
||||||
|
const separator = '\n\n' + '='.repeat(80) + '\n\n';
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
const mergedContent = [
|
||||||
|
this.existingContent.trimEnd(),
|
||||||
|
separator,
|
||||||
|
'<!-- TDD_AUTOMATION_START -->',
|
||||||
|
`<!-- Added by tdd-automation skill -->`,
|
||||||
|
`<!-- Version: ${this.version} -->`,
|
||||||
|
`<!-- Date: ${timestamp} -->`,
|
||||||
|
'',
|
||||||
|
this.buildTddSection(),
|
||||||
|
'',
|
||||||
|
'<!-- TDD_AUTOMATION_END -->',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
return mergedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the complete TDD automation section
|
||||||
|
* @returns {string} TDD section content
|
||||||
|
*/
|
||||||
|
buildTddSection() {
|
||||||
|
return `# TDD Red-Green-Refactor Automation (Auto-Installed)
|
||||||
|
|
||||||
|
## ⚠️ CRITICAL: TDD is MANDATORY for all feature development
|
||||||
|
|
||||||
|
\`\`\`yaml
|
||||||
|
tdd-automation-version: ${this.version}
|
||||||
|
tdd-enforcement-level: strict
|
||||||
|
tdd-phase-tracking: required
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Development Workflow (STRICTLY ENFORCED)
|
||||||
|
|
||||||
|
When implementing ANY new feature or functionality, you MUST follow this sequence:
|
||||||
|
|
||||||
|
### Phase 1: RED (Write Failing Test First)
|
||||||
|
|
||||||
|
**ALWAYS START HERE. DO NOT SKIP.**
|
||||||
|
|
||||||
|
1. **Create or modify the test file BEFORE writing ANY implementation code**
|
||||||
|
2. Write a test that describes the expected behavior
|
||||||
|
3. Run the test: \`npm run test:tdd -- <test-file>\`
|
||||||
|
4. **VERIFY the test fails for the RIGHT reason** (not syntax error, but missing functionality)
|
||||||
|
5. Use TodoWrite to mark RED phase complete
|
||||||
|
|
||||||
|
**DO NOT proceed to implementation until test fails correctly.**
|
||||||
|
|
||||||
|
#### RED Phase Checklist:
|
||||||
|
- [ ] Test file created/modified
|
||||||
|
- [ ] Test describes expected behavior clearly
|
||||||
|
- [ ] Test executed and verified to fail
|
||||||
|
- [ ] Failure reason is correct (missing functionality, not syntax error)
|
||||||
|
- [ ] RED phase marked in TodoWrite
|
||||||
|
|
||||||
|
### Phase 2: GREEN (Minimal Implementation)
|
||||||
|
|
||||||
|
**Only after RED phase is verified:**
|
||||||
|
|
||||||
|
1. Write ONLY enough code to make the failing test pass
|
||||||
|
2. No extra features, no "while we're here" additions
|
||||||
|
3. Keep it simple and focused on passing the test
|
||||||
|
4. Run test: \`npm run test:tdd -- <test-file>\`
|
||||||
|
5. **VERIFY the test now passes**
|
||||||
|
6. Use TodoWrite to mark GREEN phase complete
|
||||||
|
|
||||||
|
#### GREEN Phase Checklist:
|
||||||
|
- [ ] Minimal code written (no extras)
|
||||||
|
- [ ] Test executed and verified to pass
|
||||||
|
- [ ] All existing tests still pass
|
||||||
|
- [ ] GREEN phase marked in TodoWrite
|
||||||
|
|
||||||
|
### Phase 3: REFACTOR (Improve Quality)
|
||||||
|
|
||||||
|
**Only after GREEN phase is verified:**
|
||||||
|
|
||||||
|
1. Improve code structure, naming, and quality
|
||||||
|
2. Extract duplicated code
|
||||||
|
3. Simplify complex logic
|
||||||
|
4. Run full test suite: \`npm run test:tdd\`
|
||||||
|
5. **VERIFY all tests still pass after refactoring**
|
||||||
|
6. Use TodoWrite to mark REFACTOR phase complete
|
||||||
|
|
||||||
|
#### REFACTOR Phase Checklist:
|
||||||
|
- [ ] Code structure improved
|
||||||
|
- [ ] Duplicated code extracted
|
||||||
|
- [ ] Complex logic simplified
|
||||||
|
- [ ] All tests still pass
|
||||||
|
- [ ] REFACTOR phase marked in TodoWrite
|
||||||
|
|
||||||
|
## Pre-Implementation TodoWrite Template (ALWAYS Use)
|
||||||
|
|
||||||
|
Before writing ANY implementation code, create a todo list with these phases:
|
||||||
|
|
||||||
|
\`\`\`markdown
|
||||||
|
[ ] RED: Write failing test for [feature name]
|
||||||
|
[ ] Verify test fails with expected error message
|
||||||
|
[ ] GREEN: Implement minimal code to pass test
|
||||||
|
[ ] Verify test passes
|
||||||
|
[ ] REFACTOR: Improve code quality (if needed)
|
||||||
|
[ ] Verify all tests still pass
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
\`\`\`markdown
|
||||||
|
[ ] RED: Write failing test for user authentication
|
||||||
|
[ ] Verify test fails with "authenticateUser is not defined"
|
||||||
|
[ ] GREEN: Implement minimal authenticateUser function
|
||||||
|
[ ] Verify test passes
|
||||||
|
[ ] REFACTOR: Extract validation logic to separate function
|
||||||
|
[ ] Verify all tests still pass
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Critical Rules (NEVER Violate)
|
||||||
|
|
||||||
|
- ❌ **NEVER** write implementation code before writing the test
|
||||||
|
- ❌ **NEVER** skip the RED phase verification
|
||||||
|
- ❌ **NEVER** skip the GREEN phase verification
|
||||||
|
- ❌ **NEVER** skip TodoWrite phase tracking
|
||||||
|
- ✅ **ALWAYS** run tests to verify RED and GREEN states
|
||||||
|
- ✅ **ALWAYS** use TodoWrite to track TDD phases
|
||||||
|
- ✅ **ALWAYS** create test files BEFORE implementation files
|
||||||
|
- ✅ **ALWAYS** commit tests BEFORE committing implementation
|
||||||
|
- ✅ **ALWAYS** use semantic test names: \`"should [behavior] when [condition]"\`
|
||||||
|
|
||||||
|
## Test Execution Commands
|
||||||
|
|
||||||
|
This project has been configured with TDD-optimized test scripts:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Run all tests once (non-watch mode)
|
||||||
|
npm run test:tdd
|
||||||
|
|
||||||
|
# Run tests in watch mode for development
|
||||||
|
npm run test:tdd:watch
|
||||||
|
|
||||||
|
# Run specific test file (RED/GREEN phase)
|
||||||
|
npm run test:tdd -- path/to/test.test.ts
|
||||||
|
|
||||||
|
# Validate TDD compliance
|
||||||
|
npm run validate:tdd
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## File Structure Convention
|
||||||
|
|
||||||
|
For every implementation file, there must be a corresponding test file:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
src/features/auth/login.ts → Implementation
|
||||||
|
src/features/auth/login.test.ts → Tests (created FIRST)
|
||||||
|
|
||||||
|
src/utils/validation.ts → Implementation
|
||||||
|
src/utils/validation.test.ts → Tests (created FIRST)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Test Naming Convention
|
||||||
|
|
||||||
|
Use the pattern: \`"should [behavior] when [condition]"\`
|
||||||
|
|
||||||
|
**Good Examples:**
|
||||||
|
- \`"should return true when email is valid"\`
|
||||||
|
- \`"should throw error when password is too short"\`
|
||||||
|
- \`"should update user profile when data is valid"\`
|
||||||
|
|
||||||
|
**Bad Examples:**
|
||||||
|
- \`"test email validation"\` (not descriptive)
|
||||||
|
- \`"it works"\` (not specific)
|
||||||
|
- \`"validates email"\` (missing condition)
|
||||||
|
|
||||||
|
## Violation Handling
|
||||||
|
|
||||||
|
If you accidentally start writing implementation before tests:
|
||||||
|
|
||||||
|
1. **STOP immediately**
|
||||||
|
2. Create the test file first
|
||||||
|
3. Write the failing test
|
||||||
|
4. Verify RED state
|
||||||
|
5. **THEN** proceed with implementation
|
||||||
|
|
||||||
|
## TDD Automation Features
|
||||||
|
|
||||||
|
This installation includes:
|
||||||
|
|
||||||
|
- ✅ **Pre-commit hooks**: Validate tests exist before committing implementation
|
||||||
|
- ✅ **Test scaffolding**: Generate test file templates with \`npm run generate:test <file>\`
|
||||||
|
- ✅ **TDD validation**: Check compliance with \`npm run validate:tdd\`
|
||||||
|
- ✅ **Rollback capability**: Restore previous CLAUDE.md if needed
|
||||||
|
|
||||||
|
## Help & Maintenance
|
||||||
|
|
||||||
|
### Check TDD Compliance
|
||||||
|
\`\`\`bash
|
||||||
|
npm run validate:tdd
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Generate Test Template
|
||||||
|
\`\`\`bash
|
||||||
|
npm run generate:test src/features/auth/login.ts
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Rollback This Automation
|
||||||
|
\`\`\`bash
|
||||||
|
node .tdd-automation/scripts/rollback-tdd.js
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Remove TDD Section
|
||||||
|
\`\`\`bash
|
||||||
|
node .tdd-automation/scripts/remove-tdd-section.js
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### View Backups
|
||||||
|
\`\`\`bash
|
||||||
|
ls -lh .claude/CLAUDE.md.backup.*
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- Setup documentation: \`.tdd-automation/README.md\`
|
||||||
|
- Pre-commit hook: \`.git/hooks/pre-commit\`
|
||||||
|
- Test templates: \`.tdd-automation/templates/\`
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
Track these metrics to measure TDD adherence:
|
||||||
|
|
||||||
|
- Test-first adherence rate: >95%
|
||||||
|
- RED-GREEN-REFACTOR cycle completion: >90%
|
||||||
|
- Defect escape rate: <2%
|
||||||
|
- Test coverage: >80%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note:** This section was automatically added by the tdd-automation skill.
|
||||||
|
For support or to report issues, see: .tdd-automation/README.md`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create standalone TDD section (for new CLAUDE.md files)
|
||||||
|
* @returns {string} TDD section content without existing content
|
||||||
|
*/
|
||||||
|
static createNew() {
|
||||||
|
const merger = new ClaudeMdMerger('');
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'<!-- TDD_AUTOMATION_START -->',
|
||||||
|
`<!-- Added by tdd-automation skill -->`,
|
||||||
|
`<!-- Version: 0.2.0 -->`,
|
||||||
|
`<!-- Date: ${timestamp} -->`,
|
||||||
|
'',
|
||||||
|
merger.buildTddSection(),
|
||||||
|
'',
|
||||||
|
'<!-- TDD_AUTOMATION_END -->',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ClaudeMdMerger;
|
||||||
186
skills/tdd-automation/utils/update-package-json.js
Normal file
186
skills/tdd-automation/utils/update-package-json.js
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Package.json Updater
|
||||||
|
*
|
||||||
|
* Safely adds TDD-related npm scripts to package.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class PackageJsonUpdater {
|
||||||
|
constructor(projectRoot) {
|
||||||
|
this.projectRoot = projectRoot;
|
||||||
|
this.packageJsonPath = path.join(projectRoot, 'package.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add TDD scripts to package.json
|
||||||
|
* @param {string} testCommand - Base test command (e.g., 'vitest --run')
|
||||||
|
* @returns {object} Update result
|
||||||
|
*/
|
||||||
|
addTddScripts(testCommand = 'vitest --run') {
|
||||||
|
if (!fs.existsSync(this.packageJsonPath)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: 'package.json not found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create backup
|
||||||
|
const backupPath = this.packageJsonPath + '.backup';
|
||||||
|
fs.copyFileSync(this.packageJsonPath, backupPath);
|
||||||
|
|
||||||
|
// Read and parse package.json
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf-8'));
|
||||||
|
|
||||||
|
// Ensure scripts object exists
|
||||||
|
if (!packageJson.scripts) {
|
||||||
|
packageJson.scripts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define TDD scripts
|
||||||
|
const tddScripts = {
|
||||||
|
'test:tdd': testCommand,
|
||||||
|
'test:tdd:watch': testCommand.replace('--run', '').trim(),
|
||||||
|
'test:red': `${testCommand} --reporter=verbose`,
|
||||||
|
'test:green': `${testCommand} --reporter=verbose`,
|
||||||
|
'validate:tdd': 'node .tdd-automation/scripts/validate-tdd.js',
|
||||||
|
'generate:test': 'node .tdd-automation/scripts/generate-test.js'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track what was added
|
||||||
|
const added = [];
|
||||||
|
const skipped = [];
|
||||||
|
|
||||||
|
// Add scripts that don't exist
|
||||||
|
for (const [key, value] of Object.entries(tddScripts)) {
|
||||||
|
if (!packageJson.scripts[key]) {
|
||||||
|
packageJson.scripts[key] = value;
|
||||||
|
added.push(key);
|
||||||
|
} else {
|
||||||
|
skipped.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write updated package.json with pretty formatting
|
||||||
|
fs.writeFileSync(
|
||||||
|
this.packageJsonPath,
|
||||||
|
JSON.stringify(packageJson, null, 2) + '\n',
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
added,
|
||||||
|
skipped,
|
||||||
|
backup: backupPath
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove TDD scripts from package.json
|
||||||
|
* @returns {object} Removal result
|
||||||
|
*/
|
||||||
|
removeTddScripts() {
|
||||||
|
if (!fs.existsSync(this.packageJsonPath)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: 'package.json not found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create backup
|
||||||
|
const backupPath = this.packageJsonPath + '.backup';
|
||||||
|
fs.copyFileSync(this.packageJsonPath, backupPath);
|
||||||
|
|
||||||
|
// Read and parse package.json
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf-8'));
|
||||||
|
|
||||||
|
if (!packageJson.scripts) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
removed: [],
|
||||||
|
backup: backupPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define TDD script keys to remove
|
||||||
|
const tddScriptKeys = [
|
||||||
|
'test:tdd',
|
||||||
|
'test:tdd:watch',
|
||||||
|
'test:red',
|
||||||
|
'test:green',
|
||||||
|
'validate:tdd',
|
||||||
|
'generate:test'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Track what was removed
|
||||||
|
const removed = [];
|
||||||
|
|
||||||
|
// Remove TDD scripts
|
||||||
|
for (const key of tddScriptKeys) {
|
||||||
|
if (packageJson.scripts[key]) {
|
||||||
|
delete packageJson.scripts[key];
|
||||||
|
removed.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write updated package.json
|
||||||
|
fs.writeFileSync(
|
||||||
|
this.packageJsonPath,
|
||||||
|
JSON.stringify(packageJson, null, 2) + '\n',
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
removed,
|
||||||
|
backup: backupPath
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if TDD scripts are already installed
|
||||||
|
* @returns {boolean} True if TDD scripts exist
|
||||||
|
*/
|
||||||
|
hasTddScripts() {
|
||||||
|
if (!fs.existsSync(this.packageJsonPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf-8'));
|
||||||
|
|
||||||
|
if (!packageJson.scripts) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any TDD scripts exist
|
||||||
|
const tddScriptKeys = ['test:tdd', 'validate:tdd', 'generate:test'];
|
||||||
|
return tddScriptKeys.some(key => packageJson.scripts[key]);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PackageJsonUpdater;
|
||||||
267
skills/tdd-automation/utils/validate-claude-md.js
Normal file
267
skills/tdd-automation/utils/validate-claude-md.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLAUDE.md Validator
|
||||||
|
*
|
||||||
|
* Validates and manages CLAUDE.md files with backup and rollback capabilities.
|
||||||
|
* Ensures safe installation of TDD automation without data loss.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class ClaudeMdValidator {
|
||||||
|
constructor(projectRoot) {
|
||||||
|
this.projectRoot = projectRoot;
|
||||||
|
this.claudeDir = path.join(projectRoot, '.claude');
|
||||||
|
this.claudeMdPath = path.join(this.claudeDir, 'CLAUDE.md');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate current CLAUDE.md state and determine installation strategy
|
||||||
|
* @returns {object} Validation result with strategy recommendation
|
||||||
|
*/
|
||||||
|
validate() {
|
||||||
|
const result = {
|
||||||
|
exists: false,
|
||||||
|
hasExistingContent: false,
|
||||||
|
hasTddSection: false,
|
||||||
|
needsBackup: false,
|
||||||
|
strategy: 'create', // create | merge | skip | abort
|
||||||
|
warnings: [],
|
||||||
|
size: 0,
|
||||||
|
path: this.claudeMdPath
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if CLAUDE.md exists
|
||||||
|
if (!fs.existsSync(this.claudeMdPath)) {
|
||||||
|
result.strategy = 'create';
|
||||||
|
result.warnings.push('No CLAUDE.md found - will create new file');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.exists = true;
|
||||||
|
const content = fs.readFileSync(this.claudeMdPath, 'utf-8');
|
||||||
|
result.size = content.length;
|
||||||
|
|
||||||
|
// Check if file has meaningful content (not just empty or whitespace)
|
||||||
|
if (content.trim().length > 0) {
|
||||||
|
result.hasExistingContent = true;
|
||||||
|
result.needsBackup = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if TDD automation is already installed
|
||||||
|
if (this.detectTddSection(content)) {
|
||||||
|
result.hasTddSection = true;
|
||||||
|
result.strategy = 'skip';
|
||||||
|
result.warnings.push('TDD automation already installed in CLAUDE.md');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine merge strategy
|
||||||
|
if (result.hasExistingContent) {
|
||||||
|
result.strategy = 'merge';
|
||||||
|
result.warnings.push(`Existing CLAUDE.md found (${result.size} bytes) - will merge`);
|
||||||
|
} else {
|
||||||
|
result.strategy = 'create';
|
||||||
|
result.warnings.push('Empty CLAUDE.md found - will replace');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if TDD automation section exists in content
|
||||||
|
* @param {string} content - CLAUDE.md content to check
|
||||||
|
* @returns {boolean} True if TDD section detected
|
||||||
|
*/
|
||||||
|
detectTddSection(content) {
|
||||||
|
const markers = [
|
||||||
|
'<!-- TDD_AUTOMATION_START -->',
|
||||||
|
'TDD Red-Green-Refactor Automation (Auto-Installed)',
|
||||||
|
'tdd-automation-version:'
|
||||||
|
];
|
||||||
|
|
||||||
|
return markers.some(marker => content.includes(marker));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create timestamped backup of current CLAUDE.md
|
||||||
|
* @returns {object} Backup result with success status and path
|
||||||
|
*/
|
||||||
|
createBackup() {
|
||||||
|
if (!fs.existsSync(this.claudeMdPath)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: 'No file to backup',
|
||||||
|
path: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure .claude directory exists
|
||||||
|
if (!fs.existsSync(this.claudeDir)) {
|
||||||
|
fs.mkdirSync(this.claudeDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const backupPath = path.join(
|
||||||
|
this.claudeDir,
|
||||||
|
`CLAUDE.md.backup.${timestamp}`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.copyFileSync(this.claudeMdPath, backupPath);
|
||||||
|
const originalSize = fs.statSync(this.claudeMdPath).size;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
path: backupPath,
|
||||||
|
originalSize,
|
||||||
|
timestamp
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: error.message,
|
||||||
|
path: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all backup files sorted by date (newest first)
|
||||||
|
* @returns {array} Array of backup file info objects
|
||||||
|
*/
|
||||||
|
listBackups() {
|
||||||
|
if (!fs.existsSync(this.claudeDir)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return fs.readdirSync(this.claudeDir)
|
||||||
|
.filter(f => f.startsWith('CLAUDE.md.backup'))
|
||||||
|
.map(f => ({
|
||||||
|
name: f,
|
||||||
|
path: path.join(this.claudeDir, f),
|
||||||
|
created: fs.statSync(path.join(this.claudeDir, f)).mtime,
|
||||||
|
size: fs.statSync(path.join(this.claudeDir, f)).size
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.created - a.created);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing backups:', error.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore CLAUDE.md from backup file
|
||||||
|
* @param {string} backupPath - Path to backup file
|
||||||
|
* @returns {object} Rollback result with success status
|
||||||
|
*/
|
||||||
|
rollback(backupPath) {
|
||||||
|
if (!fs.existsSync(backupPath)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: 'Backup file not found',
|
||||||
|
path: backupPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensure .claude directory exists
|
||||||
|
if (!fs.existsSync(this.claudeDir)) {
|
||||||
|
fs.mkdirSync(this.claudeDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(backupPath, this.claudeMdPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
restoredFrom: backupPath,
|
||||||
|
size: fs.statSync(this.claudeMdPath).size
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: error.message,
|
||||||
|
path: backupPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove TDD section from CLAUDE.md
|
||||||
|
* @returns {object} Removal result with success status
|
||||||
|
*/
|
||||||
|
removeTddSection() {
|
||||||
|
if (!fs.existsSync(this.claudeMdPath)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: 'CLAUDE.md not found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(this.claudeMdPath, 'utf-8');
|
||||||
|
|
||||||
|
// Check if TDD section exists
|
||||||
|
if (!this.detectTddSection(content)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: 'No TDD section found in CLAUDE.md'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create backup before removal
|
||||||
|
const backup = this.createBackup();
|
||||||
|
if (!backup.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: `Backup failed: ${backup.reason}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove TDD section using markers
|
||||||
|
const startMarker = '<!-- TDD_AUTOMATION_START -->';
|
||||||
|
const endMarker = '<!-- TDD_AUTOMATION_END -->';
|
||||||
|
|
||||||
|
const startIdx = content.indexOf(startMarker);
|
||||||
|
const endIdx = content.indexOf(endMarker);
|
||||||
|
|
||||||
|
if (startIdx === -1 || endIdx === -1) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: 'Could not find section markers for clean removal'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove section and clean up extra whitespace
|
||||||
|
const beforeSection = content.substring(0, startIdx).trimEnd();
|
||||||
|
const afterSection = content.substring(endIdx + endMarker.length).trimStart();
|
||||||
|
|
||||||
|
const cleanedContent = beforeSection + (afterSection ? '\n\n' + afterSection : '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(this.claudeMdPath, cleanedContent, 'utf-8');
|
||||||
|
|
||||||
|
const originalSize = content.length;
|
||||||
|
const newSize = cleanedContent.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
backup: backup.path,
|
||||||
|
originalSize,
|
||||||
|
newSize,
|
||||||
|
removed: originalSize - newSize
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Rollback on error
|
||||||
|
this.rollback(backup.path);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: `Write failed: ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ClaudeMdValidator;
|
||||||
Reference in New Issue
Block a user