T-CREATOR

Tailwind CSS と Playwright で UI テストを自動化するベストプラクティス

Tailwind CSS と Playwright で UI テストを自動化するベストプラクティス

現代の Web 開発において、UI の品質保証は開発速度と同等に重要な課題となっています。特に Tailwind CSS のようなユーティリティファーストの CSS フレームワークを使用している場合、クラス名の変更やレイアウトの調整が頻繁に発生し、手動でのテストが追いつかない状況に陥りがちです。

そんな中で注目を集めているのが、Microsoft が開発した Playwright という E2E テストツールです。Playwright は、Tailwind CSS プロジェクトの特性を活かした効率的な UI テスト自動化を実現し、開発者の心の負担を大幅に軽減してくれます。

この記事では、Tailwind CSS と Playwright を組み合わせた実践的なテスト戦略を、実際のコード例と共に詳しく解説していきます。あなたの開発プロセスに革命的な変化をもたらす、確実で効率的なテスト自動化の世界へご案内いたします。

Tailwind CSS と Playwright の組み合わせがもたらす価値

Tailwind CSS と Playwright の組み合わせは、単なる技術的な相性の良さを超えた、開発体験の根本的な改善をもたらします。

開発効率の劇的向上

Tailwind CSS のユーティリティクラスは、一見するとテストが困難に見えますが、実は Playwright の強力なセレクター機能と相性抜群です。クラス名の変更が頻繁に発生しても、適切なセレクター戦略を採用することで、テストの保守性を保ちながら開発速度を維持できます。

品質保証の自動化

従来の手動テストでは、レスポンシブデザインの確認やダークモードの切り替えテストに膨大な時間がかかっていました。Playwright のマルチブラウザ・マルチデバイス対応により、これらの作業が完全に自動化され、人間のミスを排除できます。

コスト削減とリスク軽減

UI の不具合は、ユーザー体験に直接影響し、ビジネスに大きな損失をもたらす可能性があります。Playwright による自動化テストにより、リリース前の品質チェックが確実になり、本番環境での問題発生リスクを大幅に軽減できます。

Playwright の基本セットアップ

まずは、Tailwind CSS プロジェクトに Playwright を導入する手順から始めましょう。

プロジェクトの初期化

既存の Tailwind CSS プロジェクトに Playwright を追加する場合の手順です:

bash# Playwrightのインストール
yarn add -D @playwright/test

# Playwrightの初期化
npx playwright install

設定ファイルの作成

