T-CREATOR

Tailwind CSS と Playwright で E2E テストを効率化する実践例

Tailwind CSS と Playwright で E2E テストを効率化する実践例

現代の Web 開発において、UI の一貫性とユーザー体験の品質を保つことは、プロジェクトの成功を左右する重要な要素です。特に、Tailwind CSS のようなユーティリティファーストの CSS フレームワークと、Playwright のような強力な E2E テストツールを組み合わせることで、開発効率と品質の両方を大幅に向上させることができます。

この記事では、実際のプロジェクトで遭遇する課題とその解決策を通じて、Tailwind CSS と Playwright を活用した効率的な E2E テストの実装方法を詳しく解説します。読者の皆さんが明日から実践できる具体的なテクニックと、避けるべき落とし穴についても触れていきます。

Tailwind CSS と Playwright の基本理解

Tailwind CSS の特徴と利点

Tailwind CSS は、従来の CSS フレームワークとは一線を画すユーティリティファーストアプローチを採用しています。この特徴が E2E テストにおいて大きな利点をもたらします。

まず、Tailwind CSS の基本的な特徴を理解しましょう。

html<!-- 従来のCSSクラス設計 -->
<div class="user-profile-card">
  <h2 class="user-name">田中太郎</h2>
  <p class="user-description">フロントエンド開発者</p>
</div>

<!-- Tailwind CSSのユーティリティクラス -->
<div class="bg-white rounded-lg shadow-md p-6 max-w-sm">
  <h2 class="text-xl font-semibold text-gray-800 mb-2">
    田中太郎
  </h2>
  <p class="text-gray-600">フロントエンド開発者</p>
</div>

Tailwind CSS の最大の利点は、クラス名が具体的なスタイルを表現していることです。これにより、E2E テストで要素を特定する際に、より直感的で保守しやすいセレクタを作成できます。

Playwright の基本機能

Playwright は、Microsoft が開発した次世代の E2E テストツールです。その特徴的な機能が、Tailwind CSS との組み合わせで真価を発揮します。

基本的な Playwright のセットアップから始めましょう。

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

# ブラウザのインストール
npx playwright install

Playwright の設定ファイルを作成します。

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'] },
    },
  ],
});

両技術の組み合わせによる相乗効果

Tailwind CSS と Playwright を組み合わせることで、従来の E2E テストでは実現困難だった効率性と信頼性を同時に達成できます。

具体的な相乗効果を確認してみましょう。

typescript// 従来のセレクタ(脆弱で保守が困難)
await page.click('.user-profile-card .user-name');

// Tailwind CSSを活用したセレクタ(堅牢で保守しやすい)
await page.click('[class*="text-xl font-semibold"]');

この組み合わせにより、UI の変更に強いテストコードを書くことができ、開発速度とテストの信頼性を両立させることができます。

E2E テスト環境の構築

プロジェクトの初期設定

効率的な E2E テスト環境を構築するために、まずプロジェクトの基本構造を整えましょう。

Next.js プロジェクトを例に、Tailwind CSS と Playwright の統合環境を作成します。

bash# Next.jsプロジェクトの作成
yarn create next-app e2e-test-project --typescript --tailwind --eslint

# プロジェクトディレクトリに移動
cd e2e-test-project

# 必要な依存関係のインストール
yarn add -D @playwright/test

プロジェクトの構造を整理します。

typescript// package.jsonのscriptsセクション
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "test:e2e:debug": "playwright test --debug"
  }
}

Playwright のインストールと設定

Playwright の詳細な設定を行い、効率的なテスト実行環境を構築します。

typescript// tests/global-setup.ts
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  // テスト用のデータベースセットアップなど
  await page.goto(
    'http://localhost:3000/api/setup-test-data'
  );

  await browser.close();
}

export default globalSetup;

テストヘルパー関数を作成して、共通の操作を効率化します。

typescript// tests/helpers/test-utils.ts
import { Page, expect } from '@playwright/test';

