mcpField
V
visual-regression-tester

visual-regression-tester

github.com/patricio0312revテスト・品質63332 回インストール41 週間前に更新
リポジトリ

visual-regression-tester について

visual-regression-testerは、意図しないUI変更を検出する自動ビジュアル回帰テストの設定スキルです。Playwright、Chromatic、Percyを使ったスクリーンショットのベースライン、差分しきい値、レビュー、CIワークフローを扱います。

主な機能

  1. 01スクリーンショットのベースライン取得と更新
  2. 02許容する視覚差分しきい値の設定
  3. 03レスポンシブ画面幅とダークモードのテスト
  4. 04ビジュアルチェックのCI連携
  5. 05視覚的変更のレビューと承認

活用シーン

  1. 01プルリクエストで意図しないUI変更を検出
  2. 02画面幅ごとのレイアウト確認
  3. 03コンポーネントの外観とダークモードの検証

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/visual-regression-testerを入力して読み込まれたか確認します。

詳細

ビジュアルリグレッションテスト

自動ビジュアルリグレッションテストで、意図しない UI の変更を検出します。

基本的なワークフロー

  1. ツールを選択:Playwright、Chromatic、Percy
  2. ベースラインを設定:初期スクリーンショットをキャプチャします
  3. しきい値を設定:許容する差分を定義します
  4. CI に統合:テストを自動化します
  5. 変更を確認:承認または却下します
  6. ベースラインを更新:意図した変更を受け入れます

Playwright によるビジュアルテスト

インストール
npm install -D @playwright/test
npx playwright install
設定
// 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,
  },
});
ビジュアルテストの例
// 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');
  });
});
コンポーネントのビジュアルテスト
// 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');
  });
});
レスポンシブテスト
// 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`
        );
      }
    });
  });
}
ダークモードテスト
// 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 との統合

セットアップ
npm install -D chromatic
設定
// .storybook/main.ts
export default {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@chromatic-com/storybook'],
};
CI の設定
# .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 のストーリー設定
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  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<typeof Button> = {
  args: { variant: 'primary', children: 'Button' },
};

export const AllStates: StoryObj<typeof Button> = {
  parameters: {
    chromatic: {
      // Test interaction states
      modes: {
        hover: { pseudo: { hover: true } },
        focus: { pseudo: { focus: true } },
        active: { pseudo: { active: true } },
      },
    },
  },
  render: () => (
    <div style={{ display: 'flex', gap: '1rem' }}>
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button disabled>Disabled</Button>
    </div>
  ),
};

CI との統合

# .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
ベースライン更新スクリプト
// 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"
  }
}

ベストプラクティス

  1. アニメーションを無効化:スクリーンショットの一貫性を保ちます
  2. ネットワークの待機:コンテンツが読み込まれたことを確認します
  3. 安定したセレクターを使用:data-testid 属性を使用します
  4. 複数のビューポートでテスト:レスポンシブ表示を網羅します
  5. しきい値を設定:軽微なピクセル差を許容します
  6. CI で確認:失敗時にマージをブロックします
  7. スナップショットを整理:分かりやすい命名規則を使用します
  8. 意図的に更新:ベースラインの変更をすべて確認します

出力チェックリスト

すべてのビジュアルテスト環境には、次の項目を含めてください。

  • Playwright/Chromatic の設定
  • ベースラインのスクリーンショット
  • 複数のビューポートでのテスト
  • ダークモードのテスト
  • コンポーネントの状態テスト
  • アニメーションの無効化
  • CI との統合
  • 差分のしきい値設定
  • ベースライン更新のワークフロー
  • 成果物の保存

コメント