Playwright の設定ファイルを作成し、Tailwind CSS プロジェクトに最適化します:

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],
  webServer: {
    command: 'yarn dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

基本的なテストファイルの作成

最初のテストファイルを作成して、セットアップが正常に動作することを確認しましょう:

typescript// tests/home.spec.ts
import { test, expect } from '@playwright/test';

test.describe('ホームページの基本テスト', () => {
  test('ページが正常に読み込まれる', async ({ page }) => {
    await page.goto('/');

    // ページタイトルの確認
    await expect(page).toHaveTitle(/Tailwind CSS/);

    // メインコンテンツの存在確認
    await expect(page.locator('main')).toBeVisible();
  });

  test('ナビゲーションが正常に動作する', async ({
    page,
  }) => {
    await page.goto('/');

    // ナビゲーションリンクの確認
    const navLinks = page.locator('nav a');
    await expect(navLinks).toHaveCount(3);

    // リンクのクリックテスト
    await navLinks.first().click();
    await expect(page).toHaveURL(/.*about/);
  });
});

Tailwind CSS コンポーネントのテスト戦略

Tailwind CSS のコンポーネントを効果的にテストするためには、ユーティリティクラスの特性を理解した戦略が必要です。

コンポーネントの構造化テスト

Tailwind CSS で作成されたコンポーネントの構造をテストする方法です:

typescript// tests/components/button.spec.ts
import { test, expect } from '@playwright/test';

test.describe('ボタンコンポーネント', () => {
  test('プライマリボタンのスタイル確認', async ({
    page,
  }) => {
    await page.goto('/components');

    const primaryButton = page.locator(
      'button.bg-blue-500'
    );

    // ボタンの存在確認
    await expect(primaryButton).toBeVisible();

    // テキスト内容の確認
    await expect(primaryButton).toHaveText('送信');

    // クラス名の確認
    await expect(primaryButton).toHaveClass(/bg-blue-500/);
    await expect(primaryButton).toHaveClass(/text-white/);
  });

  test('ボタンのホバー効果', async ({ page }) => {
    await page.goto('/components');

    const button = page.locator(
      'button.hover\\:bg-blue-600'
    );

    // ホバー前の状態確認
    await expect(button).toHaveCSS(
      'background-color',
      'rgb(59, 130, 246)'
    );

    // ホバー効果のテスト
    await button.hover();
    await expect(button).toHaveCSS(
      'background-color',
      'rgb(37, 99, 235)'
    );
  });
});

レスポンシブクラスのテスト

Tailwind CSS のレスポンシブクラスが正しく適用されているかをテストします:

typescript// tests/responsive.spec.ts
import { test, expect } from '@playwright/test';

test.describe('レスポンシブデザイン', () => {
  test('モバイル表示でのレイアウト', async ({ page }) => {
    // モバイルサイズに設定
    await page.setViewportSize({ width: 375, height: 667 });
    await page.goto('/');

    // モバイル用クラスの確認
    const mobileMenu = page.locator('.md\\:hidden');
    await expect(mobileMenu).toBeVisible();

    // デスクトップ用クラスの非表示確認
    const desktopMenu = page.locator('.hidden.md\\:block');
    await expect(desktopMenu).not.toBeVisible();
  });

  test('デスクトップ表示でのレイアウト', async ({
    page,
  }) => {
    // デスクトップサイズに設定
    await page.setViewportSize({
      width: 1920,
      height: 1080,
    });
    await page.goto('/');

    // デスクトップ用クラスの確認
    const desktopMenu = page.locator('.hidden.md\\:block');
    await expect(desktopMenu).toBeVisible();
  });
});

セレクター設計のベストプラクティス

Tailwind CSS プロジェクトでは、適切なセレクター設計がテストの保守性を左右します。

data-testid 属性の活用

最も推奨される方法は、テスト専用の data-testid 属性を使用することです:

typescript// コンポーネント側の実装例
function Button({
  children,
  variant = 'primary',
  ...props
}) {
  return (
    <button
      data-testid='button'
      className={`px-4 py-2 rounded ${
        variant === 'primary'
          ? 'bg-blue-500 text-white hover:bg-blue-600'
          : 'bg-gray-500 text-white hover:bg-gray-600'
      }`}
      {...props}
    >
      {children}
    </button>
  );
}

// テストファイル
test('ボタンのバリアント切り替え', async ({ page }) => {
  await page.goto('/components');

  // data-testidを使用したセレクター
  const primaryButton = page
    .locator('[data-testid="button"]')
    .first();
  const secondaryButton = page
    .locator('[data-testid="button"]')
    .nth(1);

  await expect(primaryButton).toHaveClass(/bg-blue-500/);
  await expect(secondaryButton).toHaveClass(/bg-gray-500/);
});

クラス名ベースのセレクター

data-testid が使用できない場合の代替手段です:

typescript// より具体的なクラス名の組み合わせを使用
test('フォームのバリデーション', async ({ page }) => {
  await page.goto('/contact');

  // 複数のクラスを組み合わせて特定
  const emailInput = page.locator(
    'input[type="email"].border-red-500'
  );
  const submitButton = page.locator(
    'button[type="submit"].bg-blue-500'
  );

  // 無効なメールアドレスを入力
  await emailInput.fill('invalid-email');
  await submitButton.click();

  // エラーメッセージの確認
  const errorMessage = page.locator(
    '.text-red-500.text-sm'
  );
  await expect(errorMessage).toBeVisible();
  await expect(errorMessage).toContainText(
    '有効なメールアドレスを入力してください'
  );
});

レスポンシブデザインのテスト自動化

Tailwind CSS のレスポンシブ機能を確実にテストする方法を紹介します。

ブレークポイント別テスト

typescript// tests/responsive-breakpoints.spec.ts
import { test, expect } from '@playwright/test';

const breakpoints = {
  sm: { width: 640, height: 800 },
  md: { width: 768, height: 800 },
  lg: { width: 1024, height: 800 },
  xl: { width: 1280, height: 800 },
  '2xl': { width: 1536, height: 800 },
};

test.describe('ブレークポイント別テスト', () => {
  for (const [name, size] of Object.entries(breakpoints)) {
    test(`${name}ブレークポイントでの表示`, async ({
      page,
    }) => {
      await page.setViewportSize(size);
      await page.goto('/');

      // 各ブレークポイントでの表示確認
      if (name === 'sm') {
        await expect(
          page.locator('.sm\\:block')
        ).toBeVisible();
        await expect(
          page.locator('.hidden.sm\\:block')
        ).not.toBeVisible();
      } else if (name === 'md') {
        await expect(page.locator('.md\\:flex')).toHaveCSS(
          'display',
          'flex'
        );
      }
    });
  }
});

グリッドレイアウトのテスト

typescripttest('グリッドレイアウトのレスポンシブ動作', async ({
  page,
}) => {
  await page.goto('/products');

  // デスクトップ表示
  await page.setViewportSize({ width: 1920, height: 1080 });
  const desktopGrid = page.locator(
    '.grid-cols-1.md\\:grid-cols-2.lg\\:grid-cols-3'
  );
  await expect(desktopGrid).toHaveCSS(
    'grid-template-columns',
    /repeat\(3/
  );

  // タブレット表示
  await page.setViewportSize({ width: 768, height: 1024 });
  await expect(desktopGrid).toHaveCSS(
    'grid-template-columns',
    /repeat\(2/
  );

  // モバイル表示
  await page.setViewportSize({ width: 375, height: 667 });
  await expect(desktopGrid).toHaveCSS(
    'grid-template-columns',
    /repeat\(1/
  );
});

ダークモード・テーマ切り替えのテスト

Tailwind CSS のダークモード機能をテストする方法を紹介します。

ダークモード切り替えのテスト

typescript// tests/dark-mode.spec.ts
import { test, expect } from '@playwright/test';

test.describe('ダークモード機能', () => {
  test('ダークモード切り替えボタンの動作', async ({
    page,
  }) => {
    await page.goto('/');

    const themeToggle = page.locator(
      '[data-testid="theme-toggle"]'
    );

    // 初期状態(ライトモード)の確認
    await expect(page.locator('html')).toHaveClass(/light/);
    await expect(page.locator('.bg-white')).toBeVisible();

    // ダークモードに切り替え
    await themeToggle.click();
    await expect(page.locator('html')).toHaveClass(/dark/);
    await expect(
      page.locator('.dark .bg-gray-900')
    ).toBeVisible();
  });

  test('システム設定に応じた自動切り替え', async ({
    page,
  }) => {
    // システムのダークモード設定をシミュレート
    await page.addInitScript(() => {
      Object.defineProperty(window.matchMedia, 'matches', {
        value: true,
        writable: true,
      });
    });

    await page.goto('/');

    // システム設定に応じてダークモードが適用されることを確認
    await expect(page.locator('html')).toHaveClass(/dark/);
  });
});

カスタムテーマのテスト

typescripttest('カスタムテーマの適用', async ({ page }) => {
  await page.goto('/settings');

  // テーマ選択のテスト
  const themeSelect = page.locator(
    'select[data-testid="theme-select"]'
  );
  await themeSelect.selectOption('purple');

  // カスタムテーマクラスの適用確認
  await expect(
    page.locator('.bg-purple-500')
  ).toBeVisible();
  await expect(
    page.locator('.text-purple-900')
  ).toBeVisible();
});

アニメーション・トランジションのテスト

Tailwind CSS のアニメーション機能をテストする方法を紹介します。

トランジション効果のテスト

typescript// tests/animations.spec.ts
import { test, expect } from '@playwright/test';

test.describe('アニメーション・トランジション', () => {
  test('ホバー時のトランジション効果', async ({ page }) => {
    await page.goto('/components');

    const animatedButton = page.locator(
      'button.transition-all.duration-300'
    );

    // 初期状態の確認
    const initialTransform = await animatedButton.evaluate(
      (el) => window.getComputedStyle(el).transform
    );

    // ホバー効果のテスト
    await animatedButton.hover();

    // トランジション完了を待機
    await page.waitForTimeout(300);

    const hoveredTransform = await animatedButton.evaluate(
      (el) => window.getComputedStyle(el).transform
    );

    // 変形が発生したことを確認
    expect(hoveredTransform).not.toBe(initialTransform);
  });

  test('フェードインアニメーション', async ({ page }) => {
    await page.goto('/');

    // アニメーション要素の確認
    const fadeElement = page.locator('.animate-fade-in');
    await expect(fadeElement).toBeVisible();

    // アニメーションクラスの確認
    await expect(fadeElement).toHaveClass(
      /animate-fade-in/
    );
  });
});

ローディングアニメーションのテスト

typescripttest('ローディングスピナーの動作', async ({ page }) => {
  await page.goto('/data');

  // データ読み込みボタンをクリック
  await page.locator('[data-testid="load-data"]').click();

  // ローディングスピナーの表示確認
  const spinner = page.locator('.animate-spin');
  await expect(spinner).toBeVisible();

  // データ読み込み完了を待機
  await page.waitForSelector('.data-loaded', {
    timeout: 10000,
  });

  // スピナーが非表示になることを確認
  await expect(spinner).not.toBeVisible();
});

アクセシビリティテストの統合

Tailwind CSS プロジェクトでアクセシビリティを確保するためのテスト方法を紹介します。

ARIA 属性のテスト

typescript// tests/accessibility.spec.ts
import { test, expect } from '@playwright/test';

test.describe('アクセシビリティ', () => {
  test('フォームのアクセシビリティ', async ({ page }) => {
    await page.goto('/contact');

    // ラベルとフォーム要素の関連付け
    const emailLabel = page.locator('label[for="email"]');
    const emailInput = page.locator('#email');

    await expect(emailLabel).toBeVisible();
    await expect(emailInput).toHaveAttribute(
      'aria-required',
      'true'
    );

    // エラー状態のARIA属性
    await emailInput.fill('invalid');
    await page.locator('button[type="submit"]').click();

    await expect(emailInput).toHaveAttribute(
      'aria-invalid',
      'true'
    );
    await expect(emailInput).toHaveAttribute(
      'aria-describedby'
    );
  });

  test('ナビゲーションのアクセシビリティ', async ({
    page,
  }) => {
    await page.goto('/');

    // ナビゲーションのロール確認
    const nav = page.locator('nav');
    await expect(nav).toHaveAttribute('role', 'navigation');

    // リンクのaria-label確認
    const links = page.locator('nav a[aria-label]');
    await expect(links).toHaveCount(3);
  });
});

キーボードナビゲーションのテスト

typescripttest('キーボードナビゲーション', async ({ page }) => {
  await page.goto('/');

  // Tabキーでのフォーカス移動
  await page.keyboard.press('Tab');

  const firstFocusable = page
    .locator('button, a, input')
    .first();
  await expect(firstFocusable).toBeFocused();

  // フォーカスインジケーターの確認
  await expect(firstFocusable).toHaveCSS('outline', /none/);
  await expect(firstFocusable).toHaveClass(
    /focus:outline-none/
  );
});

パフォーマンステストとの連携

Tailwind CSS のパフォーマンスを監視するテスト方法を紹介します。

ページ読み込み速度のテスト

typescript// tests/performance.spec.ts
import { test, expect } from '@playwright/test';

test.describe('パフォーマンステスト', () => {
  test('ページ読み込み時間の測定', async ({ page }) => {
    const startTime = Date.now();
    await page.goto('/');
    const loadTime = Date.now() - startTime;

    // 読み込み時間が3秒以内であることを確認
    expect(loadTime).toBeLessThan(3000);
  });

  test('CSSファイルの最適化確認', async ({ page }) => {
    await page.goto('/');

    // 使用されていないCSSクラスの確認
    const response = await page.waitForResponse('**/*.css');
    const cssContent = await response.text();

    // 重要なTailwindクラスが含まれていることを確認
    expect(cssContent).toContain('.bg-blue-500');
    expect(cssContent).toContain('.text-white');
  });
});

メモリ使用量の監視

typescripttest('メモリリークの検出', async ({ page }) => {
  await page.goto('/');

  // 初期メモリ使用量の記録
  const initialMemory = await page.evaluate(
    () => performance.memory?.usedJSHeapSize || 0
  );

  // 複数回のページ操作を実行
  for (let i = 0; i < 10; i++) {
    await page
      .locator('[data-testid="toggle-menu"]')
      .click();
    await page.waitForTimeout(100);
  }

  // 最終メモリ使用量の確認
  const finalMemory = await page.evaluate(
    () => performance.memory?.usedJSHeapSize || 0
  );

  // メモリリークがないことを確認(50%以内の増加)
  const memoryIncrease =
    (finalMemory - initialMemory) / initialMemory;
  expect(memoryIncrease).toBeLessThan(0.5);
});

CI/CD パイプラインでの自動実行

GitHub Actions を使用した CI/CD パイプラインでの自動テスト実行を設定します。

GitHub Actions 設定

yaml# .github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 18
          cache: 'yarn'

      - name: Install dependencies
        run: yarn install --frozen-lockfile

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

      - name: Build application
        run: yarn build

      - name: Run Playwright tests
        run: npx playwright test

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

並列実行の最適化

yaml# 並列実行による高速化
jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]
    steps:
      # ... 他のステップ
      - name: Run Playwright tests
        run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

テストデータ管理とモック戦略

効率的なテストデータ管理とモック戦略を実装します。

テストデータの管理

typescript// tests/fixtures/test-data.ts
export const testUsers = {
  admin: {
    email: 'admin@example.com',
    password: 'admin123',
    role: 'admin',
  },
  user: {
    email: 'user@example.com',
    password: 'user123',
    role: 'user',
  },
};

export const testProducts = [
  {
    id: 1,
    name: 'テスト商品1',
    price: 1000,
    category: 'electronics',
  },
  {
    id: 2,
    name: 'テスト商品2',
    price: 2000,
    category: 'clothing',
  },
];

// テストファイルでの使用
import {
  testUsers,
  testProducts,
} from '../fixtures/test-data';

test('商品一覧の表示', async ({ page }) => {
  // モックデータの設定
  await page.route('**/api/products', (route) => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify(testProducts),
    });
  });

  await page.goto('/products');

  // 商品が正しく表示されることを確認
  for (const product of testProducts) {
    await expect(
      page.locator(`text=${product.name}`)
    ).toBeVisible();
  }
});