export class TestUtils {
  constructor(private page: Page) {}

  // Tailwind CSSクラスを活用した要素の検証
  async expectElementWithClass(
    className: string,
    text?: string
  ) {
    const element = this.page.locator(
      `[class*="${className}"]`
    );

    if (text) {
      await expect(element).toContainText(text);
    } else {
      await expect(element).toBeVisible();
    }
  }

  // フォーム入力のヘルパー
  async fillFormByClass(className: string, value: string) {
    await this.page.fill(`[class*="${className}"]`, value);
  }

  // ボタンクリックのヘルパー
  async clickButtonByClass(className: string) {
    await this.page.click(`[class*="${className}"]`);
  }
}

Tailwind CSS との連携設定

Tailwind CSS の設定を最適化して、E2E テストでの要素特定を効率化します。

javascript// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      // テスト用のカスタムクラスを追加
      colors: {
        'test-primary': '#3B82F6',
        'test-success': '#10B981',
        'test-error': '#EF4444',
      },
    },
  },
  plugins: [],
  // テスト用のセーフリスト
  safelist: [
    'bg-test-primary',
    'bg-test-success',
    'bg-test-error',
    'text-test-primary',
    'text-test-success',
    'text-test-error',
  ],
};

Tailwind CSS クラスを活用したテスト戦略

CSS セレクタの最適化

Tailwind CSS のユーティリティクラスを活用することで、より堅牢で保守しやすいセレクタを作成できます。

従来のアプローチと比較してみましょう。

typescript// 従来のアプローチ(脆弱)
test('ユーザー名を表示する', async ({ page }) => {
  await page.goto('/profile');
  await expect(page.locator('.user-name')).toBeVisible();
});

// Tailwind CSSを活用したアプローチ(堅牢)
test('ユーザー名を表示する', async ({ page }) => {
  await page.goto('/profile');
  await expect(
    page.locator('[class*="text-xl font-semibold"]')
  ).toBeVisible();
});

より高度なセレクタ戦略を実装します。

typescript// tests/selectors/tailwind-selectors.ts
export class TailwindSelectors {
  // テキストサイズによる要素の特定
  static textSize(
    size: 'sm' | 'base' | 'lg' | 'xl' | '2xl'
  ) {
    return `[class*="text-${size}"]`;
  }

  // 色による要素の特定
  static textColor(color: string) {
    return `[class*="text-${color}"]`;
  }

  // 背景色による要素の特定
  static bgColor(color: string) {
    return `[class*="bg-${color}"]`;
  }

  // 複合条件での要素特定
  static buttonWithText(
    text: string,
    variant: 'primary' | 'secondary' = 'primary'
  ) {
    const baseClass =
      variant === 'primary' ? 'bg-blue-500' : 'bg-gray-500';
    return `[class*="${baseClass}"][class*="text-white"]:has-text("${text}")`;
  }
}

カスタムクラスの活用

テスト専用のカスタムクラスを作成することで、テストの可読性と保守性を向上させます。

typescript// tests/components/TestComponent.tsx
import React from 'react';

interface TestComponentProps {
  children: React.ReactNode;
  testId?: string;
  variant?: 'primary' | 'secondary';
}

export const TestComponent: React.FC<
  TestComponentProps
> = ({ children, testId, variant = 'primary' }) => {
  const baseClasses = 'px-4 py-2 rounded-lg font-medium';
  const variantClasses = {
    primary: 'bg-blue-500 text-white hover:bg-blue-600',
    secondary: 'bg-gray-500 text-white hover:bg-gray-600',
  };

  return (
    <button
      className={`${baseClasses} ${variantClasses[variant]} data-testid-${testId}`}
      data-testid={testId}
    >
      {children}
    </button>
  );
};

テストでの活用例を示します。

typescript// tests/e2e/custom-classes.spec.ts
import { test, expect } from '@playwright/test';

