テストツール

Puppeteer

概要

PuppeteerはGoogle社開発のNode.js向けライブラリで、ヘッドレスChromeやChromiumの制御とブラウザ自動化を提供するE2Eテストツールです。Chrome DevTools Protocolを介してブラウザを直接制御し、JavaScript/TypeScriptでの高性能なWeb自動化、スクレイピング、PDFレンダリング、パフォーマンス測定、アクセシビリティテストを実現します。シンプルなAPIと軽量な設計により、開発者にとって習得しやすく、CI/CD環境での自動化に最適化されたモダンなブラウザテスト自動化ソリューション。Chrome拡張機能テスト、WebDriver BiDi対応、高速実行性能により、現代のWebアプリケーション開発における信頼性の高いEnd-to-Endテストとインテグレーションテストを支援します。

詳細

Puppeteer 2025年版は、Googleが開発するChrome DevTools Protocolの公式実装として、ブラウザ自動化における最高水準の性能と安定性を提供します。ヘッドレスChromeの直接制御により、従来のWebDriverベースのツールを大幅に上回る実行速度を実現し、メモリ使用量も最小限に抑制。Chrome拡張機能の開発・テストに完全対応し、Service Worker、Background Page、Popup Pageの詳細なテストが可能です。WebDriver BiDi プロトコル対応により、Firefox自動化も標準サポートし、真のクロスブラウザテストを実現。ページパフォーマンス測定、ネットワークインターセプト、リクエスト・レスポンスの詳細制御、カスタムJavaScript実行、スクリーンショット・PDF生成、アクセシビリティ監査など、包括的なWeb自動化機能を統合。CI/CD環境での動作に最適化され、Docker統合、並列実行、リソース効率化により、大規模開発チームでの継続的テスト自動化を強力に支援します。

主な特徴

  • Chrome DevTools Protocol: Googleの公式プロトコルによる高速で安定したブラウザ制御
  • ヘッドレス・フル表示両対応: 開発時の可視化とCI/CD環境でのヘッドレス実行
  • Chrome拡張機能テスト: Service Worker、Background Page、Popup の完全サポート
  • WebDriver BiDi対応: Firefox自動化と真のクロスブラウザテスト
  • パフォーマンス測定: ページ読み込み速度、メトリクス、Core Web Vitals監査
  • 軽量高速実行: 最小限のリソース使用量と最高水準の実行性能

メリット・デメリット

メリット

  • Google公式ライブラリによる高い信頼性とChrome/Chromiumとの完全互換性
  • Chrome DevTools Protocolの直接制御による高速なテスト実行と安定性
  • シンプルで直感的なAPIと優れた開発者体験、学習コストの低さ
  • ヘッドレス・フル表示の柔軟な切り替えとデバッグ支援機能
  • Chrome拡張機能開発・テストの完全サポートと詳細制御
  • WebDriver BiDi対応によるFirefox自動化とクロスブラウザテスト
  • 包括的なパフォーマンス測定、PDF生成、アクセシビリティ監査機能
  • 軽量でCI/CD環境に最適化された設計とDocker統合サポート

デメリット

  • JavaScript/TypeScript・Node.js環境に限定され、他言語サポートなし
  • 主要対象がChromium系ブラウザで、Safari、IE、Edge(Legacy)は非対応
  • PlaywrightやSeleniumと比較してクロスブラウザテストの範囲が限定的
  • 複雑なマルチウィンドウ・マルチタブ操作での一部制約と設定複雑性
  • Webアプリケーション以外のデスクトップアプリケーション自動化は非対応
  • 企業環境でのプロキシ設定やセキュリティポリシー対応の複雑さ

参考ページ

書き方の例

インストールとセットアップ

# Puppeteerのインストール(ブラウザバイナリ自動ダウンロード)
npm install puppeteer

# Puppeteer Core(ブラウザバイナリなし)
npm install puppeteer-core

# 特定のブラウザバージョンのインストール
npx @puppeteer/browsers install chrome@stable
npx @puppeteer/browsers install [email protected]
npx @puppeteer/browsers install firefox@latest

# システム依存関係のインストール(Ubuntu/Debian)
npx puppeteer browsers install chrome --install-deps

# インストール済みブラウザの確認・削除
npx @puppeteer/browsers list
npx @puppeteer/browsers clear

基本的なブラウザ自動化

// basic-automation.js
import puppeteer from 'puppeteer';