API モックの実装

typescript// tests/mocks/api-mocks.ts
export async function mockApiResponses(page) {
  // ログインAPIのモック
  await page.route('**/api/auth/login', (route) => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        token: 'mock-jwt-token',
        user: { id: 1, email: 'test@example.com' },
      }),
    });
  });

  // 商品APIのモック
  await page.route('**/api/products', (route) => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: '商品1', price: 1000 },
        { id: 2, name: '商品2', price: 2000 },
      ]),
    });
  });
}

// テストファイルでの使用
test('ログインフロー', async ({ page }) => {
  await mockApiResponses(page);
  await page.goto('/login');

  await page.locator('#email').fill('test@example.com');
  await page.locator('#password').fill('password123');
  await page.locator('button[type="submit"]').click();

  // ログイン成功後のリダイレクト確認
  await expect(page).toHaveURL('/dashboard');
});

エラー処理とリトライ機能

実際のエラーケースに対応した堅牢なテストを実装します。

ネットワークエラーの処理

typescript// tests/error-handling.spec.ts
import { test, expect } from '@playwright/test';

test.describe('エラー処理', () => {
  test('ネットワークエラー時の表示', async ({ page }) => {
    // ネットワークエラーをシミュレート
    await page.route('**/api/products', (route) => {
      route.abort('failed');
    });

    await page.goto('/products');

    // エラーメッセージの表示確認
    const errorMessage = page.locator('.text-red-500');
    await expect(errorMessage).toBeVisible();
    await expect(errorMessage).toContainText(
      'データの読み込みに失敗しました'
    );

    // リトライボタンの確認
    const retryButton = page.locator(
      'button:has-text("再試行")'
    );
    await expect(retryButton).toBeVisible();
  });

  test('404エラーページの表示', async ({ page }) => {
    await page.goto('/non-existent-page');

    // 404ページの表示確認
    await expect(page.locator('h1')).toContainText('404');
    await expect(page.locator('a[href="/"]')).toBeVisible();
  });
});