test('カスタムクラスを使用したテスト', async ({ page }) => {
  await page.goto('/test-page');

  // data-testid属性を使用した要素の特定
  await expect(
    page.locator('[data-testid="submit-button"]')
  ).toBeVisible();

  // Tailwind CSSクラスと組み合わせた検証
  await expect(
    page.locator(
      '[data-testid="submit-button"][class*="bg-blue-500"]'
    )
  ).toBeVisible();
});

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

Tailwind CSS のレスポンシブクラスを活用して、様々な画面サイズでのテストを効率的に実行します。

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

test.describe('レスポンシブデザインのテスト', () => {
  test('デスクトップ表示での要素確認', async ({ page }) => {
    await page.setViewportSize({
      width: 1280,
      height: 720,
    });
    await page.goto('/dashboard');

    // デスクトップ専用の要素を確認
    await expect(
      page.locator('[class*="lg:block"]')
    ).toBeVisible();
    await expect(
      page.locator('[class*="md:hidden"]')
    ).not.toBeVisible();
  });

  test('モバイル表示での要素確認', async ({ page }) => {
    await page.setViewportSize({ width: 375, height: 667 });
    await page.goto('/dashboard');

    // モバイル専用の要素を確認
    await expect(
      page.locator('[class*="md:hidden"]')
    ).toBeVisible();
    await expect(
      page.locator('[class*="lg:block"]')
    ).not.toBeVisible();
  });
});

効率的なテストケース設計

ページオブジェクトモデルの実装

ページオブジェクトモデル(POM)を活用して、テストコードの保守性と再利用性を向上させます。

typescript// tests/pages/BasePage.ts
import { Page, Locator } from '@playwright/test';

export abstract class BasePage {
  protected page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  // 共通のナビゲーション
  async goto(path: string) {
    await this.page.goto(path);
  }

  // 共通の待機処理
  async waitForPageLoad() {
    await this.page.waitForLoadState('networkidle');
  }

  // Tailwind CSSクラスを活用した要素の取得
  protected getElementByClass(className: string): Locator {
    return this.page.locator(`[class*="${className}"]`);
  }
}

具体的なページクラスの実装例を示します。

typescript// tests/pages/LoginPage.ts
import { Page, expect } from '@playwright/test';
import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
  constructor(page: Page) {
    super(page);
  }

  // セレクタの定義
  private get emailInput() {
    return this.getElementByClass('border-gray-300');
  }

  private get passwordInput() {
    return this.getElementByClass('border-gray-300').nth(1);
  }

  private get loginButton() {
    return this.getElementByClass('bg-blue-500');
  }

  private get errorMessage() {
    return this.getElementByClass('text-red-500');
  }

  // ページの操作メソッド
  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async expectErrorMessage(message: string) {
    await expect(this.errorMessage).toContainText(message);
  }

  async expectLoginSuccess() {
    await expect(this.page).toHaveURL(/.*dashboard/);
  }
}

再利用可能なテストコンポーネント

共通のテストロジックをコンポーネント化して、複数のテストで再利用できるようにします。

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

export class FormTester {
  constructor(private page: Page) {}

  // フォーム入力の共通処理
  async fillForm(fields: Record<string, string>) {
    for (const [fieldName, value] of Object.entries(
      fields
    )) {
      const input = this.page.locator(
        `[name="${fieldName}"]`
      );
      await input.fill(value);
    }
  }

  // バリデーションエラーの確認
  async expectValidationErrors(errors: string[]) {
    for (const error of errors) {
      await expect(
        this.page.locator('[class*="text-red-500"]')
      ).toContainText(error);
    }
  }

  // 成功メッセージの確認
  async expectSuccessMessage(message: string) {
    await expect(
      this.page.locator('[class*="text-green-500"]')
    ).toContainText(message);
  }
}

データ駆動テストの活用

テストデータを外部化して、同じテストロジックで複数のケースを効率的に実行します。