(async () => {
  // ブラウザの起動
  const browser = await puppeteer.launch({
    headless: false, // フル表示モード(デバッグ用)
    slowMo: 50,     // 操作間隔(ミリ秒)
    devtools: true  // DevToolsの自動オープン
  });

  // 新しいページの作成
  const page = await browser.newPage();

  // ビューポートの設定
  await page.setViewport({ width: 1280, height: 720 });

  // ページへの移動
  await page.goto('https://example.com', {
    waitUntil: 'networkidle2' // ネットワーク待機
  });

  // ページタイトルの取得
  const title = await page.title();
  console.log('Page title:', title);

  // 要素の存在確認とクリック
  const button = await page.$('#submit-button');
  if (button) {
    await button.click();
  }

  // テキスト入力
  await page.type('#username', 'testuser');
  await page.type('#password', 'password123');

  // セレクターボックスの選択
  await page.select('#country', 'JP');

  // フォーム送信
  await page.keyboard.press('Enter');

  // レスポンスの待機
  await page.waitForSelector('.success-message', { timeout: 5000 });

  // ブラウザの終了
  await browser.close();
})();

E2Eテストとアサーション

// e2e-test.js
import puppeteer from 'puppeteer';
import { expect } from 'chai';

describe('E2E Testing with Puppeteer', () => {
  let browser;
  let page;

  before(async () => {
    browser = await puppeteer.launch({
      headless: true,
      args: ['--no-sandbox', '--disable-dev-shm-usage']
    });
  });

  beforeEach(async () => {
    page = await browser.newPage();
    await page.setViewport({ width: 1920, height: 1080 });
  });

  afterEach(async () => {
    await page.close();
  });

  after(async () => {
    await browser.close();
  });

  it('ログイン機能のテスト', async () => {
    await page.goto('https://example.com/login');

    // ログインフォームの入力
    await page.waitForSelector('#login-form');
    await page.type('#username', 'testuser');
    await page.type('#password', 'testpass');

    // ログインボタンのクリック
    await page.click('#login-button');

    // ダッシュボードページへの遷移確認
    await page.waitForNavigation({ waitUntil: 'networkidle0' });
    const url = page.url();
    expect(url).to.include('/dashboard');

    // ユーザー情報の表示確認
    const welcomeMessage = await page.$eval('.welcome-message', el => el.textContent);
    expect(welcomeMessage).to.include('Welcome, testuser');
  });

  it('商品検索とフィルタリング', async () => {
    await page.goto('https://example.com/products');

    // 検索フィールドへの入力
    await page.type('[data-testid="search-input"]', 'laptop');
    await page.keyboard.press('Enter');

    // 検索結果の待機
    await page.waitForSelector('.product-list');

    // 商品数の確認
    const productCount = await page.$$eval('.product-item', items => items.length);
    expect(productCount).to.be.greaterThan(0);

    // 価格フィルターの適用
    await page.click('#price-filter-100-500');
    await page.waitForSelector('.loading-spinner', { hidden: true });

    // フィルタ後の商品価格確認
    const prices = await page.$$eval('.product-price', 
      elements => elements.map(el => parseFloat(el.textContent.replace('$', '')))
    );
    
    prices.forEach(price => {
      expect(price).to.be.within(100, 500);
    });
  });

  it('ショッピングカート機能', async () => {
    await page.goto('https://example.com/products/laptop-001');

    // 商品詳細ページでの操作
    await page.click('[data-testid="add-to-cart"]');
    
    // カート追加通知の確認
    const notification = await page.waitForSelector('.cart-notification');
    const notificationText = await notification.evaluate(el => el.textContent);
    expect(notificationText).to.include('カートに追加されました');

    // カートページへの移動
    await page.click('[data-testid="cart-link"]');
    await page.waitForSelector('.cart-items');

    // カート内商品の確認
    const cartItems = await page.$$('.cart-item');
    expect(cartItems).to.have.length(1);

    // 商品情報の詳細確認
    const itemName = await page.$eval('.cart-item-name', el => el.textContent);
    const itemPrice = await page.$eval('.cart-item-price', el => el.textContent);
    
    expect(itemName).to.include('Laptop');
    expect(itemPrice).to.match(/\$\d+(\.\d{2})?/);
  });
});

ネットワークインターセプトとAPIテスト