リトライ機能の実装

typescript// playwright.config.ts でのリトライ設定
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    // アクションのリトライ設定
    actionTimeout: 10000,
    navigationTimeout: 30000,
  },
});

// カスタムリトライロジック
test('不安定な要素のテスト', async ({ page }) => {
  await page.goto('/dynamic-content');

  // 動的に読み込まれる要素の待機
  await expect(async () => {
    const element = page.locator('.dynamic-element');
    await expect(element).toBeVisible();
    await expect(element).toHaveText('読み込み完了');
  }).toPass({ timeout: 10000 });
});

エラーログの収集

typescripttest('エラーログの確認', async ({ page }) => {
  const errors = [];

  // コンソールエラーの監視
  page.on('console', (msg) => {
    if (msg.type() === 'error') {
      errors.push(msg.text());
    }
  });

  // ページエラーの監視
  page.on('pageerror', (error) => {
    errors.push(error.message);
  });

  await page.goto('/');

  // エラーが発生していないことを確認
  expect(errors).toHaveLength(0);
});

レポート生成と可視化

テスト結果を効果的に可視化する方法を紹介します。

HTML レポートの設定

typescript// playwright.config.ts
export default defineConfig({
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['json', { outputFile: 'test-results.json' }],
    ['junit', { outputFile: 'test-results.xml' }],
  ],
});