typescript// tests/data/login-test-data.ts
export const loginTestData = [
  {
    name: '有効な認証情報',
    email: 'test@example.com',
    password: 'password123',
    expectedResult: 'success',
    description:
      '正しいメールアドレスとパスワードでログインできる',
  },
  {
    name: '無効なメールアドレス',
    email: 'invalid-email',
    password: 'password123',
    expectedResult: 'error',
    expectedError: '有効なメールアドレスを入力してください',
    description: '無効なメールアドレスでエラーが表示される',
  },
  {
    name: '空のパスワード',
    email: 'test@example.com',
    password: '',
    expectedResult: 'error',
    expectedError: 'パスワードは必須です',
    description: 'パスワードが空の場合にエラーが表示される',
  },
];

データ駆動テストの実装例を示します。

typescript// tests/e2e/data-driven-login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { loginTestData } from '../data/login-test-data';

test.describe('データ駆動ログインテスト', () => {
  for (const testCase of loginTestData) {
    test(testCase.description, async ({ page }) => {
      const loginPage = new LoginPage(page);
      await loginPage.goto('/login');

      await loginPage.login(
        testCase.email,
        testCase.password
      );

      if (testCase.expectedResult === 'success') {
        await loginPage.expectLoginSuccess();
      } else {
        await loginPage.expectErrorMessage(
          testCase.expectedError!
        );
      }
    });
  }
});

実践的なテストシナリオ

フォーム入力のテスト

実際のプロジェクトでよく使用されるフォームテストの実装例を示します。

typescript// tests/e2e/form-input.spec.ts
import { test, expect } from '@playwright/test';

test.describe('フォーム入力テスト', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/contact');
  });

  test('必須フィールドのバリデーション', async ({
    page,
  }) => {
    // 空のフォームを送信
    await page.click('[class*="bg-blue-500"]');

    // エラーメッセージの確認
    await expect(
      page.locator('[class*="text-red-500"]')
    ).toContainText('名前は必須です');
    await expect(
      page.locator('[class*="text-red-500"]')
    ).toContainText('メールアドレスは必須です');
  });

  test('正常なフォーム送信', async ({ page }) => {
    // フォームの入力
    await page.fill('[name="name"]', '田中太郎');
    await page.fill('[name="email"]', 'tanaka@example.com');
    await page.fill(
      '[name="message"]',
      'お問い合わせ内容です'
    );

    // 送信ボタンのクリック
    await page.click('[class*="bg-blue-500"]');

    // 成功メッセージの確認
    await expect(
      page.locator('[class*="text-green-500"]')
    ).toContainText('送信完了しました');
  });
});

ナビゲーションのテスト

サイト内のナビゲーション機能をテストする実装例を示します。

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

test.describe('ナビゲーションテスト', () => {
  test('ヘッダーナビゲーション', async ({ page }) => {
    await page.goto('/');

    // ナビゲーションリンクの確認
    const navLinks = page.locator(
      '[class*="text-gray-600"]'
    );
    await expect(navLinks).toHaveCount(4);

    // 各リンクのクリックテスト
    await page.click('text=ホーム');
    await expect(page).toHaveURL('/');

    await page.click('text=サービス');
    await expect(page).toHaveURL('/services');

    await page.click('text=お問い合わせ');
    await expect(page).toHaveURL('/contact');
  });

  test('モバイルメニューの動作', async ({ page }) => {
    await page.setViewportSize({ width: 375, height: 667 });
    await page.goto('/');

    // ハンバーガーメニューのクリック
    await page.click('[class*="md:hidden"]');

    // モバイルメニューの表示確認
    await expect(
      page.locator('[class*="mobile-menu"]')
    ).toBeVisible();

    // メニュー項目のクリック
    await page.click('text=ホーム');
    await expect(
      page.locator('[class*="mobile-menu"]')
    ).not.toBeVisible();
  });
});

レスポンシブ動作のテスト

様々な画面サイズでの表示と動作を確認するテストを実装します。

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

