Initial commit
This commit is contained in:
545
skills/visual-regression/SKILL.md
Normal file
545
skills/visual-regression/SKILL.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# Visual Regression Testing Setup Skill
|
||||
|
||||
**Auto-invokes when user says**:
|
||||
- "Set up visual regression"
|
||||
- "Add Chromatic tests"
|
||||
- "Create visual tests for [component]"
|
||||
- "Configure visual regression testing"
|
||||
- "Add screenshot testing"
|
||||
- "Set up Percy"
|
||||
- "Add BackstopJS"
|
||||
|
||||
---
|
||||
|
||||
## Skill Purpose
|
||||
|
||||
Generate complete visual regression testing setup with Storybook stories, configuration files, and CI/CD workflows.
|
||||
|
||||
**Supports**: Chromatic, Percy, BackstopJS
|
||||
**Frameworks**: React, Vue, Svelte (TypeScript/JavaScript)
|
||||
**CI/CD**: GitHub Actions, GitLab CI, CircleCI
|
||||
|
||||
---
|
||||
|
||||
## What This Skill Does
|
||||
|
||||
1. **Detects existing setup**: Storybook version, VR tool, CI platform
|
||||
2. **Validates component**: Extract props, variants, states
|
||||
3. **Generates stories**: Complete `.stories.tsx` with all variants
|
||||
4. **Creates config files**: Chromatic, Percy, or BackstopJS configuration
|
||||
5. **Sets up CI/CD**: Auto-generate workflow files
|
||||
6. **Provides instructions**: Next steps for API tokens, first baseline
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Validate Project Setup
|
||||
|
||||
**Execute**: `vr_setup_validator.py`
|
||||
|
||||
**Check**:
|
||||
- Framework (React/Vue/Svelte) from package.json
|
||||
- Existing Storybook config (.storybook/ directory)
|
||||
- Existing VR tool (chromatic, percy, backstopjs in dependencies)
|
||||
- CI platform (.github/, .gitlab-ci.yml, .circleci/)
|
||||
- Component file exists and is valid
|
||||
|
||||
**Output**:
|
||||
```json
|
||||
{
|
||||
"framework": "react",
|
||||
"storybook_version": "7.6.0",
|
||||
"vr_tool": "chromatic",
|
||||
"ci_platform": "github",
|
||||
"component": {
|
||||
"path": "src/components/ProfileCard.tsx",
|
||||
"name": "ProfileCard",
|
||||
"props": [...],
|
||||
"valid": true
|
||||
},
|
||||
"dependencies": {
|
||||
"installed": ["@storybook/react", "@storybook/addon-essentials"],
|
||||
"missing": ["chromatic", "@chromatic-com/storybook"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**If Storybook not found**: Ask user if they want to install Storybook first, provide setup instructions.
|
||||
|
||||
**If multiple VR tools found**: Ask user which to use (Chromatic recommended).
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Generate Storybook Stories
|
||||
|
||||
**Execute**: `story_generator.py`
|
||||
|
||||
**Process**:
|
||||
1. Parse component file (TypeScript/JSX/Vue SFC)
|
||||
2. Extract props, prop types, default values
|
||||
3. Identify variants (size, variant, disabled, etc.)
|
||||
4. Generate story file from template
|
||||
5. Add accessibility tests (@storybook/addon-a11y)
|
||||
6. Add interaction tests (if @storybook/test available)
|
||||
|
||||
**Template**: `templates/story-template.tsx.j2`
|
||||
|
||||
**Example output** (`ProfileCard.stories.tsx`):
|
||||
```typescript
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ProfileCard } from './ProfileCard';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/ProfileCard',
|
||||
component: ProfileCard,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: { control: 'select', options: ['sm', 'md', 'lg'] },
|
||||
variant: { control: 'select', options: ['default', 'compact'] },
|
||||
},
|
||||
} satisfies Meta<typeof ProfileCard>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
name: 'John Doe',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
bio: 'Software Engineer',
|
||||
size: 'md',
|
||||
variant: 'default',
|
||||
},
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
size: 'sm',
|
||||
},
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
size: 'lg',
|
||||
},
|
||||
};
|
||||
|
||||
export const Compact: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
variant: 'compact',
|
||||
},
|
||||
};
|
||||
|
||||
// Accessibility test
|
||||
Default.parameters = {
|
||||
a11y: {
|
||||
config: {
|
||||
rules: [
|
||||
{ id: 'color-contrast', enabled: true },
|
||||
{ id: 'label', enabled: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Write to**: `{component_directory}/{ComponentName}.stories.tsx`
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Generate Configuration Files
|
||||
|
||||
**Execute**: `chromatic_config_generator.py` (or percy/backstop equivalent)
|
||||
|
||||
#### For Chromatic:
|
||||
|
||||
**Generate 3 files**:
|
||||
|
||||
1. **chromatic.config.json**:
|
||||
```json
|
||||
{
|
||||
"projectId": "<PROJECT_ID_PLACEHOLDER>",
|
||||
"buildScriptName": "build-storybook",
|
||||
"exitZeroOnChanges": true,
|
||||
"exitOnceUploaded": true,
|
||||
"onlyChanged": true,
|
||||
"externals": ["public/**"],
|
||||
"skip": "dependabot/**",
|
||||
"ignoreLastBuildOnBranch": "main"
|
||||
}
|
||||
```
|
||||
|
||||
2. **Update .storybook/main.js** (add addon):
|
||||
```javascript
|
||||
module.exports = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@chromatic-com/storybook', // ← Added
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
3. **Update package.json** (add scripts):
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"chromatic": "npx chromatic",
|
||||
"chromatic:ci": "npx chromatic --exit-zero-on-changes"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**For Percy**: Generate `.percy.yml` instead
|
||||
**For BackstopJS**: Generate `backstop.config.js` instead
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Generate CI/CD Workflow
|
||||
|
||||
**Execute**: `ci_workflow_generator.py`
|
||||
|
||||
**Detect CI platform** from existing files:
|
||||
- `.github/workflows/` → GitHub Actions
|
||||
- `.gitlab-ci.yml` → GitLab CI
|
||||
- `.circleci/config.yml` → CircleCI
|
||||
- None → Ask user, default to GitHub Actions
|
||||
|
||||
#### GitHub Actions Example:
|
||||
|
||||
**Generate**: `.github/workflows/chromatic.yml`
|
||||
|
||||
```yaml
|
||||
name: Visual Regression Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
chromatic:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Required for Chromatic
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run Chromatic
|
||||
uses: chromaui/action@latest
|
||||
with:
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
exitZeroOnChanges: true
|
||||
onlyChanged: true
|
||||
```
|
||||
|
||||
**For GitLab CI**: Add job to `.gitlab-ci.yml`
|
||||
**For CircleCI**: Add job to `.circleci/config.yml`
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Provide Setup Instructions
|
||||
|
||||
**Output to user**:
|
||||
|
||||
````markdown
|
||||
✅ Visual regression testing setup complete!
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
✅ {ComponentName}.stories.tsx (Storybook story with variants)
|
||||
✅ chromatic.config.json (Chromatic configuration)
|
||||
✅ .storybook/main.js (Added @chromatic-com/storybook addon)
|
||||
✅ package.json (Added chromatic scripts)
|
||||
✅ .github/workflows/chromatic.yml (CI workflow)
|
||||
|
||||
## Next Steps
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install --save-dev chromatic @chromatic-com/storybook
|
||||
```
|
||||
|
||||
### 2. Create Chromatic Project
|
||||
|
||||
1. Go to https://www.chromatic.com/start
|
||||
2. Sign in with GitHub
|
||||
3. Create new project
|
||||
4. Copy project token
|
||||
|
||||
### 3. Add Secret to GitHub
|
||||
|
||||
1. Go to repository Settings → Secrets and variables → Actions
|
||||
2. Create secret: `CHROMATIC_PROJECT_TOKEN`
|
||||
3. Paste your project token
|
||||
|
||||
### 4. Update chromatic.config.json
|
||||
|
||||
Replace `<PROJECT_ID_PLACEHOLDER>` with your actual project ID from Chromatic dashboard.
|
||||
|
||||
### 5. Create Baseline
|
||||
|
||||
```bash
|
||||
npm run chromatic
|
||||
```
|
||||
|
||||
This captures the initial screenshots as your baseline.
|
||||
|
||||
### 6. Test Visual Regression
|
||||
|
||||
1. Make a visual change to ProfileCard
|
||||
2. Commit and push
|
||||
3. CI will run Chromatic automatically
|
||||
4. Review changes in Chromatic dashboard
|
||||
|
||||
## Documentation
|
||||
|
||||
See `.agent/sops/testing/visual-regression-setup.md` for detailed workflow.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Storybook build fails**: Ensure all component dependencies are installed
|
||||
**Chromatic upload fails**: Check project token in secrets
|
||||
**No changes detected**: Chromatic only runs on changed stories (use `--force-rebuild` to test)
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## Predefined Functions Reference
|
||||
|
||||
### vr_setup_validator.py
|
||||
|
||||
```python
|
||||
def detect_storybook_config(project_root: str) -> dict
|
||||
def detect_vr_tool(project_root: str) -> str
|
||||
def validate_component_path(component_path: str) -> dict
|
||||
def check_dependencies(project_root: str) -> dict
|
||||
```
|
||||
|
||||
**Returns**: Validation report with detected setup and missing dependencies
|
||||
|
||||
### story_generator.py
|
||||
|
||||
```python
|
||||
def analyze_component(component_path: str, framework: str) -> dict
|
||||
def generate_story(component_info: dict, template_path: str) -> str
|
||||
def create_accessibility_tests(component_info: dict) -> str
|
||||
def create_interaction_tests(component_info: dict) -> str
|
||||
```
|
||||
|
||||
**Returns**: Generated story file content
|
||||
|
||||
### chromatic_config_generator.py
|
||||
|
||||
```python
|
||||
def generate_chromatic_config(project_info: dict) -> str
|
||||
def generate_storybook_config(existing_config: dict) -> str
|
||||
def generate_package_scripts(existing_scripts: dict) -> dict
|
||||
def generate_percy_config(project_info: dict) -> str # Percy alternative
|
||||
def generate_backstop_config(project_info: dict) -> str # BackstopJS alternative
|
||||
```
|
||||
|
||||
**Returns**: Configuration file contents as strings
|
||||
|
||||
### ci_workflow_generator.py
|
||||
|
||||
```python
|
||||
def detect_ci_platform(project_root: str) -> str
|
||||
def generate_github_workflow(project_info: dict) -> str
|
||||
def generate_gitlab_ci(project_info: dict) -> str
|
||||
def generate_circleci_config(project_info: dict) -> str
|
||||
```
|
||||
|
||||
**Returns**: CI workflow file contents
|
||||
|
||||
---
|
||||
|
||||
## Templates Reference
|
||||
|
||||
- **story-template.tsx.j2**: React/TypeScript story template
|
||||
- **story-template.vue.j2**: Vue SFC story template
|
||||
- **chromatic-config.json.j2**: Chromatic configuration
|
||||
- **percy-config.yml.j2**: Percy configuration
|
||||
- **github-workflow.yml.j2**: GitHub Actions workflow
|
||||
- **gitlab-ci.yml.j2**: GitLab CI job
|
||||
- **storybook-main.js.j2**: Storybook addon configuration
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Simple Component
|
||||
|
||||
```
|
||||
User: "Set up visual regression for ProfileCard component"
|
||||
|
||||
→ Detects: React, existing Storybook, no VR tool
|
||||
→ Generates: ProfileCard.stories.tsx with 4 variants
|
||||
→ Creates: Chromatic config, GitHub workflow
|
||||
→ Outputs: Setup instructions
|
||||
```
|
||||
|
||||
See: `examples/simple-component-vr.md`
|
||||
|
||||
### Example 2: Full Design System
|
||||
|
||||
```
|
||||
User: "Set up visual regression for entire design system"
|
||||
|
||||
→ Detects: React, Storybook, components in src/components/
|
||||
→ Generates: Stories for all components (Button, Input, Card, etc.)
|
||||
→ Creates: Chromatic config with design token validation
|
||||
→ Outputs: Bulk setup instructions
|
||||
```
|
||||
|
||||
See: `examples/design-system-vr.md`
|
||||
|
||||
### Example 3: Existing Storybook
|
||||
|
||||
```
|
||||
User: "Add Chromatic to existing Storybook"
|
||||
|
||||
→ Detects: Storybook v7, existing stories
|
||||
→ Adds: @chromatic-com/storybook addon
|
||||
→ Creates: Chromatic config, CI workflow
|
||||
→ Preserves: Existing stories and configuration
|
||||
```
|
||||
|
||||
See: `examples/existing-storybook-vr.md`
|
||||
|
||||
---
|
||||
|
||||
## Integration with product-design Skill
|
||||
|
||||
After `product-design` generates implementation plan, suggest visual regression:
|
||||
|
||||
```
|
||||
"Implementation plan created! Consider setting up visual regression testing:
|
||||
|
||||
'Set up visual regression for {ComponentName}'
|
||||
|
||||
This ensures pixel-perfect implementation and prevents visual drift."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Comparison
|
||||
|
||||
### Chromatic (Recommended)
|
||||
- ✅ Purpose-built for Storybook
|
||||
- ✅ Component-focused testing
|
||||
- ✅ UI review workflow
|
||||
- ✅ Free tier: 5,000 snapshots/month
|
||||
- ❌ Requires cloud service
|
||||
|
||||
### Percy
|
||||
- ✅ Multi-framework support
|
||||
- ✅ Responsive testing
|
||||
- ✅ Visual reviews
|
||||
- ❌ More expensive
|
||||
- ❌ Less Storybook-specific
|
||||
|
||||
### BackstopJS
|
||||
- ✅ Open source, self-hosted
|
||||
- ✅ No cloud dependency
|
||||
- ✅ Free
|
||||
- ❌ More manual setup
|
||||
- ❌ Less automation
|
||||
|
||||
**Default**: Chromatic (best Storybook integration)
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Component Not Found
|
||||
```
|
||||
Error: Component file not found at {path}
|
||||
|
||||
Please provide correct path:
|
||||
"Set up visual regression for src/components/ProfileCard.tsx"
|
||||
```
|
||||
|
||||
### Storybook Not Installed
|
||||
```
|
||||
Storybook not detected. Install first:
|
||||
|
||||
npm install --save-dev @storybook/react @storybook/addon-essentials
|
||||
npx storybook init
|
||||
|
||||
Then retry: "Set up visual regression for ProfileCard"
|
||||
```
|
||||
|
||||
### Multiple VR Tools Detected
|
||||
```
|
||||
Multiple VR tools found: chromatic, percy
|
||||
|
||||
Which should I use?
|
||||
- "Use Chromatic for visual regression"
|
||||
- "Use Percy for visual regression"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start with key components**: Don't test everything, focus on design system primitives
|
||||
2. **Use interaction tests**: Combine visual + functional testing
|
||||
3. **Baseline on main**: Always merge baselines to main branch
|
||||
4. **Review changes**: Don't auto-accept visual changes
|
||||
5. **Test states**: Capture hover, focus, error states
|
||||
6. **Accessibility**: Include a11y tests in all stories
|
||||
|
||||
---
|
||||
|
||||
## Token Efficiency
|
||||
|
||||
**Traditional approach** (50k tokens):
|
||||
1. Read Storybook docs (20k)
|
||||
2. Read Chromatic docs (15k)
|
||||
3. Write stories manually (10k)
|
||||
4. Configure CI (5k)
|
||||
|
||||
**With visual-regression skill** (3k tokens):
|
||||
1. Skill auto-invokes (0 tokens)
|
||||
2. Instructions load (3k tokens)
|
||||
3. Functions execute (0 tokens)
|
||||
|
||||
**Savings**: 94% (47k tokens)
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
- **v3.3.0**: Initial release with Chromatic support
|
||||
- **Future**: Percy, BackstopJS, Vue, Svelte support
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-10-21
|
||||
**Skill Type**: Project-specific
|
||||
**Generator**: nav-skill-creator (self-improving)
|
||||
107
skills/visual-regression/examples/design-system-vr.md
Normal file
107
skills/visual-regression/examples/design-system-vr.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Example: Visual Regression for Full Design System
|
||||
|
||||
Setup visual regression for entire design system with token validation.
|
||||
|
||||
---
|
||||
|
||||
## Scenario
|
||||
|
||||
You have a design system with multiple components:
|
||||
- Button, Input, Card, Avatar, Badge, Modal, etc.
|
||||
- Design tokens extracted from Figma (via product-design skill)
|
||||
- Want to ensure pixel-perfect implementation
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
"Set up visual regression for entire design system in src/components"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Skill Does
|
||||
|
||||
1. **Discovers components**: Scans `src/components/` directory
|
||||
2. **Generates stories**: Creates `.stories.tsx` for each component
|
||||
3. **Token validation**: Compares CSS values to design tokens
|
||||
4. **Bulk setup**: Single Chromatic config for all components
|
||||
|
||||
---
|
||||
|
||||
## Generated Files
|
||||
|
||||
```
|
||||
src/components/
|
||||
├── Button/
|
||||
│ ├── Button.tsx
|
||||
│ └── Button.stories.tsx # ← Generated
|
||||
├── Input/
|
||||
│ ├── Input.tsx
|
||||
│ └── Input.stories.tsx # ← Generated
|
||||
├── Card/
|
||||
│ ├── Card.tsx
|
||||
│ └── Card.stories.tsx # ← Generated
|
||||
...
|
||||
|
||||
chromatic.config.json # ← Generated
|
||||
.github/workflows/chromatic.yml # ← Generated
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with product-design Skill
|
||||
|
||||
If you used `product-design` skill to extract Figma tokens:
|
||||
|
||||
```
|
||||
1. "Review this design from Figma"
|
||||
→ Extracts tokens to tokens.json
|
||||
|
||||
2. "Set up visual regression for design system"
|
||||
→ Generates stories with token values
|
||||
→ Validates implementation matches tokens
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Token Validation Example
|
||||
|
||||
**Design token** (from Figma):
|
||||
```json
|
||||
{
|
||||
"color": {
|
||||
"primary": {
|
||||
"value": "#3B82F6"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Story validation**:
|
||||
```typescript
|
||||
export const Primary: Story = {
|
||||
args: { variant: 'primary' },
|
||||
play: async ({ canvasElement }) => {
|
||||
const button = within(canvasElement).getByRole('button');
|
||||
const computedStyle = window.getComputedStyle(button);
|
||||
expect(computedStyle.backgroundColor).toBe('rgb(59, 130, 246)'); // #3B82F6
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Prevent drift**: Catch when code diverges from designs
|
||||
- **Scale testing**: Test 50+ components in one workflow
|
||||
- **Token enforcement**: Ensure design tokens are used correctly
|
||||
- **Design review**: Designers see visual diffs in Chromatic
|
||||
|
||||
---
|
||||
|
||||
**Time saved**: 6-10 hours → 15 minutes (95% reduction)
|
||||
**Components**: All in design system
|
||||
**Tokens validated**: Automatically
|
||||
109
skills/visual-regression/examples/existing-storybook-vr.md
Normal file
109
skills/visual-regression/examples/existing-storybook-vr.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Example: Add Chromatic to Existing Storybook
|
||||
|
||||
Add visual regression to project that already has Storybook configured.
|
||||
|
||||
---
|
||||
|
||||
## Scenario
|
||||
|
||||
- Storybook 7.x already installed and configured
|
||||
- Existing `.stories.tsx` files for components
|
||||
- Want to add Chromatic without breaking existing setup
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
"Add Chromatic to existing Storybook"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Skill Does
|
||||
|
||||
1. **Detects existing setup**: Reads `.storybook/main.js`, existing stories
|
||||
2. **Non-destructive update**: Only adds Chromatic addon
|
||||
3. **Preserves config**: Keeps existing addons, framework, settings
|
||||
4. **CI integration**: Generates workflow
|
||||
|
||||
---
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Before
|
||||
|
||||
**.storybook/main.js**:
|
||||
```javascript
|
||||
module.exports = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@chromatic-com/storybook', // ← Added
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Files
|
||||
|
||||
- `chromatic.config.json` (new)
|
||||
- `.github/workflows/chromatic.yml` (new)
|
||||
- `package.json` scripts updated
|
||||
|
||||
---
|
||||
|
||||
## No Stories Generated
|
||||
|
||||
Skill detects existing stories and **skips generation**:
|
||||
|
||||
```
|
||||
✅ Existing Storybook detected
|
||||
✅ Found 23 existing story files
|
||||
✅ Skipping story generation
|
||||
✅ Adding Chromatic configuration only
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
```bash
|
||||
# Install Chromatic
|
||||
npm install --save-dev chromatic @chromatic-com/storybook
|
||||
|
||||
# Run on existing stories
|
||||
npm run chromatic
|
||||
```
|
||||
|
||||
All existing stories are captured as baseline automatically.
|
||||
|
||||
---
|
||||
|
||||
**Time saved**: 1-2 hours → 3 minutes
|
||||
**Stories affected**: 0 (uses existing)
|
||||
**Breaking changes**: None
|
||||
297
skills/visual-regression/examples/simple-component-vr.md
Normal file
297
skills/visual-regression/examples/simple-component-vr.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# Example: Visual Regression for Simple Component
|
||||
|
||||
This example shows how to set up visual regression testing for a single React component.
|
||||
|
||||
---
|
||||
|
||||
## Scenario
|
||||
|
||||
You have a `ProfileCard` component that you want to test for visual regressions.
|
||||
|
||||
**Component**: `src/components/ProfileCard.tsx`
|
||||
|
||||
```typescript
|
||||
interface ProfileCardProps {
|
||||
name: string;
|
||||
avatar: string;
|
||||
bio: string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
variant?: 'default' | 'compact';
|
||||
}
|
||||
|
||||
export function ProfileCard({ name, avatar, bio, size = 'md', variant = 'default' }: ProfileCardProps) {
|
||||
// Component implementation
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Step 1: Invoke Skill
|
||||
|
||||
```
|
||||
"Set up visual regression for ProfileCard component"
|
||||
```
|
||||
|
||||
### Step 2: Skill Auto-Invokes
|
||||
|
||||
Navigator detects the command and invokes `visual-regression` skill.
|
||||
|
||||
### Step 3: Validation
|
||||
|
||||
Skill executes `vr_setup_validator.py`:
|
||||
|
||||
```json
|
||||
{
|
||||
"framework": "react",
|
||||
"storybook": {
|
||||
"installed": true,
|
||||
"version": "7.6.0"
|
||||
},
|
||||
"vr_tool": null,
|
||||
"ci_platform": "github",
|
||||
"component": {
|
||||
"valid": true,
|
||||
"path": "src/components/ProfileCard.tsx",
|
||||
"name": "ProfileCard"
|
||||
},
|
||||
"dependencies": {
|
||||
"installed": ["@storybook/react", "@storybook/addon-essentials"],
|
||||
"missing": ["chromatic", "@chromatic-com/storybook"]
|
||||
},
|
||||
"ready": true
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Generate Story
|
||||
|
||||
Skill executes `story_generator.py`:
|
||||
|
||||
**Created**: `src/components/ProfileCard.stories.tsx`
|
||||
|
||||
```typescript
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ProfileCard } from './ProfileCard';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/ProfileCard',
|
||||
component: ProfileCard,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: { control: 'select', options: ['sm', 'md', 'lg'] },
|
||||
variant: { control: 'select', options: ['default', 'compact'] },
|
||||
},
|
||||
} satisfies Meta<typeof ProfileCard>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
name: 'John Doe',
|
||||
avatar: 'https://via.placeholder.com/150',
|
||||
bio: 'Software Engineer',
|
||||
size: 'md',
|
||||
variant: 'default',
|
||||
},
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
size: 'sm',
|
||||
},
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
size: 'lg',
|
||||
},
|
||||
};
|
||||
|
||||
export const Compact: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
variant: 'compact',
|
||||
},
|
||||
};
|
||||
|
||||
// Accessibility tests
|
||||
Default.parameters = {
|
||||
a11y: {
|
||||
config: {
|
||||
rules: [
|
||||
{ id: 'color-contrast', enabled: true },
|
||||
{ id: 'label', enabled: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Step 5: Generate Chromatic Config
|
||||
|
||||
Skill executes `chromatic_config_generator.py`:
|
||||
|
||||
**Created**: `chromatic.config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"projectId": "<PROJECT_ID_PLACEHOLDER>",
|
||||
"buildScriptName": "build-storybook",
|
||||
"exitZeroOnChanges": true,
|
||||
"exitOnceUploaded": true,
|
||||
"onlyChanged": true,
|
||||
"externals": ["public/**"],
|
||||
"skip": "dependabot/**",
|
||||
"ignoreLastBuildOnBranch": "main"
|
||||
}
|
||||
```
|
||||
|
||||
**Updated**: `.storybook/main.js`
|
||||
|
||||
```diff
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
+ '@chromatic-com/storybook',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
```
|
||||
|
||||
**Updated**: `package.json`
|
||||
|
||||
```diff
|
||||
"scripts": {
|
||||
+ "chromatic": "npx chromatic",
|
||||
+ "chromatic:ci": "npx chromatic --exit-zero-on-changes",
|
||||
"build-storybook": "storybook build"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Generate CI Workflow
|
||||
|
||||
Skill executes `ci_workflow_generator.py`:
|
||||
|
||||
**Created**: `.github/workflows/chromatic.yml`
|
||||
|
||||
```yaml
|
||||
name: Visual Regression Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main', 'develop']
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
chromatic:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run Chromatic
|
||||
uses: chromaui/action@latest
|
||||
with:
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
exitZeroOnChanges: true
|
||||
onlyChanged: true
|
||||
autoAcceptChanges: 'main'
|
||||
```
|
||||
|
||||
### Step 7: Setup Instructions
|
||||
|
||||
Skill outputs:
|
||||
|
||||
```markdown
|
||||
✅ Visual regression testing setup complete!
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
✅ ProfileCard.stories.tsx (Storybook story with 4 variants)
|
||||
✅ chromatic.config.json (Chromatic configuration)
|
||||
✅ .storybook/main.js (Added @chromatic-com/storybook addon)
|
||||
✅ package.json (Added chromatic scripts)
|
||||
✅ .github/workflows/chromatic.yml (CI workflow)
|
||||
|
||||
## Next Steps
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install --save-dev chromatic @chromatic-com/storybook
|
||||
```
|
||||
|
||||
### 2. Create Chromatic Project
|
||||
|
||||
1. Go to https://www.chromatic.com/start
|
||||
2. Sign in with GitHub
|
||||
3. Create new project
|
||||
4. Copy project token
|
||||
|
||||
### 3. Add Secret to GitHub
|
||||
|
||||
1. Go to repository Settings → Secrets and variables → Actions
|
||||
2. Create secret: `CHROMATIC_PROJECT_TOKEN`
|
||||
3. Paste your project token
|
||||
|
||||
### 4. Update chromatic.config.json
|
||||
|
||||
Replace `<PROJECT_ID_PLACEHOLDER>` with your actual project ID.
|
||||
|
||||
### 5. Create Baseline
|
||||
|
||||
```bash
|
||||
npm run chromatic
|
||||
```
|
||||
|
||||
### 6. Test Visual Regression
|
||||
|
||||
1. Make a visual change to ProfileCard
|
||||
2. Commit and push
|
||||
3. CI will run Chromatic automatically
|
||||
4. Review changes in Chromatic dashboard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Result
|
||||
|
||||
**Time saved**: 2-3 hours → 5 minutes (96% reduction)
|
||||
|
||||
**Files created**: 4
|
||||
**Lines of code generated**: ~150
|
||||
**Token usage**: 3k tokens (vs 50k manual)
|
||||
|
||||
---
|
||||
|
||||
## Follow-up Tasks
|
||||
|
||||
After setup, you can:
|
||||
|
||||
1. **Add more variants**: Edit `ProfileCard.stories.tsx` to add edge cases
|
||||
2. **Customize Chromatic**: Adjust `chromatic.config.json` settings
|
||||
3. **Add interaction tests**: Use `@storybook/test` for click/focus states
|
||||
4. **Review visual diffs**: Monitor Chromatic dashboard for changes
|
||||
|
||||
---
|
||||
|
||||
**Skill used**: visual-regression
|
||||
**Time to complete**: ~5 minutes
|
||||
**Automated**: Story generation, config creation, CI setup
|
||||
363
skills/visual-regression/functions/chromatic_config_generator.py
Normal file
363
skills/visual-regression/functions/chromatic_config_generator.py
Normal file
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Chromatic Configuration Generator
|
||||
|
||||
Generates Chromatic config files, updates Storybook configuration,
|
||||
and adds package.json scripts for visual regression testing.
|
||||
|
||||
Usage:
|
||||
python chromatic_config_generator.py <project_root> [vr_tool]
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
def generate_chromatic_config(project_info: Dict) -> str:
|
||||
"""
|
||||
Generate chromatic.config.json content.
|
||||
|
||||
Args:
|
||||
project_info: Project information (main_branch, etc.)
|
||||
|
||||
Returns:
|
||||
JSON config as string
|
||||
"""
|
||||
main_branch = project_info.get('main_branch', 'main')
|
||||
|
||||
config = {
|
||||
"projectId": "<PROJECT_ID_PLACEHOLDER>",
|
||||
"buildScriptName": "build-storybook",
|
||||
"exitZeroOnChanges": True,
|
||||
"exitOnceUploaded": True,
|
||||
"onlyChanged": True,
|
||||
"externals": ["public/**"],
|
||||
"skip": "dependabot/**",
|
||||
"ignoreLastBuildOnBranch": main_branch
|
||||
}
|
||||
|
||||
return json.dumps(config, indent=2)
|
||||
|
||||
|
||||
def generate_percy_config(project_info: Dict) -> str:
|
||||
"""
|
||||
Generate .percy.yml content for Percy.
|
||||
|
||||
Args:
|
||||
project_info: Project information
|
||||
|
||||
Returns:
|
||||
YAML config as string
|
||||
"""
|
||||
config = """version: 2
|
||||
static:
|
||||
build-dir: storybook-static
|
||||
clean-urls: false
|
||||
snapshot:
|
||||
widths:
|
||||
- 375
|
||||
- 768
|
||||
- 1280
|
||||
min-height: 1024
|
||||
percy-css: ''
|
||||
"""
|
||||
return config
|
||||
|
||||
|
||||
def generate_backstop_config(project_info: Dict) -> str:
|
||||
"""
|
||||
Generate backstop.config.js for BackstopJS.
|
||||
|
||||
Args:
|
||||
project_info: Project information
|
||||
|
||||
Returns:
|
||||
JS config as string
|
||||
"""
|
||||
config = """module.exports = {
|
||||
id: 'backstop_default',
|
||||
viewports: [
|
||||
{
|
||||
label: 'phone',
|
||||
width: 375,
|
||||
height: 667
|
||||
},
|
||||
{
|
||||
label: 'tablet',
|
||||
width: 768,
|
||||
height: 1024
|
||||
},
|
||||
{
|
||||
label: 'desktop',
|
||||
width: 1280,
|
||||
height: 1024
|
||||
}
|
||||
],
|
||||
scenarios: [],
|
||||
paths: {
|
||||
bitmaps_reference: 'backstop_data/bitmaps_reference',
|
||||
bitmaps_test: 'backstop_data/bitmaps_test',
|
||||
engine_scripts: 'backstop_data/engine_scripts',
|
||||
html_report: 'backstop_data/html_report',
|
||||
ci_report: 'backstop_data/ci_report'
|
||||
},
|
||||
report: ['browser'],
|
||||
engine: 'puppeteer',
|
||||
engineOptions: {
|
||||
args: ['--no-sandbox']
|
||||
},
|
||||
asyncCaptureLimit: 5,
|
||||
asyncCompareLimit: 50,
|
||||
debug: false,
|
||||
debugWindow: false
|
||||
};
|
||||
"""
|
||||
return config
|
||||
|
||||
|
||||
def update_storybook_main_config(main_js_path: str, vr_tool: str = 'chromatic') -> str:
|
||||
"""
|
||||
Update .storybook/main.js to include VR tool addon.
|
||||
|
||||
Args:
|
||||
main_js_path: Path to main.js file
|
||||
vr_tool: VR tool name ('chromatic', 'percy', 'backstopjs')
|
||||
|
||||
Returns:
|
||||
Updated main.js content
|
||||
"""
|
||||
# Read existing config
|
||||
if not os.path.exists(main_js_path):
|
||||
# Generate new config if doesn't exist
|
||||
return generate_new_storybook_config(vr_tool)
|
||||
|
||||
with open(main_js_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Determine addon to add
|
||||
if vr_tool == 'chromatic':
|
||||
addon = '@chromatic-com/storybook'
|
||||
elif vr_tool == 'percy':
|
||||
addon = '@percy/storybook'
|
||||
else:
|
||||
return content # BackstopJS doesn't need addon
|
||||
|
||||
# Check if addon already exists
|
||||
if addon in content:
|
||||
return content # Already configured
|
||||
|
||||
# Find addons array and insert
|
||||
addons_pattern = r'addons:\s*\[(.*?)\]'
|
||||
match = re.search(addons_pattern, content, re.DOTALL)
|
||||
|
||||
if match:
|
||||
existing_addons = match.group(1).strip()
|
||||
# Add new addon
|
||||
updated_addons = f"{existing_addons},\n '{addon}'"
|
||||
updated_content = content.replace(match.group(0), f"addons: [\n {updated_addons}\n ]")
|
||||
return updated_content
|
||||
else:
|
||||
# No addons array found - append at end
|
||||
return content + f"\n// Added by Navigator visual-regression skill\nmodule.exports.addons.push('{addon}');\n"
|
||||
|
||||
|
||||
def generate_new_storybook_config(vr_tool: str = 'chromatic') -> str:
|
||||
"""
|
||||
Generate new .storybook/main.js from scratch.
|
||||
|
||||
Args:
|
||||
vr_tool: VR tool name
|
||||
|
||||
Returns:
|
||||
main.js content
|
||||
"""
|
||||
addon = '@chromatic-com/storybook' if vr_tool == 'chromatic' else '@percy/storybook'
|
||||
|
||||
config = f"""module.exports = {{
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'{addon}',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {{
|
||||
name: '@storybook/react-vite',
|
||||
options: {{}},
|
||||
}},
|
||||
}};
|
||||
"""
|
||||
return config
|
||||
|
||||
|
||||
def update_package_json_scripts(package_json_path: str, vr_tool: str = 'chromatic') -> Dict:
|
||||
"""
|
||||
Add VR tool scripts to package.json.
|
||||
|
||||
Args:
|
||||
package_json_path: Path to package.json
|
||||
vr_tool: VR tool name
|
||||
|
||||
Returns:
|
||||
Updated package.json data
|
||||
"""
|
||||
with open(package_json_path, 'r') as f:
|
||||
package_data = json.load(f)
|
||||
|
||||
scripts = package_data.get('scripts', {})
|
||||
|
||||
# Add VR tool scripts
|
||||
if vr_tool == 'chromatic':
|
||||
scripts['chromatic'] = 'npx chromatic'
|
||||
scripts['chromatic:ci'] = 'npx chromatic --exit-zero-on-changes'
|
||||
elif vr_tool == 'percy':
|
||||
scripts['percy'] = 'percy storybook storybook-static'
|
||||
scripts['percy:ci'] = 'percy storybook storybook-static --partial'
|
||||
elif vr_tool == 'backstopjs':
|
||||
scripts['backstop:reference'] = 'backstop reference'
|
||||
scripts['backstop:test'] = 'backstop test'
|
||||
scripts['backstop:approve'] = 'backstop approve'
|
||||
|
||||
# Ensure build-storybook script exists
|
||||
if 'build-storybook' not in scripts:
|
||||
scripts['build-storybook'] = 'storybook build'
|
||||
|
||||
package_data['scripts'] = scripts
|
||||
|
||||
return package_data
|
||||
|
||||
|
||||
def detect_main_branch(project_root: str) -> str:
|
||||
"""
|
||||
Detect main branch name from git.
|
||||
|
||||
Args:
|
||||
project_root: Project root directory
|
||||
|
||||
Returns:
|
||||
Branch name ('main' or 'master')
|
||||
"""
|
||||
git_head = Path(project_root) / '.git' / 'HEAD'
|
||||
|
||||
if git_head.exists():
|
||||
with open(git_head, 'r') as f:
|
||||
content = f.read().strip()
|
||||
if 'refs/heads/main' in content:
|
||||
return 'main'
|
||||
elif 'refs/heads/master' in content:
|
||||
return 'master'
|
||||
|
||||
return 'main' # Default
|
||||
|
||||
|
||||
def generate_configs(project_root: str, vr_tool: str = 'chromatic') -> Dict:
|
||||
"""
|
||||
Generate all configuration files for VR setup.
|
||||
|
||||
Args:
|
||||
project_root: Project root directory
|
||||
vr_tool: VR tool to configure
|
||||
|
||||
Returns:
|
||||
Dict with file paths and contents
|
||||
"""
|
||||
project_info = {
|
||||
'main_branch': detect_main_branch(project_root)
|
||||
}
|
||||
|
||||
result = {
|
||||
'configs_generated': [],
|
||||
'configs_updated': [],
|
||||
'errors': []
|
||||
}
|
||||
|
||||
# Generate tool-specific config
|
||||
if vr_tool == 'chromatic':
|
||||
config_path = os.path.join(project_root, 'chromatic.config.json')
|
||||
config_content = generate_chromatic_config(project_info)
|
||||
result['configs_generated'].append({
|
||||
'path': config_path,
|
||||
'content': config_content
|
||||
})
|
||||
|
||||
elif vr_tool == 'percy':
|
||||
config_path = os.path.join(project_root, '.percy.yml')
|
||||
config_content = generate_percy_config(project_info)
|
||||
result['configs_generated'].append({
|
||||
'path': config_path,
|
||||
'content': config_content
|
||||
})
|
||||
|
||||
elif vr_tool == 'backstopjs':
|
||||
config_path = os.path.join(project_root, 'backstop.config.js')
|
||||
config_content = generate_backstop_config(project_info)
|
||||
result['configs_generated'].append({
|
||||
'path': config_path,
|
||||
'content': config_content
|
||||
})
|
||||
|
||||
# Update Storybook main.js
|
||||
storybook_dir = Path(project_root) / '.storybook'
|
||||
main_js_candidates = [
|
||||
storybook_dir / 'main.js',
|
||||
storybook_dir / 'main.ts'
|
||||
]
|
||||
|
||||
main_js_path = None
|
||||
for candidate in main_js_candidates:
|
||||
if candidate.exists():
|
||||
main_js_path = str(candidate)
|
||||
break
|
||||
|
||||
if main_js_path:
|
||||
main_js_content = update_storybook_main_config(main_js_path, vr_tool)
|
||||
result['configs_updated'].append({
|
||||
'path': main_js_path,
|
||||
'content': main_js_content
|
||||
})
|
||||
elif storybook_dir.exists():
|
||||
# Create new main.js
|
||||
main_js_path = str(storybook_dir / 'main.js')
|
||||
main_js_content = generate_new_storybook_config(vr_tool)
|
||||
result['configs_generated'].append({
|
||||
'path': main_js_path,
|
||||
'content': main_js_content
|
||||
})
|
||||
|
||||
# Update package.json
|
||||
package_json_path = os.path.join(project_root, 'package.json')
|
||||
if os.path.exists(package_json_path):
|
||||
updated_package = update_package_json_scripts(package_json_path, vr_tool)
|
||||
result['configs_updated'].append({
|
||||
'path': package_json_path,
|
||||
'content': json.dumps(updated_package, indent=2)
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point."""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python chromatic_config_generator.py <project_root> [vr_tool]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
project_root = sys.argv[1]
|
||||
vr_tool = sys.argv[2] if len(sys.argv) > 2 else 'chromatic'
|
||||
|
||||
if vr_tool not in ['chromatic', 'percy', 'backstopjs']:
|
||||
print(f"Unsupported VR tool: {vr_tool}. Use: chromatic, percy, or backstopjs", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
result = generate_configs(project_root, vr_tool)
|
||||
|
||||
# Output as JSON
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
490
skills/visual-regression/functions/ci_workflow_generator.py
Normal file
490
skills/visual-regression/functions/ci_workflow_generator.py
Normal file
@@ -0,0 +1,490 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CI/CD Workflow Generator for Visual Regression
|
||||
|
||||
Generates GitHub Actions, GitLab CI, and CircleCI workflows for Chromatic/Percy/BackstopJS.
|
||||
|
||||
Usage:
|
||||
python ci_workflow_generator.py <project_root> <ci_platform> <vr_tool>
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
|
||||
def detect_node_version(project_root: str) -> str:
|
||||
"""
|
||||
Detect Node.js version from .nvmrc or package.json.
|
||||
|
||||
Args:
|
||||
project_root: Project root directory
|
||||
|
||||
Returns:
|
||||
Node version string (default: '20')
|
||||
"""
|
||||
# Check .nvmrc
|
||||
nvmrc = Path(project_root) / '.nvmrc'
|
||||
if nvmrc.exists():
|
||||
with open(nvmrc, 'r') as f:
|
||||
return f.read().strip()
|
||||
|
||||
# Check package.json engines.node
|
||||
package_json = Path(project_root) / 'package.json'
|
||||
if package_json.exists():
|
||||
with open(package_json, 'r') as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
node_version = data.get('engines', {}).get('node')
|
||||
if node_version:
|
||||
# Extract version number (handle ">=18.0.0" format)
|
||||
import re
|
||||
match = re.search(r'\d+', node_version)
|
||||
if match:
|
||||
return match.group(0)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
return '20' # Default
|
||||
|
||||
|
||||
def detect_package_manager(project_root: str) -> str:
|
||||
"""
|
||||
Detect package manager from lock files.
|
||||
|
||||
Args:
|
||||
project_root: Project root directory
|
||||
|
||||
Returns:
|
||||
Package manager name ('npm', 'yarn', 'pnpm')
|
||||
"""
|
||||
root = Path(project_root)
|
||||
|
||||
if (root / 'pnpm-lock.yaml').exists():
|
||||
return 'pnpm'
|
||||
elif (root / 'yarn.lock').exists():
|
||||
return 'yarn'
|
||||
else:
|
||||
return 'npm'
|
||||
|
||||
|
||||
def get_install_command(package_manager: str) -> str:
|
||||
"""Get install command for package manager."""
|
||||
commands = {
|
||||
'npm': 'npm ci',
|
||||
'yarn': 'yarn install --frozen-lockfile',
|
||||
'pnpm': 'pnpm install --frozen-lockfile'
|
||||
}
|
||||
return commands.get(package_manager, 'npm ci')
|
||||
|
||||
|
||||
def detect_branches(project_root: str) -> list:
|
||||
"""
|
||||
Detect main branches from git config.
|
||||
|
||||
Args:
|
||||
project_root: Project root directory
|
||||
|
||||
Returns:
|
||||
List of branch names
|
||||
"""
|
||||
git_head = Path(project_root) / '.git' / 'HEAD'
|
||||
|
||||
if git_head.exists():
|
||||
with open(git_head, 'r') as f:
|
||||
content = f.read().strip()
|
||||
if 'refs/heads/main' in content:
|
||||
return ['main', 'develop']
|
||||
elif 'refs/heads/master' in content:
|
||||
return ['master', 'develop']
|
||||
|
||||
return ['main', 'develop']
|
||||
|
||||
|
||||
def generate_github_workflow_chromatic(project_info: Dict) -> str:
|
||||
"""
|
||||
Generate GitHub Actions workflow for Chromatic.
|
||||
|
||||
Args:
|
||||
project_info: Project information
|
||||
|
||||
Returns:
|
||||
YAML workflow content
|
||||
"""
|
||||
node_version = project_info.get('node_version', '20')
|
||||
package_manager = project_info.get('package_manager', 'npm')
|
||||
install_command = get_install_command(package_manager)
|
||||
branches = project_info.get('branches', ['main', 'develop'])
|
||||
|
||||
workflow = f"""name: Visual Regression Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: {json.dumps(branches)}
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
chromatic:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Required for Chromatic
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '{node_version}'
|
||||
cache: '{package_manager}'
|
||||
|
||||
- name: Install dependencies
|
||||
run: {install_command}
|
||||
|
||||
- name: Run Chromatic
|
||||
uses: chromaui/action@latest
|
||||
with:
|
||||
projectToken: ${{{{ secrets.CHROMATIC_PROJECT_TOKEN }}}}
|
||||
exitZeroOnChanges: true
|
||||
onlyChanged: true
|
||||
autoAcceptChanges: 'main' # Auto-accept on main branch
|
||||
"""
|
||||
return workflow
|
||||
|
||||
|
||||
def generate_github_workflow_percy(project_info: Dict) -> str:
|
||||
"""
|
||||
Generate GitHub Actions workflow for Percy.
|
||||
|
||||
Args:
|
||||
project_info: Project information
|
||||
|
||||
Returns:
|
||||
YAML workflow content
|
||||
"""
|
||||
node_version = project_info.get('node_version', '20')
|
||||
package_manager = project_info.get('package_manager', 'npm')
|
||||
install_command = get_install_command(package_manager)
|
||||
branches = project_info.get('branches', ['main', 'develop'])
|
||||
|
||||
workflow = f"""name: Visual Regression Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: {json.dumps(branches)}
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
percy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '{node_version}'
|
||||
cache: '{package_manager}'
|
||||
|
||||
- name: Install dependencies
|
||||
run: {install_command}
|
||||
|
||||
- name: Build Storybook
|
||||
run: npm run build-storybook
|
||||
|
||||
- name: Run Percy
|
||||
run: npx percy storybook storybook-static
|
||||
env:
|
||||
PERCY_TOKEN: ${{{{ secrets.PERCY_TOKEN }}}}
|
||||
"""
|
||||
return workflow
|
||||
|
||||
|
||||
def generate_github_workflow_backstop(project_info: Dict) -> str:
|
||||
"""
|
||||
Generate GitHub Actions workflow for BackstopJS.
|
||||
|
||||
Args:
|
||||
project_info: Project information
|
||||
|
||||
Returns:
|
||||
YAML workflow content
|
||||
"""
|
||||
node_version = project_info.get('node_version', '20')
|
||||
package_manager = project_info.get('package_manager', 'npm')
|
||||
install_command = get_install_command(package_manager)
|
||||
branches = project_info.get('branches', ['main', 'develop'])
|
||||
|
||||
workflow = f"""name: Visual Regression Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: {json.dumps(branches)}
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
backstop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '{node_version}'
|
||||
cache: '{package_manager}'
|
||||
|
||||
- name: Install dependencies
|
||||
run: {install_command}
|
||||
|
||||
- name: Run BackstopJS
|
||||
run: npm run backstop:test
|
||||
|
||||
- name: Upload test results
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: backstop-results
|
||||
path: backstop_data/
|
||||
"""
|
||||
return workflow
|
||||
|
||||
|
||||
def generate_gitlab_ci_chromatic(project_info: Dict) -> str:
|
||||
"""
|
||||
Generate GitLab CI job for Chromatic.
|
||||
|
||||
Args:
|
||||
project_info: Project information
|
||||
|
||||
Returns:
|
||||
YAML job content
|
||||
"""
|
||||
node_version = project_info.get('node_version', '20')
|
||||
install_command = get_install_command(project_info.get('package_manager', 'npm'))
|
||||
|
||||
job = f"""# Add to .gitlab-ci.yml
|
||||
|
||||
chromatic:
|
||||
stage: test
|
||||
image: node:{node_version}
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
script:
|
||||
- {install_command}
|
||||
- npx chromatic --exit-zero-on-changes --only-changed
|
||||
variables:
|
||||
CHROMATIC_PROJECT_TOKEN: $CHROMATIC_PROJECT_TOKEN
|
||||
only:
|
||||
- main
|
||||
- develop
|
||||
- merge_requests
|
||||
"""
|
||||
return job
|
||||
|
||||
|
||||
def generate_gitlab_ci_percy(project_info: Dict) -> str:
|
||||
"""
|
||||
Generate GitLab CI job for Percy.
|
||||
|
||||
Args:
|
||||
project_info: Project information
|
||||
|
||||
Returns:
|
||||
YAML job content
|
||||
"""
|
||||
node_version = project_info.get('node_version', '20')
|
||||
install_command = get_install_command(project_info.get('package_manager', 'npm'))
|
||||
|
||||
job = f"""# Add to .gitlab-ci.yml
|
||||
|
||||
percy:
|
||||
stage: test
|
||||
image: node:{node_version}
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
script:
|
||||
- {install_command}
|
||||
- npm run build-storybook
|
||||
- npx percy storybook storybook-static
|
||||
variables:
|
||||
PERCY_TOKEN: $PERCY_TOKEN
|
||||
only:
|
||||
- main
|
||||
- develop
|
||||
- merge_requests
|
||||
"""
|
||||
return job
|
||||
|
||||
|
||||
def generate_circleci_config_chromatic(project_info: Dict) -> str:
|
||||
"""
|
||||
Generate CircleCI job for Chromatic.
|
||||
|
||||
Args:
|
||||
project_info: Project information
|
||||
|
||||
Returns:
|
||||
YAML job content
|
||||
"""
|
||||
node_version = project_info.get('node_version', '20')
|
||||
install_command = get_install_command(project_info.get('package_manager', 'npm'))
|
||||
|
||||
config = f"""# Add to .circleci/config.yml
|
||||
|
||||
version: 2.1
|
||||
|
||||
executors:
|
||||
node:
|
||||
docker:
|
||||
- image: cimg/node:{node_version}
|
||||
|
||||
jobs:
|
||||
chromatic:
|
||||
executor: node
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{{{ checksum "package.json" }}}}
|
||||
- run: {install_command}
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{{{ checksum "package.json" }}}}
|
||||
- run:
|
||||
name: Run Chromatic
|
||||
command: npx chromatic --exit-zero-on-changes --only-changed
|
||||
environment:
|
||||
CHROMATIC_PROJECT_TOKEN: $CHROMATIC_PROJECT_TOKEN
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
test:
|
||||
jobs:
|
||||
- chromatic
|
||||
"""
|
||||
return config
|
||||
|
||||
|
||||
def generate_workflow(project_root: str, ci_platform: str, vr_tool: str) -> Dict:
|
||||
"""
|
||||
Generate CI/CD workflow for specified platform and VR tool.
|
||||
|
||||
Args:
|
||||
project_root: Project root directory
|
||||
ci_platform: CI platform ('github', 'gitlab', 'circleci')
|
||||
vr_tool: VR tool ('chromatic', 'percy', 'backstopjs')
|
||||
|
||||
Returns:
|
||||
Dict with workflow path and content
|
||||
"""
|
||||
# Gather project info
|
||||
project_info = {
|
||||
'node_version': detect_node_version(project_root),
|
||||
'package_manager': detect_package_manager(project_root),
|
||||
'branches': detect_branches(project_root)
|
||||
}
|
||||
|
||||
result = {
|
||||
'platform': ci_platform,
|
||||
'vr_tool': vr_tool,
|
||||
'workflow_path': None,
|
||||
'workflow_content': None,
|
||||
'instructions': None
|
||||
}
|
||||
|
||||
# Generate workflow based on platform and tool
|
||||
if ci_platform == 'github':
|
||||
workflow_dir = Path(project_root) / '.github' / 'workflows'
|
||||
workflow_file = 'chromatic.yml' if vr_tool == 'chromatic' else f'{vr_tool}.yml'
|
||||
result['workflow_path'] = str(workflow_dir / workflow_file)
|
||||
|
||||
if vr_tool == 'chromatic':
|
||||
result['workflow_content'] = generate_github_workflow_chromatic(project_info)
|
||||
elif vr_tool == 'percy':
|
||||
result['workflow_content'] = generate_github_workflow_percy(project_info)
|
||||
elif vr_tool == 'backstopjs':
|
||||
result['workflow_content'] = generate_github_workflow_backstop(project_info)
|
||||
|
||||
result['instructions'] = f"""
|
||||
GitHub Actions workflow created: {result['workflow_path']}
|
||||
|
||||
Next steps:
|
||||
1. Add secret: Repository Settings → Secrets → Actions
|
||||
2. Create secret: CHROMATIC_PROJECT_TOKEN (or PERCY_TOKEN)
|
||||
3. Commit and push this file
|
||||
4. Workflow will run automatically on push/PR
|
||||
"""
|
||||
|
||||
elif ci_platform == 'gitlab':
|
||||
result['workflow_path'] = str(Path(project_root) / '.gitlab-ci.yml')
|
||||
|
||||
if vr_tool == 'chromatic':
|
||||
result['workflow_content'] = generate_gitlab_ci_chromatic(project_info)
|
||||
elif vr_tool == 'percy':
|
||||
result['workflow_content'] = generate_gitlab_ci_percy(project_info)
|
||||
|
||||
result['instructions'] = """
|
||||
GitLab CI job generated. Add to your .gitlab-ci.yml file.
|
||||
|
||||
Next steps:
|
||||
1. Add variable: Project Settings → CI/CD → Variables
|
||||
2. Create variable: CHROMATIC_PROJECT_TOKEN (or PERCY_TOKEN)
|
||||
3. Commit and push .gitlab-ci.yml
|
||||
4. Pipeline will run automatically
|
||||
"""
|
||||
|
||||
elif ci_platform == 'circleci':
|
||||
result['workflow_path'] = str(Path(project_root) / '.circleci' / 'config.yml')
|
||||
|
||||
if vr_tool == 'chromatic':
|
||||
result['workflow_content'] = generate_circleci_config_chromatic(project_info)
|
||||
|
||||
result['instructions'] = """
|
||||
CircleCI job generated. Add to your .circleci/config.yml file.
|
||||
|
||||
Next steps:
|
||||
1. Add environment variable in CircleCI project settings
|
||||
2. Variable name: CHROMATIC_PROJECT_TOKEN
|
||||
3. Commit and push config.yml
|
||||
4. Build will run automatically
|
||||
"""
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point."""
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: python ci_workflow_generator.py <project_root> <ci_platform> <vr_tool>", file=sys.stderr)
|
||||
print(" ci_platform: github, gitlab, circleci", file=sys.stderr)
|
||||
print(" vr_tool: chromatic, percy, backstopjs", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
project_root = sys.argv[1]
|
||||
ci_platform = sys.argv[2].lower()
|
||||
vr_tool = sys.argv[3].lower()
|
||||
|
||||
if ci_platform not in ['github', 'gitlab', 'circleci']:
|
||||
print(f"Unsupported CI platform: {ci_platform}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if vr_tool not in ['chromatic', 'percy', 'backstopjs']:
|
||||
print(f"Unsupported VR tool: {vr_tool}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
result = generate_workflow(project_root, ci_platform, vr_tool)
|
||||
|
||||
# Output as JSON
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
476
skills/visual-regression/functions/story_generator.py
Normal file
476
skills/visual-regression/functions/story_generator.py
Normal file
@@ -0,0 +1,476 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Storybook Story Generator
|
||||
|
||||
Analyzes React/Vue/Svelte components and generates comprehensive Storybook stories
|
||||
with variants, accessibility tests, and interaction tests.
|
||||
|
||||
Usage:
|
||||
python story_generator.py <component_path> <framework> [template_path]
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
def extract_component_name(file_path: str) -> str:
|
||||
"""Extract component name from file path."""
|
||||
return Path(file_path).stem
|
||||
|
||||
|
||||
def analyze_react_component(component_path: str) -> Dict:
|
||||
"""
|
||||
Analyze React/TypeScript component to extract props and metadata.
|
||||
|
||||
Args:
|
||||
component_path: Path to component file
|
||||
|
||||
Returns:
|
||||
Dict with component info: name, props, prop_types, exports
|
||||
"""
|
||||
with open(component_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
component_name = extract_component_name(component_path)
|
||||
|
||||
result = {
|
||||
'name': component_name,
|
||||
'path': component_path,
|
||||
'props': [],
|
||||
'has_typescript': component_path.endswith(('.tsx', '.ts')),
|
||||
'is_default_export': False,
|
||||
'story_title': f'Components/{component_name}'
|
||||
}
|
||||
|
||||
# Check for default export
|
||||
if re.search(r'export\s+default\s+' + component_name, content):
|
||||
result['is_default_export'] = True
|
||||
|
||||
# Extract TypeScript interface/type props
|
||||
if result['has_typescript']:
|
||||
# Match interface or type definition
|
||||
interface_pattern = r'(?:interface|type)\s+' + component_name + r'Props\s*{([^}]+)}'
|
||||
match = re.search(interface_pattern, content, re.DOTALL)
|
||||
|
||||
if match:
|
||||
props_block = match.group(1)
|
||||
# Parse each prop
|
||||
prop_pattern = r'(\w+)(\?)?:\s*([^;]+);?'
|
||||
for prop_match in re.finditer(prop_pattern, props_block):
|
||||
prop_name = prop_match.group(1)
|
||||
is_optional = prop_match.group(2) == '?'
|
||||
prop_type = prop_match.group(3).strip()
|
||||
|
||||
# Determine control type based on prop type
|
||||
control = infer_control_type(prop_type)
|
||||
|
||||
# Extract possible values for enums
|
||||
values = extract_enum_values(prop_type)
|
||||
|
||||
result['props'].append({
|
||||
'name': prop_name,
|
||||
'type': prop_type,
|
||||
'optional': is_optional,
|
||||
'control': control,
|
||||
'values': values,
|
||||
'default': infer_default_value(prop_type, prop_name)
|
||||
})
|
||||
|
||||
# Fallback: extract props from function signature
|
||||
if not result['props']:
|
||||
func_pattern = r'(?:function|const)\s+' + component_name + r'\s*(?:<[^>]+>)?\s*\(\s*{\s*([^}]+)\s*}'
|
||||
match = re.search(func_pattern, content)
|
||||
|
||||
if match:
|
||||
props_str = match.group(1)
|
||||
# Simple extraction of prop names
|
||||
prop_names = [p.strip().split(':')[0].strip() for p in props_str.split(',')]
|
||||
|
||||
for prop_name in prop_names:
|
||||
result['props'].append({
|
||||
'name': prop_name,
|
||||
'type': 'any',
|
||||
'optional': False,
|
||||
'control': 'text',
|
||||
'values': None,
|
||||
'default': None
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def infer_control_type(prop_type: str) -> str:
|
||||
"""
|
||||
Infer Storybook control type from TypeScript type.
|
||||
|
||||
Args:
|
||||
prop_type: TypeScript type string
|
||||
|
||||
Returns:
|
||||
Storybook control type
|
||||
"""
|
||||
prop_type_lower = prop_type.lower()
|
||||
|
||||
# Boolean
|
||||
if 'boolean' in prop_type_lower:
|
||||
return 'boolean'
|
||||
|
||||
# Number
|
||||
if 'number' in prop_type_lower:
|
||||
return 'number'
|
||||
|
||||
# Union types (enums)
|
||||
if '|' in prop_type:
|
||||
return 'select'
|
||||
|
||||
# Objects
|
||||
if prop_type_lower in ['object', 'record']:
|
||||
return 'object'
|
||||
|
||||
# Arrays
|
||||
if '[]' in prop_type or prop_type.startswith('array'):
|
||||
return 'object'
|
||||
|
||||
# Functions
|
||||
if '=>' in prop_type or prop_type.startswith('('):
|
||||
return 'function'
|
||||
|
||||
# Default to text
|
||||
return 'text'
|
||||
|
||||
|
||||
def extract_enum_values(prop_type: str) -> Optional[List[str]]:
|
||||
"""
|
||||
Extract possible values from union type.
|
||||
|
||||
Args:
|
||||
prop_type: TypeScript type string (e.g., "'sm' | 'md' | 'lg'")
|
||||
|
||||
Returns:
|
||||
List of possible values or None
|
||||
"""
|
||||
if '|' not in prop_type:
|
||||
return None
|
||||
|
||||
# Extract string literals
|
||||
values = re.findall(r"['\"]([^'\"]+)['\"]", prop_type)
|
||||
|
||||
return values if values else None
|
||||
|
||||
|
||||
def infer_default_value(prop_type: str, prop_name: str) -> any:
|
||||
"""
|
||||
Infer reasonable default value for prop.
|
||||
|
||||
Args:
|
||||
prop_type: TypeScript type string
|
||||
prop_name: Prop name
|
||||
|
||||
Returns:
|
||||
Default value
|
||||
"""
|
||||
prop_type_lower = prop_type.lower()
|
||||
prop_name_lower = prop_name.lower()
|
||||
|
||||
# Boolean
|
||||
if 'boolean' in prop_type_lower:
|
||||
return False
|
||||
|
||||
# Number
|
||||
if 'number' in prop_type_lower:
|
||||
if 'count' in prop_name_lower:
|
||||
return 0
|
||||
return 1
|
||||
|
||||
# Union types - return first value
|
||||
values = extract_enum_values(prop_type)
|
||||
if values:
|
||||
return values[0]
|
||||
|
||||
# Strings - context-aware defaults
|
||||
if 'name' in prop_name_lower:
|
||||
return 'John Doe'
|
||||
if 'title' in prop_name_lower:
|
||||
return 'Example Title'
|
||||
if 'description' in prop_name_lower or 'bio' in prop_name_lower:
|
||||
return 'This is an example description'
|
||||
if 'email' in prop_name_lower:
|
||||
return 'user@example.com'
|
||||
if 'url' in prop_name_lower or 'href' in prop_name_lower:
|
||||
return 'https://example.com'
|
||||
if 'image' in prop_name_lower or 'avatar' in prop_name_lower:
|
||||
return 'https://via.placeholder.com/150'
|
||||
|
||||
return 'Example text'
|
||||
|
||||
|
||||
def generate_variants(component_info: Dict) -> List[Dict]:
|
||||
"""
|
||||
Generate story variants based on component props.
|
||||
|
||||
Args:
|
||||
component_info: Component analysis result
|
||||
|
||||
Returns:
|
||||
List of variant definitions
|
||||
"""
|
||||
variants = []
|
||||
|
||||
# Generate variants for enum props
|
||||
for prop in component_info['props']:
|
||||
if prop['values'] and len(prop['values']) > 1:
|
||||
# Create variant for each enum value
|
||||
for value in prop['values']:
|
||||
if value != prop['default']: # Skip default (already in Default story)
|
||||
variant_name = value.capitalize()
|
||||
variants.append({
|
||||
'name': variant_name,
|
||||
'prop_name': prop['name'],
|
||||
'value': value
|
||||
})
|
||||
|
||||
# Generate boolean state variants
|
||||
for prop in component_info['props']:
|
||||
if prop['type'].lower() == 'boolean' and not prop['default']:
|
||||
variant_name = prop['name'].capitalize()
|
||||
variants.append({
|
||||
'name': variant_name,
|
||||
'prop_name': prop['name'],
|
||||
'value': True
|
||||
})
|
||||
|
||||
return variants
|
||||
|
||||
|
||||
def generate_story_content(component_info: Dict, framework: str = 'react') -> str:
|
||||
"""
|
||||
Generate complete Storybook story file content.
|
||||
|
||||
Args:
|
||||
component_info: Component analysis result
|
||||
framework: Framework name ('react', 'vue', 'svelte')
|
||||
|
||||
Returns:
|
||||
Story file content as string
|
||||
"""
|
||||
if framework == 'react':
|
||||
return generate_react_story(component_info)
|
||||
elif framework == 'vue':
|
||||
return generate_vue_story(component_info)
|
||||
elif framework == 'svelte':
|
||||
return generate_svelte_story(component_info)
|
||||
else:
|
||||
raise ValueError(f"Unsupported framework: {framework}")
|
||||
|
||||
|
||||
def generate_react_story(component_info: Dict) -> str:
|
||||
"""Generate React/TypeScript story."""
|
||||
name = component_info['name']
|
||||
props = component_info['props']
|
||||
variants = generate_variants(component_info)
|
||||
|
||||
# Build imports
|
||||
imports = f"""import type {{ Meta, StoryObj }} from '@storybook/react';
|
||||
import {{ {name} }} from './{name}';
|
||||
"""
|
||||
|
||||
# Build argTypes
|
||||
arg_types = []
|
||||
for prop in props:
|
||||
if prop['values']:
|
||||
arg_types.append(f" {prop['name']}: {{ control: '{prop['control']}', options: {json.dumps(prop['values'])} }}")
|
||||
else:
|
||||
arg_types.append(f" {prop['name']}: {{ control: '{prop['control']}' }}")
|
||||
|
||||
arg_types_str = ',\n'.join(arg_types) if arg_types else ''
|
||||
|
||||
# Build default args
|
||||
default_args = []
|
||||
for prop in props:
|
||||
if prop['default'] is not None:
|
||||
if isinstance(prop['default'], str):
|
||||
default_args.append(f" {prop['name']}: '{prop['default']}'")
|
||||
else:
|
||||
default_args.append(f" {prop['name']}: {json.dumps(prop['default'])}")
|
||||
|
||||
default_args_str = ',\n'.join(default_args) if default_args else ''
|
||||
|
||||
# Build meta
|
||||
meta = f"""
|
||||
const meta = {{
|
||||
title: '{component_info['story_title']}',
|
||||
component: {name},
|
||||
parameters: {{
|
||||
layout: 'centered',
|
||||
}},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {{
|
||||
{arg_types_str}
|
||||
}},
|
||||
}} satisfies Meta<typeof {name}>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
"""
|
||||
|
||||
# Default story
|
||||
default_story = f"""
|
||||
export const Default: Story = {{
|
||||
args: {{
|
||||
{default_args_str}
|
||||
}},
|
||||
}};
|
||||
"""
|
||||
|
||||
# Variant stories
|
||||
variant_stories = []
|
||||
for variant in variants:
|
||||
if isinstance(variant['value'], str):
|
||||
value_str = f"'{variant['value']}'"
|
||||
else:
|
||||
value_str = json.dumps(variant['value'])
|
||||
|
||||
variant_stories.append(f"""
|
||||
export const {variant['name']}: Story = {{
|
||||
args: {{
|
||||
...Default.args,
|
||||
{variant['prop_name']}: {value_str},
|
||||
}},
|
||||
}};
|
||||
""")
|
||||
|
||||
variant_stories_str = ''.join(variant_stories)
|
||||
|
||||
# Accessibility tests
|
||||
a11y = f"""
|
||||
// Accessibility tests
|
||||
Default.parameters = {{
|
||||
a11y: {{
|
||||
config: {{
|
||||
rules: [
|
||||
{{ id: 'color-contrast', enabled: true }},
|
||||
{{ id: 'label', enabled: true }},
|
||||
],
|
||||
}},
|
||||
}},
|
||||
}};
|
||||
"""
|
||||
|
||||
return imports + meta + default_story + variant_stories_str + a11y
|
||||
|
||||
|
||||
def generate_vue_story(component_info: Dict) -> str:
|
||||
"""Generate Vue story (simplified)."""
|
||||
name = component_info['name']
|
||||
|
||||
return f"""import type {{ Meta, StoryObj }} from '@storybook/vue3';
|
||||
import {name} from './{name}.vue';
|
||||
|
||||
const meta = {{
|
||||
title: 'Components/{name}',
|
||||
component: {name},
|
||||
tags: ['autodocs'],
|
||||
}} satisfies Meta<typeof {name}>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {{
|
||||
args: {{}},
|
||||
}};
|
||||
"""
|
||||
|
||||
|
||||
def generate_svelte_story(component_info: Dict) -> str:
|
||||
"""Generate Svelte story (simplified)."""
|
||||
name = component_info['name']
|
||||
|
||||
return f"""import type {{ Meta, StoryObj }} from '@storybook/svelte';
|
||||
import {name} from './{name}.svelte';
|
||||
|
||||
const meta = {{
|
||||
title: 'Components/{name}',
|
||||
component: {name},
|
||||
tags: ['autodocs'],
|
||||
}} satisfies Meta<typeof {name}>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {{
|
||||
args: {{}},
|
||||
}};
|
||||
"""
|
||||
|
||||
|
||||
def write_story_file(component_path: str, story_content: str) -> str:
|
||||
"""
|
||||
Write story file next to component file.
|
||||
|
||||
Args:
|
||||
component_path: Path to component file
|
||||
story_content: Generated story content
|
||||
|
||||
Returns:
|
||||
Path to created story file
|
||||
"""
|
||||
component_file = Path(component_path)
|
||||
story_file = component_file.parent / f"{component_file.stem}.stories{component_file.suffix}"
|
||||
|
||||
with open(story_file, 'w') as f:
|
||||
f.write(story_content)
|
||||
|
||||
return str(story_file)
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point."""
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python story_generator.py <component_path> <framework>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
component_path = sys.argv[1]
|
||||
framework = sys.argv[2].lower()
|
||||
|
||||
if framework not in ['react', 'vue', 'svelte']:
|
||||
print(f"Unsupported framework: {framework}. Use: react, vue, or svelte", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.exists(component_path):
|
||||
print(f"Component file not found: {component_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Analyze component
|
||||
if framework == 'react':
|
||||
component_info = analyze_react_component(component_path)
|
||||
else:
|
||||
# Simplified for Vue/Svelte
|
||||
component_info = {
|
||||
'name': extract_component_name(component_path),
|
||||
'path': component_path,
|
||||
'props': [],
|
||||
'story_title': f'Components/{extract_component_name(component_path)}'
|
||||
}
|
||||
|
||||
# Generate story
|
||||
story_content = generate_story_content(component_info, framework)
|
||||
|
||||
# Write story file
|
||||
story_file_path = write_story_file(component_path, story_content)
|
||||
|
||||
# Output result
|
||||
result = {
|
||||
'component': component_info,
|
||||
'story_file': story_file_path,
|
||||
'success': True
|
||||
}
|
||||
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
409
skills/visual-regression/functions/vr_setup_validator.py
Normal file
409
skills/visual-regression/functions/vr_setup_validator.py
Normal file
@@ -0,0 +1,409 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Visual Regression Setup Validator
|
||||
|
||||
Detects existing Storybook setup, VR tools, CI platform, and validates component paths.
|
||||
Returns comprehensive validation report to guide skill execution.
|
||||
|
||||
Usage:
|
||||
python vr_setup_validator.py <project_root> [component_path]
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
def detect_framework(project_root: str) -> Optional[str]:
|
||||
"""
|
||||
Detect frontend framework from package.json dependencies.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root directory
|
||||
|
||||
Returns:
|
||||
Framework name ('react', 'vue', 'svelte') or None
|
||||
"""
|
||||
package_json_path = Path(project_root) / 'package.json'
|
||||
|
||||
if not package_json_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(package_json_path, 'r') as f:
|
||||
package_data = json.load(f)
|
||||
|
||||
dependencies = {
|
||||
**package_data.get('dependencies', {}),
|
||||
**package_data.get('devDependencies', {})
|
||||
}
|
||||
|
||||
if 'react' in dependencies:
|
||||
return 'react'
|
||||
elif 'vue' in dependencies:
|
||||
return 'vue'
|
||||
elif 'svelte' in dependencies:
|
||||
return 'svelte'
|
||||
|
||||
return None
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
|
||||
def detect_storybook_config(project_root: str) -> Dict:
|
||||
"""
|
||||
Detect Storybook version and configuration.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root directory
|
||||
|
||||
Returns:
|
||||
Dict with version, addons, framework, and config path
|
||||
"""
|
||||
storybook_dir = Path(project_root) / '.storybook'
|
||||
package_json_path = Path(project_root) / 'package.json'
|
||||
|
||||
result = {
|
||||
'installed': False,
|
||||
'version': None,
|
||||
'config_path': None,
|
||||
'main_js_path': None,
|
||||
'addons': [],
|
||||
'framework': None
|
||||
}
|
||||
|
||||
# Check if .storybook directory exists
|
||||
if not storybook_dir.exists():
|
||||
return result
|
||||
|
||||
result['installed'] = True
|
||||
result['config_path'] = str(storybook_dir)
|
||||
|
||||
# Check for main.js or main.ts
|
||||
main_js = storybook_dir / 'main.js'
|
||||
main_ts = storybook_dir / 'main.ts'
|
||||
|
||||
if main_js.exists():
|
||||
result['main_js_path'] = str(main_js)
|
||||
elif main_ts.exists():
|
||||
result['main_js_path'] = str(main_ts)
|
||||
|
||||
# Extract version from package.json
|
||||
if package_json_path.exists():
|
||||
try:
|
||||
with open(package_json_path, 'r') as f:
|
||||
package_data = json.load(f)
|
||||
|
||||
dependencies = {
|
||||
**package_data.get('dependencies', {}),
|
||||
**package_data.get('devDependencies', {})
|
||||
}
|
||||
|
||||
# Find Storybook version
|
||||
for dep in dependencies:
|
||||
if dep.startswith('@storybook/'):
|
||||
result['version'] = dependencies[dep].replace('^', '').replace('~', '')
|
||||
break
|
||||
|
||||
# Extract addons from dependencies
|
||||
result['addons'] = [
|
||||
dep for dep in dependencies.keys()
|
||||
if dep.startswith('@storybook/addon-') or dep == '@chromatic-com/storybook'
|
||||
]
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# Try to parse main.js for framework
|
||||
if result['main_js_path']:
|
||||
try:
|
||||
with open(result['main_js_path'], 'r') as f:
|
||||
content = f.read()
|
||||
if '@storybook/react' in content:
|
||||
result['framework'] = 'react'
|
||||
elif '@storybook/vue' in content:
|
||||
result['framework'] = 'vue'
|
||||
elif '@storybook/svelte' in content:
|
||||
result['framework'] = 'svelte'
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def detect_vr_tool(project_root: str) -> Optional[str]:
|
||||
"""
|
||||
Detect existing visual regression tool from package.json.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root directory
|
||||
|
||||
Returns:
|
||||
Tool name ('chromatic', 'percy', 'backstopjs') or None
|
||||
"""
|
||||
package_json_path = Path(project_root) / 'package.json'
|
||||
|
||||
if not package_json_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(package_json_path, 'r') as f:
|
||||
package_data = json.load(f)
|
||||
|
||||
dependencies = {
|
||||
**package_data.get('dependencies', {}),
|
||||
**package_data.get('devDependencies', {})
|
||||
}
|
||||
|
||||
if 'chromatic' in dependencies or '@chromatic-com/storybook' in dependencies:
|
||||
return 'chromatic'
|
||||
elif '@percy/cli' in dependencies or '@percy/storybook' in dependencies:
|
||||
return 'percy'
|
||||
elif 'backstopjs' in dependencies:
|
||||
return 'backstopjs'
|
||||
|
||||
return None
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
|
||||
def detect_ci_platform(project_root: str) -> Optional[str]:
|
||||
"""
|
||||
Detect CI/CD platform from existing configuration files.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root directory
|
||||
|
||||
Returns:
|
||||
Platform name ('github', 'gitlab', 'circleci', 'bitbucket') or None
|
||||
"""
|
||||
root = Path(project_root)
|
||||
|
||||
# GitHub Actions
|
||||
if (root / '.github' / 'workflows').exists():
|
||||
return 'github'
|
||||
|
||||
# GitLab CI
|
||||
if (root / '.gitlab-ci.yml').exists():
|
||||
return 'gitlab'
|
||||
|
||||
# CircleCI
|
||||
if (root / '.circleci' / 'config.yml').exists():
|
||||
return 'circleci'
|
||||
|
||||
# Bitbucket Pipelines
|
||||
if (root / 'bitbucket-pipelines.yml').exists():
|
||||
return 'bitbucket'
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def validate_component_path(component_path: str, project_root: str = '.') -> Dict:
|
||||
"""
|
||||
Validate component file exists and extract basic information.
|
||||
|
||||
Args:
|
||||
component_path: Path to component file (relative or absolute)
|
||||
project_root: Project root directory
|
||||
|
||||
Returns:
|
||||
Dict with validation status and component info
|
||||
"""
|
||||
# Handle relative paths
|
||||
if not os.path.isabs(component_path):
|
||||
component_path = os.path.join(project_root, component_path)
|
||||
|
||||
component_file = Path(component_path)
|
||||
|
||||
result = {
|
||||
'valid': False,
|
||||
'path': component_path,
|
||||
'name': None,
|
||||
'extension': None,
|
||||
'directory': None,
|
||||
'error': None
|
||||
}
|
||||
|
||||
# Check if file exists
|
||||
if not component_file.exists():
|
||||
result['error'] = f"Component file not found: {component_path}"
|
||||
return result
|
||||
|
||||
# Check if it's a file (not directory)
|
||||
if not component_file.is_file():
|
||||
result['error'] = f"Path is not a file: {component_path}"
|
||||
return result
|
||||
|
||||
# Validate extension
|
||||
valid_extensions = ['.tsx', '.ts', '.jsx', '.js', '.vue', '.svelte']
|
||||
if component_file.suffix not in valid_extensions:
|
||||
result['error'] = f"Invalid file extension. Expected one of: {', '.join(valid_extensions)}"
|
||||
return result
|
||||
|
||||
# Extract component name (filename without extension)
|
||||
result['name'] = component_file.stem
|
||||
result['extension'] = component_file.suffix
|
||||
result['directory'] = str(component_file.parent)
|
||||
result['valid'] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def check_dependencies(project_root: str, vr_tool: Optional[str] = 'chromatic') -> Dict:
|
||||
"""
|
||||
Check which required dependencies are installed.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root directory
|
||||
vr_tool: VR tool to check for ('chromatic', 'percy', 'backstopjs')
|
||||
|
||||
Returns:
|
||||
Dict with installed and missing dependencies
|
||||
"""
|
||||
package_json_path = Path(project_root) / 'package.json'
|
||||
|
||||
result = {
|
||||
'installed': [],
|
||||
'missing': []
|
||||
}
|
||||
|
||||
if not package_json_path.exists():
|
||||
result['missing'] = ['package.json not found']
|
||||
return result
|
||||
|
||||
try:
|
||||
with open(package_json_path, 'r') as f:
|
||||
package_data = json.load(f)
|
||||
|
||||
dependencies = {
|
||||
**package_data.get('dependencies', {}),
|
||||
**package_data.get('devDependencies', {})
|
||||
}
|
||||
|
||||
# Core Storybook dependencies
|
||||
required_deps = [
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
]
|
||||
|
||||
# Add VR tool specific dependencies
|
||||
if vr_tool == 'chromatic':
|
||||
required_deps.extend(['chromatic', '@chromatic-com/storybook'])
|
||||
elif vr_tool == 'percy':
|
||||
required_deps.extend(['@percy/cli', '@percy/storybook'])
|
||||
elif vr_tool == 'backstopjs':
|
||||
required_deps.append('backstopjs')
|
||||
|
||||
# Check each dependency
|
||||
for dep in required_deps:
|
||||
if dep in dependencies:
|
||||
result['installed'].append(dep)
|
||||
else:
|
||||
result['missing'].append(dep)
|
||||
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
result['missing'] = ['Error reading package.json']
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_package_manager(project_root: str) -> str:
|
||||
"""
|
||||
Detect package manager from lock files.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root directory
|
||||
|
||||
Returns:
|
||||
Package manager name ('npm', 'yarn', 'pnpm')
|
||||
"""
|
||||
root = Path(project_root)
|
||||
|
||||
if (root / 'pnpm-lock.yaml').exists():
|
||||
return 'pnpm'
|
||||
elif (root / 'yarn.lock').exists():
|
||||
return 'yarn'
|
||||
else:
|
||||
return 'npm' # Default to npm
|
||||
|
||||
|
||||
def validate_setup(project_root: str, component_path: Optional[str] = None) -> Dict:
|
||||
"""
|
||||
Comprehensive validation of VR setup requirements.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root directory
|
||||
component_path: Optional path to component file
|
||||
|
||||
Returns:
|
||||
Complete validation report
|
||||
"""
|
||||
report = {
|
||||
'project_root': project_root,
|
||||
'framework': detect_framework(project_root),
|
||||
'storybook': detect_storybook_config(project_root),
|
||||
'vr_tool': detect_vr_tool(project_root),
|
||||
'ci_platform': detect_ci_platform(project_root),
|
||||
'package_manager': get_package_manager(project_root),
|
||||
'component': None,
|
||||
'dependencies': None,
|
||||
'ready': False,
|
||||
'warnings': [],
|
||||
'errors': []
|
||||
}
|
||||
|
||||
# Validate component if path provided
|
||||
if component_path:
|
||||
report['component'] = validate_component_path(component_path, project_root)
|
||||
if not report['component']['valid']:
|
||||
report['errors'].append(report['component']['error'])
|
||||
|
||||
# Check framework
|
||||
if not report['framework']:
|
||||
report['errors'].append('Framework not detected. Ensure React, Vue, or Svelte is installed.')
|
||||
|
||||
# Check Storybook
|
||||
if not report['storybook']['installed']:
|
||||
report['errors'].append('Storybook not installed. Run: npx storybook init')
|
||||
|
||||
# Determine VR tool (use detected or default to Chromatic)
|
||||
vr_tool = report['vr_tool'] or 'chromatic'
|
||||
report['dependencies'] = check_dependencies(project_root, vr_tool)
|
||||
|
||||
# Add warnings for missing dependencies
|
||||
if report['dependencies']['missing']:
|
||||
report['warnings'].append(
|
||||
f"Missing dependencies: {', '.join(report['dependencies']['missing'])}"
|
||||
)
|
||||
|
||||
# Determine if ready to proceed
|
||||
report['ready'] = (
|
||||
report['framework'] is not None and
|
||||
report['storybook']['installed'] and
|
||||
len(report['errors']) == 0
|
||||
)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point."""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python vr_setup_validator.py <project_root> [component_path]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
project_root = sys.argv[1]
|
||||
component_path = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
report = validate_setup(project_root, component_path)
|
||||
|
||||
# Output as JSON
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
# Exit with error code if not ready
|
||||
sys.exit(0 if report['ready'] else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
10
skills/visual-regression/templates/chromatic-config.json.j2
Normal file
10
skills/visual-regression/templates/chromatic-config.json.j2
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"projectId": "{{ project_id | default('<PROJECT_ID_PLACEHOLDER>') }}",
|
||||
"buildScriptName": "build-storybook",
|
||||
"exitZeroOnChanges": true,
|
||||
"exitOnceUploaded": true,
|
||||
"onlyChanged": true,
|
||||
"externals": ["public/**"],
|
||||
"skip": "{{ skip_pattern | default('dependabot/**') }}",
|
||||
"ignoreLastBuildOnBranch": "{{ main_branch | default('main') }}"
|
||||
}
|
||||
80
skills/visual-regression/templates/github-workflow.yml.j2
Normal file
80
skills/visual-regression/templates/github-workflow.yml.j2
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Visual Regression Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: {{ branches | default(['main', 'develop']) | tojson }}
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
{% if vr_tool == 'chromatic' -%}
|
||||
chromatic:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Required for Chromatic
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '{{ node_version | default('20') }}'
|
||||
cache: '{{ package_manager | default('npm') }}'
|
||||
|
||||
- name: Install dependencies
|
||||
run: {{ install_command | default('npm ci') }}
|
||||
|
||||
- name: Run Chromatic
|
||||
uses: chromaui/action@latest
|
||||
with:
|
||||
projectToken: {% raw %}${{ secrets.CHROMATIC_PROJECT_TOKEN }}{% endraw %}
|
||||
exitZeroOnChanges: true
|
||||
onlyChanged: true
|
||||
autoAcceptChanges: 'main'
|
||||
{% elif vr_tool == 'percy' -%}
|
||||
percy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '{{ node_version | default('20') }}'
|
||||
cache: '{{ package_manager | default('npm') }}'
|
||||
|
||||
- name: Install dependencies
|
||||
run: {{ install_command | default('npm ci') }}
|
||||
|
||||
- name: Build Storybook
|
||||
run: npm run build-storybook
|
||||
|
||||
- name: Run Percy
|
||||
run: npx percy storybook storybook-static
|
||||
env:
|
||||
PERCY_TOKEN: {% raw %}${{ secrets.PERCY_TOKEN }}{% endraw %}
|
||||
{% elif vr_tool == 'backstopjs' -%}
|
||||
backstop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '{{ node_version | default('20') }}'
|
||||
cache: '{{ package_manager | default('npm') }}'
|
||||
|
||||
- name: Install dependencies
|
||||
run: {{ install_command | default('npm ci') }}
|
||||
|
||||
- name: Run BackstopJS
|
||||
run: npm run backstop:test
|
||||
|
||||
- name: Upload test results
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: backstop-results
|
||||
path: backstop_data/
|
||||
{% endif -%}
|
||||
54
skills/visual-regression/templates/gitlab-ci.yml.j2
Normal file
54
skills/visual-regression/templates/gitlab-ci.yml.j2
Normal file
@@ -0,0 +1,54 @@
|
||||
# Add to .gitlab-ci.yml
|
||||
|
||||
{% if vr_tool == 'chromatic' -%}
|
||||
chromatic:
|
||||
stage: test
|
||||
image: node:{{ node_version | default('20') }}
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
script:
|
||||
- {{ install_command | default('npm ci') }}
|
||||
- npx chromatic --exit-zero-on-changes --only-changed
|
||||
variables:
|
||||
CHROMATIC_PROJECT_TOKEN: $CHROMATIC_PROJECT_TOKEN
|
||||
only:
|
||||
- main
|
||||
- develop
|
||||
- merge_requests
|
||||
{% elif vr_tool == 'percy' -%}
|
||||
percy:
|
||||
stage: test
|
||||
image: node:{{ node_version | default('20') }}
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
script:
|
||||
- {{ install_command | default('npm ci') }}
|
||||
- npm run build-storybook
|
||||
- npx percy storybook storybook-static
|
||||
variables:
|
||||
PERCY_TOKEN: $PERCY_TOKEN
|
||||
only:
|
||||
- main
|
||||
- develop
|
||||
- merge_requests
|
||||
{% elif vr_tool == 'backstopjs' -%}
|
||||
backstop:
|
||||
stage: test
|
||||
image: node:{{ node_version | default('20') }}
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
script:
|
||||
- {{ install_command | default('npm ci') }}
|
||||
- npm run backstop:test
|
||||
artifacts:
|
||||
when: on_failure
|
||||
paths:
|
||||
- backstop_data/
|
||||
only:
|
||||
- main
|
||||
- develop
|
||||
- merge_requests
|
||||
{% endif -%}
|
||||
60
skills/visual-regression/templates/story-template.tsx.j2
Normal file
60
skills/visual-regression/templates/story-template.tsx.j2
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { {{ component_name }} } from './{{ component_name }}';
|
||||
|
||||
const meta = {
|
||||
title: '{{ story_title }}',
|
||||
component: {{ component_name }},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
{% for prop in props -%}
|
||||
{% if prop.values -%}
|
||||
{{ prop.name }}: { control: '{{ prop.control }}', options: {{ prop.values | tojson }} },
|
||||
{% else -%}
|
||||
{{ prop.name }}: { control: '{{ prop.control }}' },
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
},
|
||||
} satisfies Meta<typeof {{ component_name }}>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
{% for prop in props -%}
|
||||
{% if prop.default is string -%}
|
||||
{{ prop.name }}: '{{ prop.default }}',
|
||||
{% elif prop.default is not none -%}
|
||||
{{ prop.name }}: {{ prop.default | tojson }},
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
},
|
||||
};
|
||||
|
||||
{% for variant in variants %}
|
||||
export const {{ variant.name }}: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
{% if variant.value is string -%}
|
||||
{{ variant.prop_name }}: '{{ variant.value }}',
|
||||
{% else -%}
|
||||
{{ variant.prop_name }}: {{ variant.value | tojson }},
|
||||
{% endif -%}
|
||||
},
|
||||
};
|
||||
{% endfor %}
|
||||
|
||||
// Accessibility tests
|
||||
Default.parameters = {
|
||||
a11y: {
|
||||
config: {
|
||||
rules: [
|
||||
{ id: 'color-contrast', enabled: true },
|
||||
{ id: 'label', enabled: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
17
skills/visual-regression/templates/storybook-main.js.j2
Normal file
17
skills/visual-regression/templates/storybook-main.js.j2
Normal file
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
{% if vr_tool == 'chromatic' -%}
|
||||
'@chromatic-com/storybook',
|
||||
{% elif vr_tool == 'percy' -%}
|
||||
'@percy/storybook',
|
||||
{% endif -%}
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/{{ framework | default('react') }}-vite',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user