カスタムレポートの作成

typescript// tests/utils/custom-reporter.ts
import { Reporter } from '@playwright/test/reporter';

class CustomReporter implements Reporter {
  onBegin(config, suite) {
    console.log(
      `🚀 テスト実行開始: ${
        suite.allTests().length
      }個のテスト`
    );
  }

  onTestBegin(test) {
    console.log(`📝 テスト開始: ${test.title}`);
  }

  onTestEnd(test, result) {
    const status = result.status === 'passed' ? '✅' : '❌';
    console.log(
      `${status} テスト完了: ${test.title} (${result.duration}ms)`
    );
  }

  onEnd(result) {
    console.log(`🎉 全テスト完了: ${result.status}`);
    console.log(
      `📊 成功: ${result.passed}, 失敗: ${result.failed}`
    );
  }
}

export default CustomReporter;

スクリーンショットとビデオの活用

typescript// playwright.config.ts
export default defineConfig({
  use: {
    // 失敗時のスクリーンショット
    screenshot: 'only-on-failure',
    // 全テストのビデオ録画
    video: 'retain-on-failure',
    // トレースの記録
    trace: 'on-first-retry',
  },
});

// カスタムスクリーンショット
test('重要な画面のスクリーンショット', async ({ page }) => {
  await page.goto('/dashboard');

  // ダッシュボードのスクリーンショット
  await page.screenshot({
    path: 'screenshots/dashboard.png',
    fullPage: true,
  });

  // 特定要素のスクリーンショット
  const chart = page.locator('.chart-container');
  await chart.screenshot({
    path: 'screenshots/chart.png',
  });
});