// network-intercept.js
import puppeteer from 'puppeteer';

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  // リクエストのインターセプト設定
  await page.setRequestInterception(true);

  const requestData = [];
  const responseData = [];

  page.on('request', (request) => {
    console.log('Request:', request.method(), request.url());
    requestData.push({
      method: request.method(),
      url: request.url(),
      headers: request.headers(),
      postData: request.postData()
    });

    // 特定のリクエストのブロック
    if (request.url().includes('analytics.google.com')) {
      request.abort();
    } else {
      request.continue();
    }
  });

  page.on('response', (response) => {
    console.log('Response:', response.status(), response.url());
    responseData.push({
      status: response.status(),
      url: response.url(),
      headers: response.headers()
    });
  });

  // リクエスト・レスポンスのモック
  await page.setRequestInterception(true);
  page.on('request', (request) => {
    if (request.url() === 'https://api.example.com/users') {
      request.respond({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: 1, name: 'Test User 1', email: '[email protected]' },
          { id: 2, name: 'Test User 2', email: '[email protected]' }
        ])
      });
    } else {
      request.continue();
    }
  });

  await page.goto('https://example.com/users');

  // API レスポンスの検証
  await page.waitForSelector('.user-list');
  const users = await page.$$eval('.user-item', items => 
    items.map(item => ({
      name: item.querySelector('.user-name').textContent,
      email: item.querySelector('.user-email').textContent
    }))
  );

  console.log('Users loaded:', users);

  // ネットワーク統計の出力
  console.log('Total requests:', requestData.length);
  console.log('Failed responses:', responseData.filter(r => r.status >= 400).length);

  await browser.close();
})();

パフォーマンス測定とCore Web Vitals

// performance-metrics.js
import puppeteer from 'puppeteer';

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  // パフォーマンス測定の開始
  await page.coverage.startJSCoverage();
  await page.coverage.startCSSCoverage();

  // Core Web Vitals の測定
  await page.evaluateOnNewDocument(() => {
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'largest-contentful-paint') {
          console.log('LCP:', entry.startTime);
        }
      }
    }).observe({ type: 'largest-contentful-paint', buffered: true });

    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.name === 'first-contentful-paint') {
          console.log('FCP:', entry.startTime);
        }
      }
    }).observe({ type: 'paint', buffered: true });
  });

  await page.goto('https://example.com', { waitUntil: 'networkidle0' });

  // パフォーマンスメトリクスの取得
  const performanceTiming = JSON.parse(
    await page.evaluate(() => JSON.stringify(window.performance.timing))
  );

  const navigationTiming = JSON.parse(
    await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType('navigation')[0]))
  );

  // ページ読み込み時間の計算
  const loadTime = performanceTiming.loadEventEnd - performanceTiming.navigationStart;
  const domContentLoaded = performanceTiming.domContentLoadedEventEnd - performanceTiming.navigationStart;
  const firstByte = performanceTiming.responseStart - performanceTiming.navigationStart;

  console.log('Performance Metrics:');
  console.log('Page Load Time:', loadTime + 'ms');
  console.log('DOM Content Loaded:', domContentLoaded + 'ms');
  console.log('Time to First Byte:', firstByte + 'ms');

  // リソース使用量の確認
  const resourceTiming = await page.evaluate(() => {
    return window.performance.getEntriesByType('resource').map(entry => ({
      name: entry.name,
      size: entry.transferSize || 0,
      duration: entry.duration
    }));
  });

  const totalSize = resourceTiming.reduce((sum, resource) => sum + resource.size, 0);
  console.log('Total Resources Size:', (totalSize / 1024 / 1024).toFixed(2) + 'MB');

  // JavaScript・CSS カバレッジの測定
  const [jsCoverage, cssCoverage] = await Promise.all([
    page.coverage.stopJSCoverage(),
    page.coverage.stopCSSCoverage()
  ]);

  let totalBytes = 0;
  let usedBytes = 0;

  [...jsCoverage, ...cssCoverage].forEach(entry => {
    totalBytes += entry.text.length;
    entry.ranges.forEach(range => {
      usedBytes += range.end - range.start - 1;
    });
  });

  console.log('Code Coverage:');
  console.log('Used:', (usedBytes / totalBytes * 100).toFixed(2) + '%');
  console.log('Unused:', ((totalBytes - usedBytes) / totalBytes * 100).toFixed(2) + '%');

  await browser.close();
})();

スクリーンショットとPDF生成

// screenshot-pdf.js
import puppeteer from 'puppeteer';
import path from 'path';

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.goto('https://example.com/report');

  // 全画面スクリーンショット
  await page.screenshot({
    path: 'screenshots/fullpage.png',
    fullPage: true
  });

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

  // レスポンシブデザインのテスト
  const devices = [
    { name: 'mobile', width: 375, height: 667 },
    { name: 'tablet', width: 768, height: 1024 },
    { name: 'desktop', width: 1920, height: 1080 }
  ];

  for (const device of devices) {
    await page.setViewport({ width: device.width, height: device.height });
    await page.screenshot({
      path: `screenshots/${device.name}.png`,
      fullPage: true
    });
  }

  // PDF生成(高品質設定)
  await page.pdf({
    path: 'reports/document.pdf',
    format: 'A4',
    printBackground: true,
    margin: {
      top: '20mm',
      right: '20mm',
      bottom: '20mm',
      left: '20mm'
    },
    headerTemplate: '<div style="font-size:10px;margin:auto">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
    footerTemplate: '<div style="font-size:10px;margin:auto">Generated on ' + new Date().toISOString() + '</div>',
    displayHeaderFooter: true
  });

  await browser.close();
})();