test.describe('レスポンシブ動作テスト', () => {
  const viewports = [
    { name: 'mobile', width: 375, height: 667 },
    { name: 'tablet', width: 768, height: 1024 },
    { name: 'desktop', width: 1280, height: 720 },
  ];

  for (const viewport of viewports) {
    test(`${viewport.name}での表示確認`, async ({
      page,
    }) => {
      await page.setViewportSize(viewport);
      await page.goto('/dashboard');

      // 画面サイズに応じた要素の表示確認
      if (viewport.name === 'mobile') {
        await expect(
          page.locator('[class*="md:hidden"]')
        ).toBeVisible();
        await expect(
          page.locator('[class*="lg:block"]')
        ).not.toBeVisible();
      } else if (viewport.name === 'desktop') {
        await expect(
          page.locator('[class*="lg:block"]')
        ).toBeVisible();
        await expect(
          page.locator('[class*="md:hidden"]')
        ).not.toBeVisible();
      }
    });
  }
});

テスト実行の最適化

並列実行の設定

テストの実行時間を短縮するために、並列実行の設定を最適化します。

typescript// playwright.config.ts(並列実行の設定)
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  workers: process.env.CI ? 4 : undefined, // CI環境では4並列、ローカルでは自動
  retries: process.env.CI ? 2 : 0,
  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'] },
    },
  ],
  // テストの実行順序を制御
  globalSetup: require.resolve('./tests/global-setup.ts'),
  globalTeardown: require.resolve(
    './tests/global-teardown.ts'
  ),
});

テストデータの管理

テストデータを効率的に管理する仕組みを構築します。

typescript// tests/utils/test-data-manager.ts
import { Page } from '@playwright/test';

export class TestDataManager {
  constructor(private page: Page) {}

  // テスト用ユーザーの作成
  async createTestUser(userData: {
    email: string;
    password: string;
    name: string;
  }) {
    await this.page.goto('/api/test/create-user');
    await this.page.fill('[name="email"]', userData.email);
    await this.page.fill(
      '[name="password"]',
      userData.password
    );
    await this.page.fill('[name="name"]', userData.name);
    await this.page.click('[class*="bg-blue-500"]');
  }

  // テストデータのクリーンアップ
  async cleanupTestData() {
    await this.page.goto('/api/test/cleanup');
    await this.page.waitForResponse(
      (response) =>
        response.url().includes('/api/test/cleanup') &&
        response.status() === 200
    );
  }

  // テスト用のファイルアップロード
  async uploadTestFile(filePath: string) {
    const fileChooserPromise =
      this.page.waitForEvent('filechooser');
    await this.page.click('[class*="file-upload"]');
    const fileChooser = await fileChooserPromise;
    await fileChooser.setFiles(filePath);
  }
}

CI/CD パイプラインの構築

GitHub Actions を使用した CI/CD パイプラインの設定例を示します。

yaml# .github/workflows/e2e-tests.yml
name: E2E 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: Start application
        run: yarn start &

      - name: Wait for application
        run: npx wait-on http://localhost:3000

      - 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

デバッグとトラブルシューティング

テスト実行時の問題解決

実際のプロジェクトで遭遇する可能性のある問題とその解決策を示します。

よくある問題 1: 要素が見つからない

typescript// 問題のあるコード
test('要素のクリック', async ({ page }) => {
  await page.goto('/dashboard');
  await page.click('.non-existent-class'); // エラー: 要素が見つからない
});

// 解決策: 適切な待機処理の追加
test('要素のクリック(修正版)', async ({ page }) => {
  await page.goto('/dashboard');

  // 要素が表示されるまで待機
  await page.waitForSelector('[class*="bg-blue-500"]', {
    state: 'visible',
  });
  await page.click('[class*="bg-blue-500"]');
});

よくある問題 2: 非同期処理の待機

typescript// 問題のあるコード
test('フォーム送信', async ({ page }) => {
  await page.fill('[name="email"]', 'test@example.com');
  await page.click('[class*="bg-blue-500"]');
  await expect(
    page.locator('[class*="text-green-500"]')
  ).toBeVisible(); // エラー: まだ表示されていない
});

