Testing Tool

Puppeteer

Overview

Puppeteer is a Node.js library developed by Google that provides browser automation and control for headless Chrome or Chromium, serving as a comprehensive E2E testing tool. By directly controlling browsers through the Chrome DevTools Protocol, it enables high-performance web automation, scraping, PDF rendering, performance measurement, and accessibility testing in JavaScript/TypeScript. With its simple API and lightweight design, it offers excellent developer experience and ease of learning, optimized for automation in CI/CD environments as a modern browser test automation solution. Supporting Chrome extension testing, WebDriver BiDi compatibility, and high-speed execution performance, it facilitates reliable End-to-End and integration testing for contemporary web application development.

Details

Puppeteer 2025 edition provides industry-leading performance and stability as Google's official implementation of the Chrome DevTools Protocol for browser automation. Direct control of headless Chrome achieves execution speeds significantly surpassing traditional WebDriver-based tools while minimizing memory usage. Complete support for Chrome extension development and testing enables detailed testing of Service Workers, Background Pages, and Popup Pages. WebDriver BiDi protocol support enables Firefox automation and true cross-browser testing capabilities. Comprehensive web automation features include page performance measurement, network interception, detailed request/response control, custom JavaScript execution, screenshot/PDF generation, and accessibility auditing. Optimized for CI/CD environments with Docker integration, parallel execution, and resource efficiency, it strongly supports continuous test automation for large-scale development teams.

Key Features

  • Chrome DevTools Protocol: High-speed and stable browser control through Google's official protocol
  • Headless & Full Display Support: Visualization during development and headless execution in CI/CD environments
  • Chrome Extension Testing: Complete support for Service Worker, Background Page, and Popup testing
  • WebDriver BiDi Support: Firefox automation and true cross-browser testing capabilities
  • Performance Measurement: Page load speed, metrics, and Core Web Vitals auditing
  • Lightweight High-Speed Execution: Minimal resource usage with industry-leading execution performance

Pros and Cons

Pros

  • High reliability and complete Chrome/Chromium compatibility through Google's official library
  • Fast test execution and stability through direct Chrome DevTools Protocol control
  • Simple and intuitive API with excellent developer experience and low learning curve
  • Flexible switching between headless and full display modes with debugging support features
  • Complete support for Chrome extension development and testing with detailed control capabilities
  • Firefox automation and cross-browser testing through WebDriver BiDi support
  • Comprehensive performance measurement, PDF generation, and accessibility auditing features
  • Lightweight design optimized for CI/CD environments with Docker integration support

Cons

  • Limited to JavaScript/TypeScript and Node.js environments with no support for other languages
  • Primary focus on Chromium-based browsers with no support for Safari, IE, or Edge (Legacy)
  • Limited cross-browser testing scope compared to Playwright or Selenium
  • Some constraints and configuration complexity for complex multi-window/multi-tab operations
  • No support for desktop application automation beyond web applications
  • Complexity of proxy settings and security policy compliance in enterprise environments

Reference Links

Code Examples

Installation and Setup

# Puppeteer installation (automatic browser binary download)
npm install puppeteer

# Puppeteer Core (without browser binary)
npm install puppeteer-core

# Specific browser version installation
npx @puppeteer/browsers install chrome@stable
npx @puppeteer/browsers install [email protected]
npx @puppeteer/browsers install firefox@latest

# System dependency installation (Ubuntu/Debian)
npx puppeteer browsers install chrome --install-deps

# Check and remove installed browsers
npx @puppeteer/browsers list
npx @puppeteer/browsers clear

Basic Browser Automation

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