Chrome拡張機能のテスト

// extension-testing.js
import puppeteer from 'puppeteer';
import path from 'path';

(async () => {
  const pathToExtension = path.join(process.cwd(), 'my-extension');
  
  // Chrome拡張機能付きでブラウザ起動
  const browser = await puppeteer.launch({
    headless: false, // 拡張機能テストは通常フル表示で実行
    args: [
      `--disable-extensions-except=${pathToExtension}`,
      `--load-extension=${pathToExtension}`,
      '--no-sandbox',
      '--disable-setuid-sandbox'
    ]
  });

  // Manifest V3 Service Worker のテスト
  const workerTarget = await browser.waitForTarget(
    target => target.type() === 'service_worker' && 
              target.url().endsWith('background.js')
  );

  const worker = await workerTarget.worker();

  // Background Scriptでの処理テスト
  const result = await worker.evaluate(() => {
    // 拡張機能のAPIを使用
    return new Promise((resolve) => {
      chrome.storage.local.set({ testKey: 'testValue' }, () => {
        chrome.storage.local.get(['testKey'], (data) => {
          resolve(data.testKey);
        });
      });
    });
  });

  console.log('Storage test result:', result);

  // Popup ページのテスト
  await worker.evaluate('chrome.action.openPopup();');

  const popupTarget = await browser.waitForTarget(
    target => target.type() === 'page' && 
              target.url().endsWith('popup.html')
  );

  const popupPage = await popupTarget.asPage();

  // Popup内の要素操作
  await popupPage.waitForSelector('#popup-button');
  await popupPage.click('#popup-button');

  const popupText = await popupPage.$eval('#popup-text', el => el.textContent);
  console.log('Popup content:', popupText);

  // Content Script との連携テスト
  const page = await browser.newPage();
  await page.goto('https://example.com');

  // Content Script が注入されるまで待機
  await page.waitForFunction(() => window.myExtensionLoaded === true);

  // Content Script 経由でのページ操作
  const injectedData = await page.evaluate(() => {
    return window.myExtensionData;
  });

  console.log('Content script data:', injectedData);

  await browser.close();
})();

CI/CD統合とDocker設定

# .github/workflows/puppeteer-tests.yml
name: Puppeteer E2E Tests

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

jobs:
  e2e-tests:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18, 20]

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install dependencies
        run: |
          npm ci
          # システム依存関係のインストール
          sudo apt-get update
          sudo apt-get install -y \
            libnss3-dev \
            libatk-bridge2.0-dev \
            libdrm2 \
            libxcomposite1 \
            libxdamage1 \
            libxrandr2 \
            libgbm1 \
            libxss1 \
            libasound2

      - name: Install browsers
        run: npx puppeteer browsers install chrome --install-deps

      - name: Start application
        run: |
          npm start &
          npx wait-on http://localhost:3000

      - name: Run Puppeteer tests
        run: npm run test:e2e
        env:
          CI: true
          PUPPETEER_CACHE_DIR: ~/.cache/puppeteer

      - name: Upload test artifacts
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: test-results-node-${{ matrix.node-version }}
          path: |
            screenshots/
            test-results/
            coverage/

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          directory: ./coverage
# Dockerfile.puppeteer
FROM node:18-slim

# システム依存関係のインストール
RUN apt-get update \
    && apt-get install -y wget gnupg \
    && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
      --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

# 作業ディレクトリの設定
WORKDIR /app

# パッケージファイルのコピーと依存関係インストール
COPY package*.json ./
RUN npm ci --only=production

# アプリケーションファイルのコピー
COPY . .

# Puppeteerの設定
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable

# テスト実行
CMD ["npm", "run", "test:e2e"]
// docker-compose.test.js - Docker環境でのテスト設定
import puppeteer from 'puppeteer';

const config = {
  // Docker環境での Puppeteer 設定
  launch: {
    headless: true,
    executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || null,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-dev-shm-usage',
      '--disable-accelerated-2d-canvas',
      '--no-first-run',
      '--no-zygote',
      '--single-process',
      '--disable-gpu'
    ]
  },

  // テスト対象アプリケーションのURL
  baseUrl: process.env.TEST_URL || 'http://localhost:3000'
};

export default config;