Visual regression testing with Chromatic, Lost Pixel, and Playwright snapshots. Use when detecting UI changes, maintaining visual consistency, reviewing design changes, or setting up screenshot comparison in CI/CD.
61
72%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./skills/visual-testing/SKILL.mdVisual regression testing catches unintended UI changes by comparing screenshots. This skill covers three approaches:
npm install --save-dev chromatic# Set token
export CHROMATIC_PROJECT_TOKEN=your_token_here
# Run visual tests with Playwright
npx chromatic --playwright// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/visual',
use: {
screenshot: 'on',
},
reporter: [
['html'],
['chromatic', { projectToken: process.env.CHROMATIC_PROJECT_TOKEN }],
],
});// tests/visual/homepage.spec.ts
import { test, expect } from '@playwright/test';
test('homepage visual', async ({ page }) => {
await page.goto('/');
// Full page screenshot
await expect(page).toHaveScreenshot('homepage.png');
});
test('dashboard visual', async ({ page }) => {
await page.goto('/dashboard');
// Wait for data to load
await page.waitForSelector('.dashboard-loaded');
// Component screenshot
const chart = page.locator('.revenue-chart');
await expect(chart).toHaveScreenshot('revenue-chart.png');
});# .github/workflows/visual.yml
name: Visual Tests
on: [push, pull_request]
jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for Chromatic
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npx chromatic --playwright
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}| Feature | Free Tier |
|---|---|
| Snapshots | 5000/month |
| Parallelization | Unlimited |
| Browsers | Chrome, Firefox, Safari |
| Review UI | Yes |
| GitHub Integration | Yes |
For confidential projects that can't use cloud services.
npm install --save-dev lost-pixel// lostpixel.config.ts
import { CustomProjectConfig } from 'lost-pixel';
export const config: CustomProjectConfig = {
pageShots: {
pages: [
{ path: '/', name: 'home' },
{ path: '/login', name: 'login' },
{ path: '/dashboard', name: 'dashboard' },
],
baseUrl: 'http://localhost:3000',
},
// Store baselines locally
imagePathBaseline: '.lostpixel/baseline',
imagePathCurrent: '.lostpixel/current',
imagePathDifference: '.lostpixel/difference',
// Fail on difference
generateOnly: false,
failOnDifference: true,
// Comparison settings
threshold: 0.1, // 0.1% pixel difference allowed
};# Generate baselines (first run)
npx lost-pixel update
# Run comparison
npx lost-pixel
# View differences in .lostpixel/difference/# Dockerfile.lostpixel
FROM mcr.microsoft.com/playwright:v1.40.0-focal
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "lost-pixel"]# .github/workflows/visual-selfhosted.yml
jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -f Dockerfile.lostpixel -t visual-tests .
- run: docker run visual-testsSimplest approach, no external services.
// tests/visual/basic.spec.ts
import { test, expect } from '@playwright/test';
test('visual regression', async ({ page }) => {
await page.goto('/');
// Full page
await expect(page).toHaveScreenshot('homepage.png');
// With options
await expect(page).toHaveScreenshot('homepage-full.png', {
fullPage: true,
maxDiffPixels: 100, // Allow 100 pixel difference
});
// Specific element
const header = page.locator('header');
await expect(header).toHaveScreenshot('header.png');
});# Update all baselines
npx playwright test --update-snapshots
# Update specific test
npx playwright test tests/visual/homepage.spec.ts --update-snapshots// playwright.config.ts
export default defineConfig({
expect: {
toHaveScreenshot: {
maxDiffPixels: 100,
maxDiffPixelRatio: 0.01, // 1%
threshold: 0.2, // Color difference threshold
},
},
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
});Problem: Screenshots differ between Mac/Linux/Windows.
Solutions:
# Run tests in consistent environment
docker run --rm -v $(pwd):/app mcr.microsoft.com/playwright:v1.40.0 \
npx playwright test --update-snapshots// playwright.config.ts
export default defineConfig({
snapshotPathTemplate: '{testDir}/__screenshots__/{platform}/{testFilePath}/{arg}{ext}',
});await expect(page).toHaveScreenshot('page.png', {
maxDiffPixelRatio: 0.05, // 5% tolerance
});| Feature | Chromatic | Lost Pixel | Playwright |
|---|---|---|---|
| Free Tier | 5000 snapshots | Unlimited | Unlimited |
| Self-Hosted | No | Yes | Yes |
| Parallelization | Unlimited | Manual | Manual |
| Review UI | Yes (web) | No (local files) | No (local files) |
| Cross-Browser | Yes | Yes | Yes |
| AI Detection | Yes | No | No |
| Setup Time | 5 min | 15 min | 5 min |
| Best For | Teams, CI/CD | Confidential | Simple projects |
// Wait for dynamic content
await page.waitForSelector('[data-loaded="true"]');
await page.waitForLoadState('networkidle');
// Hide dynamic elements
await page.evaluate(() => {
document.querySelectorAll('.timestamp').forEach(el => el.style.visibility = 'hidden');
});
await expect(page).toHaveScreenshot('page.png');// playwright.config.ts
export default defineConfig({
use: {
viewport: { width: 1280, height: 720 },
},
});
// Or per-test
test('mobile view', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await expect(page).toHaveScreenshot('mobile.png');
});// Freeze time
await page.addInitScript(() => {
Date.now = () => new Date('2024-01-15T10:00:00').getTime();
});
// Mock API responses
await page.route('**/api/user', route => {
route.fulfill({
json: { name: 'Test User', avatar: '/default-avatar.png' },
});
});
await expect(page).toHaveScreenshot('profile.png');// Test individual components
test('button states', async ({ page }) => {
await page.goto('/storybook/button');
const button = page.locator('.button');
// Default state
await expect(button).toHaveScreenshot('button-default.png');
// Hover state
await button.hover();
await expect(button).toHaveScreenshot('button-hover.png');
// Active state
await button.click({ force: true });
await expect(button).toHaveScreenshot('button-active.png');
});1. Developer pushes code
↓
2. CI runs visual tests
↓
3. Chromatic detects changes
↓
4. Review visual diff in Chromatic UI
↓
5. Approve → New baseline
Reject → Fix and re-push
↓
6. Merge PR# Update baselines locally
npx playwright test --update-snapshots
# Commit baselines to git
git add tests/__screenshots__/
git commit -m "Update visual baselines"
# Or use Chromatic (baselines stored in cloud)
npx chromatic --auto-accept-changes # Accept all changesreferences/chromatic-setup.md - Complete Chromatic configurationreferences/lost-pixel-self-hosted.md - Self-hosted setup guidereferences/playwright-snapshots.md - Built-in snapshot testingVisual testing catches UI regressions before users do - choose Chromatic for teams, Lost Pixel for confidential projects.
93ed392
Also appears in
since Sep 12, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.