// 解決策: 適切な待機処理の追加
test('フォーム送信(修正版)', async ({ page }) => {
  await page.fill('[name="email"]', 'test@example.com');
  await page.click('[class*="bg-blue-500"]');

  // 成功メッセージが表示されるまで待機
  await page.waitForSelector('[class*="text-green-500"]', {
    state: 'visible',
  });
  await expect(
    page.locator('[class*="text-green-500"]')
  ).toBeVisible();
});

視覚的回帰テストの活用

Playwright の視覚的回帰テスト機能を活用して、UI の一貫性を保証します。

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

test.describe('視覚的回帰テスト', () => {
  test('ホームページのスクリーンショット比較', async ({
    page,
  }) => {
    await page.goto('/');

    // ページ全体のスクリーンショット
    await expect(page).toHaveScreenshot('homepage.png');
  });

  test('コンポーネントのスクリーンショット比較', async ({
    page,
  }) => {
    await page.goto('/components');

    // 特定のコンポーネントのスクリーンショット
    const component = page.locator(
      '[class*="bg-white rounded-lg"]'
    );
    await expect(component).toHaveScreenshot(
      'component-card.png'
    );
  });

  test('レスポンシブデザインの視覚的確認', async ({
    page,
  }) => {
    const viewports = [
      { name: 'mobile', width: 375, height: 667 },
      { name: 'tablet', width: 768, height: 1024 },
      { name: 'desktop', width: 1280, height: 720 },
    ];

    for (const viewport of viewports) {
      await page.setViewportSize(viewport);
      await page.goto('/');
      await expect(page).toHaveScreenshot(
        `homepage-${viewport.name}.png`
      );
    }
  });
});

パフォーマンス監視

テスト実行時のパフォーマンスを監視し、最適化の指標とします。

typescript// tests/performance/performance-monitoring.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('メモリ使用量の監視', async ({ page }) => {
    await page.goto('/');

    // メモリ使用量の取得
    const memoryInfo = await page.evaluate(() => {
      return (performance as any).memory;
    });

    // メモリ使用量が適切な範囲内であることを確認
    expect(memoryInfo.usedJSHeapSize).toBeLessThan(
      50 * 1024 * 1024
    ); // 50MB以下
  });

  test('ネットワークリクエストの最適化', async ({
    page,
  }) => {
    const requests: string[] = [];

    page.on('request', (request) => {
      requests.push(request.url());
    });

    await page.goto('/');

    // 不要なリクエストがないことを確認
    const unnecessaryRequests = requests.filter(
      (url) =>
        url.includes('analytics') ||
        url.includes('tracking')
    );

    expect(unnecessaryRequests.length).toBe(0);
  });
});

まとめ

この記事では、Tailwind CSS と Playwright を組み合わせた効率的な E2E テストの実装方法について詳しく解説しました。

重要なポイントを振り返ると:

  1. Tailwind CSS のユーティリティクラスを活用することで、より堅牢で保守しやすいセレクタを作成できる
  2. ページオブジェクトモデルを採用することで、テストコードの再利用性と保守性を大幅に向上できる
  3. データ駆動テストを活用することで、同じロジックで複数のテストケースを効率的に実行できる
  4. 並列実行と CI/CD パイプラインを構築することで、開発サイクル全体の効率化を実現できる

実践する際の心構え:

E2E テストは単なる品質保証のツールではなく、開発チーム全体の生産性向上とユーザー体験の向上に直結する重要な投資です。Tailwind CSS と Playwright の組み合わせにより、従来の E2E テストでは実現困難だった効率性と信頼性を同時に達成できます。

最初は小さなテストから始めて、徐々にスコープを広げていくことをお勧めします。完璧なテストを最初から目指すのではなく、継続的な改善を通じて最適なテスト戦略を見つけていくことが重要です。

読者の皆さんが、この記事で学んだ知識を実際のプロジェクトに適用し、より効率的で信頼性の高い E2E テストを実現されることを心から願っています。

関連リンク