Web Visual Testing
Platform: web
Applicable Recipes: nextjs-supabase, sveltekit-supabase, t3-stack, django-htmx, go-templ-htmx, react-fastapi-postgres, static-site
Primary Tools: Browser MCP tools, Playwright, Percy, Chromatic
🔄 Tight Loop (Default)
Goal: Get high-signal visual feedback fast (minutes, not hours) with minimal flake.
Start Small:
- •Screens: Focus on 1–3 screens/components most impacted by the story
- •Breakpoints: Start with
mobile(375px) +desktop(1024px) only - •States: Cover default + loading + empty + error + one interaction state (hover/focus)
Run Order:
- •Stabilize UI (disable animations, wait for network idle)
- •Capture screenshots for scoped screens at scoped breakpoints
- •Run accessibility audit + check console errors
- •If diffs occur: decide "expected change" vs "bug", update baselines or fix UI
Expand Only When:
- •Initial findings reveal responsive issues → add tablet/wide
- •Story explicitly covers all breakpoints
- •Epic validation requires full matrix
🌐 Browser MCP Tools
Use these tools for live validation:
| Tool | Purpose | Example |
|---|---|---|
browser_navigate | Navigate to pages | browser_navigate("http://localhost:3000/dashboard") |
browser_resize | Set viewport for responsive | browser_resize(375, 667) |
browser_take_screenshot | Capture current state | browser_take_screenshot("dashboard-mobile.png") |
browser_snapshot | Get element refs for interaction | Before clicks/hovers |
browser_hover | Trigger hover states | browser_hover(element, ref) |
browser_click | Trigger click states | browser_click(element, ref) |
browser_wait_for | Wait for content to load | browser_wait_for(text="Dashboard") |
Audit Tools
| Tool | When to Run | What It Checks |
|---|---|---|
runAccessibilityAudit | Every validation | WCAG violations, ARIA issues |
runPerformanceAudit | When perf targets specified | LCP, CLS, TTI |
runBestPracticesAudit | Every validation | Security, modern APIs |
runSEOAudit | Public/marketing pages | Meta tags, structure |
getConsoleErrors | Every validation | Runtime errors, warnings |
📱 Standard Breakpoints
| Name | Width | Height | When to Test |
|---|---|---|---|
| Mobile | 375 | 667 | Always (Tight Loop) |
| Tablet | 768 | 1024 | When responsive scope |
| Desktop | 1024 | 768 | Always (Tight Loop) |
| Wide | 1280 | 800 | When wide layouts in scope |
| Ultrawide | 1536 | 864 | Admin dashboards only |
🔧 Validation Sequence
Follow this exact sequence for each page:
1. NAVIGATE
browser_navigate(url)
browser_wait_for(text="expected content")
2. STABILIZE (before any screenshots)
- Inject CSS to disable animations
- Wait for network idle
- Mask dynamic content if needed
3. RESPONSIVE CAPTURE (Tight Loop: mobile + desktop only)
For each breakpoint:
browser_resize(width, height)
browser_take_screenshot("{page}-{breakpoint}.png")
4. STATE CAPTURE
For each interactive component:
browser_snapshot() # Get refs
browser_hover(element, ref)
browser_take_screenshot("{component}-hover.png")
5. AUDITS
runAccessibilityAudit()
getConsoleErrors()
runPerformanceAudit() # If targets specified
6. VALIDATE
Compare screenshots to wireframes
Check ui-spec.md Testing Checklist
Report findings
🎭 Dynamic Content Handling
Before taking screenshots, stabilize dynamic content:
CSS Injection (via browser_evaluate):
// Disable all animations
document.head.insertAdjacentHTML('beforeend', `
<style>
*, *::before, *::after {
animation-duration: 0s !important;
transition-duration: 0s !important;
}
</style>
`);
Masking (for Playwright CI):
await expect(page).toHaveScreenshot({
mask: [
page.getByTestId('timestamp'),
page.getByTestId('avatar'),
page.getByTestId('ad-slot')
]
});
Tolerance (for minor differences):
await expect(page).toHaveScreenshot({
maxDiffPixels: 100, // Allow up to 100 different pixels
maxDiffPixelRatio: 0.01, // Or 1% of total
threshold: 0.2 // Per-pixel color threshold
});
🧪 Playwright for CI
For CI/CD visual regression, set up Playwright:
# Run visual tests npx playwright test --project=chromium # Update baselines when changes are intentional npx playwright test --update-snapshots
Key Patterns:
// Visual assertion
await expect(page).toHaveScreenshot('dashboard.png');
// Component screenshot
await expect(page.getByTestId('header')).toHaveScreenshot('header.png');
// Full page
await expect(page).toHaveScreenshot('page.png', { fullPage: true });
✅ Validation Criteria
| Check | Pass Criteria | Blocking? |
|---|---|---|
| Responsive layout | Correct at mobile + desktop | Yes |
| Design tokens | No hardcoded colors/spacing in changed files | Yes |
| Accessibility | 0 critical/serious issues | Yes |
| Console errors | 0 errors (warnings OK) | Yes |
| Performance | LCP < 2.5s, CLS < 0.1 (if targets specified) | No |
| Voice/tone | Matches ux-strategy.md | No |
🔗 Integration with Storybook
If the project uses Storybook:
- •Run Storybook:
npm run storybook - •Navigate to component stories
- •Capture screenshots of each state
- •Use Chromatic or Percy for automated regression
# Chromatic (if configured) npx chromatic --project-token=xxx # Percy (if configured) npx percy storybook