Initial commit
This commit is contained in:
22
skills/testing/playwright-e2e-automation/CHANGELOG.md
Normal file
22
skills/testing/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/testing/playwright-e2e-automation/README.md
Normal file
387
skills/testing/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/testing/playwright-e2e-automation/SKILL.md
Normal file
141
skills/testing/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)
|
||||
@@ -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/testing/playwright-e2e-automation/data/common-ui-bugs.md
Normal file
557
skills/testing/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.
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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/testing/playwright-e2e-automation/scripts/README.md
Normal file
294
skills/testing/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;
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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/testing/tdd-automation/CHANGELOG.md
Normal file
14
skills/testing/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/testing/tdd-automation/README.md
Normal file
340
skills/testing/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/testing/tdd-automation/SKILL.md
Normal file
235
skills/testing/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/testing/tdd-automation/examples/.gitkeep
Normal file
0
skills/testing/tdd-automation/examples/.gitkeep
Normal file
417
skills/testing/tdd-automation/index.js
Executable file
417
skills/testing/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/testing/tdd-automation/reference/.gitkeep
Normal file
0
skills/testing/tdd-automation/reference/.gitkeep
Normal file
198
skills/testing/tdd-automation/scripts/generate-test.js
Executable file
198
skills/testing/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/testing/tdd-automation/scripts/remove-tdd-section.js
Executable file
125
skills/testing/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/testing/tdd-automation/scripts/rollback-tdd.js
Executable file
141
skills/testing/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/testing/tdd-automation/scripts/validate-tdd.js
Executable file
300
skills/testing/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/testing/tdd-automation/templates/pre-commit.sh
Executable file
110
skills/testing/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/testing/tdd-automation/templates/tdd-auto-enforcer.sh
Executable file
78
skills/testing/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/testing/tdd-automation/utils/detect-project-type.js
Normal file
163
skills/testing/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/testing/tdd-automation/utils/install-hooks.js
Normal file
305
skills/testing/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/testing/tdd-automation/utils/merge-claude-md.js
Normal file
286
skills/testing/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/testing/tdd-automation/utils/update-package-json.js
Normal file
186
skills/testing/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/testing/tdd-automation/utils/validate-claude-md.js
Normal file
267
skills/testing/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