(async () => {
  // Browser launch
  const browser = await puppeteer.launch({
    headless: false, // Full display mode (for debugging)
    slowMo: 50,     // Operation interval (milliseconds)
    devtools: true  // Auto-open DevTools
  });

  // Create new page
  const page = await browser.newPage();

  // Set viewport
  await page.setViewport({ width: 1280, height: 720 });

  // Navigate to page
  await page.goto('https://example.com', {
    waitUntil: 'networkidle2' // Network wait condition
  });

  // Get page title
  const title = await page.title();
  console.log('Page title:', title);

  // Check element existence and click
  const button = await page.$('#submit-button');
  if (button) {
    await button.click();
  }

  // Text input
  await page.type('#username', 'testuser');
  await page.type('#password', 'password123');

  // Select dropdown option
  await page.select('#country', 'JP');

  // Form submission
  await page.keyboard.press('Enter');

  // Wait for response
  await page.waitForSelector('.success-message', { timeout: 5000 });

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

E2E Testing with Assertions

// 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('Login functionality test', async () => {
    await page.goto('https://example.com/login');

    // Login form input
    await page.waitForSelector('#login-form');
    await page.type('#username', 'testuser');
    await page.type('#password', 'testpass');

    // Click login button
    await page.click('#login-button');

    // Check navigation to dashboard page
    await page.waitForNavigation({ waitUntil: 'networkidle0' });
    const url = page.url();
    expect(url).to.include('/dashboard');

    // Check user information display
    const welcomeMessage = await page.$eval('.welcome-message', el => el.textContent);
    expect(welcomeMessage).to.include('Welcome, testuser');
  });

  it('Product search and filtering', async () => {
    await page.goto('https://example.com/products');

    // Search field input
    await page.type('[data-testid="search-input"]', 'laptop');
    await page.keyboard.press('Enter');

    // Wait for search results
    await page.waitForSelector('.product-list');

    // Check product count
    const productCount = await page.$$eval('.product-item', items => items.length);
    expect(productCount).to.be.greaterThan(0);

    // Apply price filter
    await page.click('#price-filter-100-500');
    await page.waitForSelector('.loading-spinner', { hidden: true });

    // Check product prices after filtering
    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('Shopping cart functionality', async () => {
    await page.goto('https://example.com/products/laptop-001');

    // Product detail page operations
    await page.click('[data-testid="add-to-cart"]');
    
    // Check cart addition notification
    const notification = await page.waitForSelector('.cart-notification');
    const notificationText = await notification.evaluate(el => el.textContent);
    expect(notificationText).to.include('Added to cart');

    // Navigate to cart page
    await page.click('[data-testid="cart-link"]');
    await page.waitForSelector('.cart-items');

    // Check cart items
    const cartItems = await page.$$('.cart-item');
    expect(cartItems).to.have.length(1);

    // Check detailed product information
    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})?/);
  });
});

Network Interception and API Testing

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

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

  // Request interception setup
  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()
    });

    // Block specific requests
    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()
    });
  });

  // Request/Response mocking
  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 response verification
  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);

  // Network statistics output
  console.log('Total requests:', requestData.length);
  console.log('Failed responses:', responseData.filter(r => r.status >= 400).length);

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

Performance Measurement and Core Web Vitals

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

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

  // Start performance measurement
  await page.coverage.startJSCoverage();
  await page.coverage.startCSSCoverage();

  // Core Web Vitals measurement
  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' });

  // Get performance metrics
  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]))
  );

  // Calculate page load times
  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');

  // Check resource usage
  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 coverage measurement
  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();
})();

Screenshot and PDF Generation

// 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');

  // Full page screenshot
  await page.screenshot({
    path: 'screenshots/fullpage.png',
    fullPage: true
  });

  // Element-specific screenshot
  const element = await page.$('#chart-container');
  await element.screenshot({
    path: 'screenshots/chart.png'
  });

  // Responsive design testing
  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 generation (high quality settings)
  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

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

(async () => {
  const pathToExtension = path.join(process.cwd(), 'my-extension');
  
  // Launch browser with Chrome extension
  const browser = await puppeteer.launch({
    headless: false, // Extension testing usually runs in full display mode
    args: [
      `--disable-extensions-except=${pathToExtension}`,
      `--load-extension=${pathToExtension}`,
      '--no-sandbox',
      '--disable-setuid-sandbox'
    ]
  });

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

  const worker = await workerTarget.worker();

  // Test Background Script processing
  const result = await worker.evaluate(() => {
    // Use extension APIs
    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);

  // Test Popup page
  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 element operations
  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 integration test
  const page = await browser.newPage();
  await page.goto('https://example.com');

  // Wait for Content Script injection
  await page.waitForFunction(() => window.myExtensionLoaded === true);

  // Page operations via Content Script
  const injectedData = await page.evaluate(() => {
    return window.myExtensionData;
  });

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

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

CI/CD Integration and Docker Configuration

# .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
          # Install system dependencies
          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

# Install system dependencies
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/*

# Set working directory
WORKDIR /app

# Copy package files and install dependencies
COPY package*.json ./
RUN npm ci --only=production

# Copy application files
COPY . .

# Puppeteer configuration
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable

# Run tests
CMD ["npm", "run", "test:e2e"]
// docker-compose.test.js - Docker environment test configuration
import puppeteer from 'puppeteer';

const config = {
  // Puppeteer configuration for Docker environment
  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'
    ]
  },

  // Test target application URL
  baseUrl: process.env.TEST_URL || 'http://localhost:3000'
};

export default config;