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
- Puppeteer Official Site
- Puppeteer GitHub Repository
- Puppeteer API Documentation
- Chrome DevTools Protocol
- Puppeteer Examples
- Puppeteer Troubleshooting
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;