まとめ

Tailwind CSS と Playwright の組み合わせは、現代の Web 開発における UI テスト自動化の最適解の一つです。この記事で紹介したベストプラクティスを実践することで、以下のような価値を得ることができます。

開発効率の向上

適切なセレクター設計とテスト戦略により、Tailwind CSS の頻繁なクラス変更にも対応できる堅牢なテストスイートを構築できます。これにより、開発速度を落とすことなく、品質を保証できるようになります。

品質保証の自動化

レスポンシブデザイン、ダークモード、アニメーションなど、従来手動で確認していた要素を自動化することで、人間のミスを排除し、一貫した品質を維持できます。

コスト削減とリスク軽減

自動化されたテストにより、リリース前の品質チェックが確実になり、本番環境での問題発生リスクを大幅に軽減できます。また、手動テストにかかっていた時間とコストを削減できます。

継続的改善の実現

CI/CD パイプラインとの統合により、コードの変更が即座にテストされ、問題を早期に発見できます。これにより、継続的な品質改善が実現できます。

Tailwind CSS と Playwright の組み合わせは、単なる技術的な相性の良さを超えた、開発体験の根本的な改善をもたらします。この記事で紹介した手法を参考に、あなたのプロジェクトでも効率的で確実な UI テスト自動化を実現してください。

関連リンク