visual-regression-tester

安装量: 54
排名: #13728

安装

npx skills add https://github.com/patricio0312rev/skills --skill visual-regression-tester

Visual Regression Tester

Catch unintended UI changes with automated visual regression testing.

Core Workflow Choose tool: Playwright, Chromatic, Percy Setup baseline: Capture initial screenshots Configure thresholds: Define acceptable diff Integrate CI: Automated testing Review changes: Approve or reject Update baselines: Accept intentional changes Playwright Visual Testing Installation npm install -D @playwright/test npx playwright install

Configuration // playwright.config.ts import { defineConfig, devices } from '@playwright/test';

export default defineConfig({ testDir: './tests/visual', testMatch: '*/.visual.ts', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: [ ['html', { open: 'never' }], ['json', { outputFile: 'test-results/results.json' }], ],

// Snapshot configuration snapshotDir: './tests/visual/snapshots', snapshotPathTemplate: '{snapshotDir}/{testFilePath}/{arg}{ext}',

expect: { toHaveScreenshot: { maxDiffPixels: 100, maxDiffPixelRatio: 0.01, threshold: 0.2, animations: 'disabled', }, toMatchSnapshot: { maxDiffPixelRatio: 0.01, }, },

use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', },

projects: [ { name: 'Desktop Chrome', use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 720 }, }, }, { name: 'Desktop Firefox', use: { ...devices['Desktop Firefox'], viewport: { width: 1280, height: 720 }, }, }, { name: 'Mobile Safari', use: { ...devices['iPhone 13'], }, }, { name: 'Tablet', use: { viewport: { width: 768, height: 1024 }, }, }, ],

webServer: { command: 'npm run start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 120 * 1000, }, });

Visual Test Examples // tests/visual/homepage.visual.ts import { test, expect } from '@playwright/test';

test.describe('Homepage Visual Tests', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); // Wait for fonts and images to load await page.waitForLoadState('networkidle'); // Disable animations for consistent screenshots await page.addStyleTag({ content: *, *::before, *::after { animation-duration: 0s !important; animation-delay: 0s !important; transition-duration: 0s !important; }, }); });

test('full page screenshot', async ({ page }) => { await expect(page).toHaveScreenshot('homepage-full.png', { fullPage: true, }); });

test('hero section', async ({ page }) => { const hero = page.locator('[data-testid="hero-section"]'); await expect(hero).toHaveScreenshot('hero-section.png'); });

test('navigation bar', async ({ page }) => { const nav = page.locator('nav'); await expect(nav).toHaveScreenshot('navigation.png'); });

test('footer', async ({ page }) => { const footer = page.locator('footer'); await footer.scrollIntoViewIfNeeded(); await expect(footer).toHaveScreenshot('footer.png'); }); });

Component Visual Tests // tests/visual/components.visual.ts import { test, expect } from '@playwright/test';

test.describe('Button Component', () => { test('all variants', async ({ page }) => { await page.goto('/storybook/button');

// Primary button
const primary = page.locator('[data-testid="button-primary"]');
await expect(primary).toHaveScreenshot('button-primary.png');

// Secondary button
const secondary = page.locator('[data-testid="button-secondary"]');
await expect(secondary).toHaveScreenshot('button-secondary.png');

// Hover state
await primary.hover();
await expect(primary).toHaveScreenshot('button-primary-hover.png');

// Focus state
await primary.focus();
await expect(primary).toHaveScreenshot('button-primary-focus.png');

// Disabled state
const disabled = page.locator('[data-testid="button-disabled"]');
await expect(disabled).toHaveScreenshot('button-disabled.png');

}); });

test.describe('Form Components', () => { test('input states', async ({ page }) => { await page.goto('/storybook/input');

const input = page.locator('[data-testid="input-default"]');

// Default state
await expect(input).toHaveScreenshot('input-default.png');

// Focused state
await input.focus();
await expect(input).toHaveScreenshot('input-focused.png');

// With value
await input.fill('Test value');
await expect(input).toHaveScreenshot('input-with-value.png');

// Error state
const errorInput = page.locator('[data-testid="input-error"]');
await expect(errorInput).toHaveScreenshot('input-error.png');

}); });

Responsive Testing // tests/visual/responsive.visual.ts import { test, expect, devices } from '@playwright/test';

const viewports = [ { name: 'mobile', width: 375, height: 667 }, { name: 'tablet', width: 768, height: 1024 }, { name: 'desktop', width: 1280, height: 720 }, { name: 'wide', width: 1920, height: 1080 }, ];

for (const viewport of viewports) { test.describe(${viewport.name} viewport, () => { test.use({ viewport: { width: viewport.width, height: viewport.height } });

test('homepage layout', async ({ page }) => {
  await page.goto('/');
  await page.waitForLoadState('networkidle');

  await expect(page).toHaveScreenshot(`homepage-${viewport.name}.png`, {
    fullPage: true,
  });
});

test('navigation menu', async ({ page }) => {
  await page.goto('/');

  if (viewport.width < 768) {
    // Mobile: test hamburger menu
    const menuButton = page.locator('[data-testid="mobile-menu-button"]');
    await menuButton.click();
    await expect(page.locator('[data-testid="mobile-menu"]')).toHaveScreenshot(
      `mobile-menu-${viewport.name}.png`
    );
  } else {
    // Desktop: test full nav
    await expect(page.locator('nav')).toHaveScreenshot(
      `nav-${viewport.name}.png`
    );
  }
});

}); }

Dark Mode Testing // tests/visual/dark-mode.visual.ts import { test, expect } from '@playwright/test';

test.describe('Dark Mode', () => { test('homepage in dark mode', async ({ page }) => { await page.goto('/');

// Enable dark mode via color scheme
await page.emulateMedia({ colorScheme: 'dark' });

await expect(page).toHaveScreenshot('homepage-dark.png', {
  fullPage: true,
});

});

test('homepage in light mode', async ({ page }) => { await page.goto('/');

await page.emulateMedia({ colorScheme: 'light' });

await expect(page).toHaveScreenshot('homepage-light.png', {
  fullPage: true,
});

});

test('theme toggle', async ({ page }) => { await page.goto('/');

// Toggle theme via button
const themeToggle = page.locator('[data-testid="theme-toggle"]');
await themeToggle.click();

// Wait for transition
await page.waitForTimeout(300);

await expect(page).toHaveScreenshot('homepage-toggled-theme.png');

}); });

Chromatic Integration Setup npm install -D chromatic

Configuration // .storybook/main.ts export default { stories: ['../src/*/.stories.@(js|jsx|ts|tsx)'], addons: ['@chromatic-com/storybook'], };

CI Configuration

.github/workflows/chromatic.yml

name: Chromatic

on: push: branches: [main] pull_request: branches: [main]

jobs: chromatic: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0

  - uses: actions/setup-node@v4
    with:
      node-version: 20
      cache: 'npm'

  - run: npm ci

  - name: Publish to Chromatic
    uses: chromaui/action@latest
    with:
      projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
      exitZeroOnChanges: true
      exitOnceUploaded: true
      onlyChanged: true

Chromatic Story Configuration // Button.stories.tsx import type { Meta, StoryObj } from '@storybook/react'; import { Button } from './Button';

const meta: Meta = { component: Button, parameters: { chromatic: { // Capture multiple viewports viewports: [375, 768, 1280], // Delay for animations delay: 300, // Disable animations pauseAnimationAtEnd: true, }, }, };

export default meta;

export const Primary: StoryObj = { args: { variant: 'primary', children: 'Button' }, };

export const AllStates: StoryObj = { parameters: { chromatic: { // Test interaction states modes: { hover: { pseudo: { hover: true } }, focus: { pseudo: { focus: true } }, active: { pseudo: { active: true } }, }, }, }, render: () => (

), };

CI Integration

.github/workflows/visual-tests.yml

name: Visual Regression Tests

on: pull_request: branches: [main]

jobs: visual-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4

  - uses: actions/setup-node@v4
    with:
      node-version: 20
      cache: 'npm'

  - run: npm ci

  - name: Install Playwright Browsers
    run: npx playwright install --with-deps chromium

  - name: Build application
    run: npm run build

  - name: Run visual tests
    run: npx playwright test --project="Desktop Chrome"

  - name: Upload test results
    if: always()
    uses: actions/upload-artifact@v4
    with:
      name: playwright-report
      path: |
        playwright-report/
        test-results/
      retention-days: 30

  - name: Upload diff images
    if: failure()
    uses: actions/upload-artifact@v4
    with:
      name: visual-diffs
      path: tests/visual/__snapshots__/*-diff.png
      retention-days: 7

Update Baselines Script // package.json { "scripts": { "test:visual": "playwright test --project='Desktop Chrome'", "test:visual:update": "playwright test --update-snapshots", "test:visual:ui": "playwright test --ui", "test:visual:report": "playwright show-report" } }

Best Practices Disable animations: Consistent screenshots Wait for network: Ensure content loaded Use stable selectors: data-testid attributes Test multiple viewports: Responsive coverage Set thresholds: Allow minor pixel differences Review in CI: Block merges on failures Organize snapshots: Clear naming convention Update intentionally: Review all baseline changes Output Checklist

Every visual testing setup should include:

Playwright/Chromatic configuration Baseline screenshots Multi-viewport testing Dark mode coverage Component state testing Animation disabling CI integration Diff threshold configuration Baseline update workflow Artifact storage

返回排行榜