About us Guides Projects Contacts
Админка
please wait

Puppeteer is Google's Node.js library for controlling headless Chrome and Chromium browsers. It excels at end-to-end testing, web scraping, and automated browser tasks. This guide covers implementing comprehensive E2E testing with Puppeteer from a senior developer's perspective.

Why Puppeteer

Puppeteer offers compelling advantages:

  1. Chrome DevTools Protocol: Direct browser control
  2. Headless/Headed: Run invisibly or watch tests execute
  3. Screenshot/PDF: Capture visual evidence
  4. Network Interception: Mock API responses
  5. Performance Metrics: Measure real user experience

Installation

# Install Puppeteer (downloads Chromium).
npm install puppeteer
# Or puppeteer-core (bring your own Chrome).
npm install puppeteer-core
# With Jest as the test framework.
npm install --save-dev jest puppeteer jest-puppeteer

Basic Usage

Simple Script

const puppeteer = require('puppeteer');
async function run() {
// Launch browser.
const browser = await puppeteer.launch({
headless: 'new', // Use new headless mode.
// headless: false, // See the browser while testing
// slowMo: 100, // Slow down actions for debugging
});
// Create page.
const page = await browser.newPage();
// Set viewport.
await page.setViewport({ width: 1280, height: 800 });
// Navigate.
await page.goto('https://example.com', {
waitUntil: 'networkidle2' // Wait for the network to be idle.
});
// Get page title.
const title = await page.title();
console.log('Page title:', title);
// Screenshot.
await page.screenshot({ path: 'screenshot.png', fullPage: true });
// Close browser.
await browser.close();
}
run();

Selectors and Interactions

Finding Elements

// CSS selectors.
await page.click('button.submit');
await page.click('#login-form input[name="email"]');
await page.click('[data-testid="submit-button"]');
// XPath.
const [element] = await page.$x('//button[contains(text(), "Submit")]');
await element.click();
// Wait for selector.
await page.waitForSelector('.loaded-content');
await page.waitForSelector('.modal', { visible: true });
await page.waitForSelector('.spinner', { hidden: true });
// Check if the element exists.
const exists = await page.$('.optional-element') !== null;

User Interactions

// Clicking.
await page.click('#submit-btn');
await page.click('a.nav-link', { clickCount: 2 }); // Double-click.
// Typing.
await page.type('#email', '[email protected]');
await page.type('#password', 'secret123', { delay: 100 }); // Human-like.
// Clear and type.
await page.click('#search', { clickCount: 3 }); // Select all.
await page.type('#search', 'new search term');
// Or clear programmatically.
await page.$eval('#search', el => el.value = '');
await page.type('#search', 'new value');
// Keyboard.
await page.keyboard.press('Enter');
await page.keyboard.press('Escape');
await page.keyboard.down('Shift');
await page.keyboard.press('Tab');
await page.keyboard.up('Shift');
// Select dropdown.
await page.select('#country', 'US');
await page.select('#colors', 'red', 'blue'); // Multi-select.
// Checkbox/radio.
await page.click('#agree-terms');
// Or check state first.
const isChecked = await page.$eval('#agree-terms', el => el.checked);
if (!isChecked) await page.click('#agree-terms');
// File upload.
const fileInput = await page.$('input[type="file"]');
await fileInput.uploadFile('/path/to/file.pdf');
// Hover.
await page.hover('.dropdown-trigger');
// Drag and drop.
await page.mouse.move(100, 100);
await page.mouse.down();
await page.mouse.move(200, 200);
await page.mouse.up();

Extracting Data

// Get text content.
const heading = await page.$eval('h1', el => el.textContent);
// Get attribute.
const href = await page.$eval('a.main-link', el => el.getAttribute('href'));
// Get multiple elements.
const links = await page.$$eval('nav a', elements =>
elements.map(el => ({
text: el.textContent,
href: el.href
}))
);
// Evaluate in page context.
const data = await page.evaluate(() => {
return {
url: window.location.href,
title: document.title,
itemCount: document.querySelectorAll('.item').length
};
});

Jest Integration

Setup

jest.config.js:

module.exports = {
preset: 'jest-puppeteer',
testMatch: ['**/*.e2e.js'],
testTimeout: 30000,
};

jest-puppeteer.config.js:

module.exports = {
launch: {
headless: 'new',
slowMo: process.env.SLOWMO ? 100 : 0,
args: ['--no-sandbox', '--disable-setuid-sandbox']
},
browserContext: 'default'
};

Test Example

login.e2e.js:

describe('Login Flow', () => {
beforeAll(async () => {
await page.goto('http://localhost:3000/login');
});
beforeEach(async () => {
// Clear form between tests.
await page.reload();
});
it('should display login form', async () => {
await expect(page).toMatchElement('form#login');
await expect(page).toMatchElement('input[name="email"]');
await expect(page).toMatchElement('input[name="password"]');
await expect(page).toMatchElement('button[type="submit"]');
});
it('should show error for invalid credentials', async () => {
await page.type('input[name="email"]', '[email protected]');
await page.type('input[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
await page.waitForSelector('.error-message');
const errorText = await page.$eval('.error-message', el => el.textContent);
expect(errorText).toContain('Invalid credentials');
});
it('should redirect on successful login', async () => {
await page.type('input[name="email"]', '[email protected]');
await page.type('input[name="password"]', 'correctpassword');
await page.click('button[type="submit"]');
await page.waitForNavigation();
expect(page.url()).toBe('http://localhost:3000/dashboard');
});
it('should validate email format', async () => {
await page.type('input[name="email"]', 'not-an-email');
await page.click('button[type="submit"]');
const validationMessage = await page.$eval(
'input[name="email"]',
el => el.validationMessage
);
expect(validationMessage).toBeTruthy();
});
});

Page Object Pattern

Organize tests for maintainability:

pages/LoginPage.js:

class LoginPage {
constructor(page) {
this.page = page;
this.url = 'http://localhost:3000/login';
// Selectors.
this.emailInput = 'input[name="email"]';
this.passwordInput = 'input[name="password"]';
this.submitButton = 'button[type="submit"]';
this.errorMessage = '.error-message';
this.successMessage = '.success-message';
}
async navigate() {
await this.page.goto(this.url);
await this.page.waitForSelector(this.emailInput);
}
async login(email, password) {
await this.page.type(this.emailInput, email);
await this.page.type(this.passwordInput, password);
await this.page.click(this.submitButton);
}
async getErrorMessage() {
await this.page.waitForSelector(this.errorMessage);
return this.page.$eval(this.errorMessage, el => el.textContent);
}
async waitForRedirect(expectedUrl) {
await this.page.waitForNavigation();
return this.page.url() === expectedUrl;
}
}
module.exports = LoginPage;

login.e2e.js:

const LoginPage = require('./pages/LoginPage');
describe('Login', () => {
let loginPage;
beforeAll(async () => {
loginPage = new LoginPage(page);
});
beforeEach(async () => {
await loginPage.navigate();
});
it('should login successfully', async () => {
await loginPage.login('[email protected]', 'password123');
const redirected = await loginPage.waitForRedirect('http://localhost:3000/dashboard');
expect(redirected).toBe(true);
});
});

Network Interception

Mock API Responses

await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().includes('/api/users')) {
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Test User' },
{ id: 2, name: 'Another User' }
])
});
} else {
request.continue();
}
});
// Navigate after setting up interception.
await page.goto('http://localhost:3000/users');

Block Resources

await page.setRequestInterception(true);
page.on('request', request => {
const resourceType = request.resourceType();
// Block images and fonts for faster tests.
if (['image', 'font', 'stylesheet'].includes(resourceType)) {
request.abort();
} else {
request.continue();
}
});

Wait for API Response

// Wait for a specific request.
const [response] = await Promise.all([
page.waitForResponse(response =>
response.url().includes('/api/submit') && response.status() === 200
),
page.click('#submit-btn')
]);
const data = await response.json();
expect(data.success).toBe(true);

Screenshots and Visual Testing

// Full-page screenshot.
await page.screenshot({
path: 'screenshots/full-page.png',
fullPage: true
});
// Element screenshot.
const element = await page.$('.hero-section');
await element.screenshot({ path: 'screenshots/hero.png' });
// PDF generation.
await page.pdf({
path: 'output.pdf',
format: 'A4',
printBackground: true
});
// Visual regression with jest-image-snapshot.
const { toMatchImageSnapshot } = require('jest-image-snapshot');
expect.extend({ toMatchImageSnapshot });
it('should match visual snapshot', async () => {
const screenshot = await page.screenshot();
expect(screenshot).toMatchImageSnapshot({
failureThreshold: 0.01,
failureThresholdType: 'percent'
});
});

Authentication

Handle Login Once

// auth.setup.js.
const fs = require('fs');
async function globalSetup() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000/login');
await page.type('#email', '[email protected]');
await page.type('#password', 'password');
await page.click('#submit');
await page.waitForNavigation();
// Save cookies.
const cookies = await page.cookies();
fs.writeFileSync('cookies.json', JSON.stringify(cookies));
await browser.close();
}
module.exports = globalSetup;
// In tests.
beforeAll(async () => {
const cookies = JSON.parse(fs.readFileSync('cookies.json'));
await page.setCookie(...cookies);
});

Debugging Tips

// Launch with DevTools.
const browser = await puppeteer.launch({
headless: false,
devtools: true
});
// Add a debugger statement in evaluate.
await page.evaluate(() => {
debugger; // Pause in DevTools.
});
// Console logs from page.
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
// Errors from page.
page.on('pageerror', error => console.error('PAGE ERROR:', error));
// Take a screenshot on failure.
afterEach(async () => {
if (jasmine.currentTest.failedExpectations.length > 0) {
await page.screenshot({
path: `screenshots/failure-${Date.now()}.png`
});
}
});

Running Tests

{
"scripts": {
"test:e2e": "jest --config jest.e2e.config.js",
"test:e2e:watch": "jest --config jest.e2e.config.js --watch",
"test:e2e:headed": "HEADLESS=false jest --config jest.e2e.config.js"
}
}
# Run all E2E tests.
npm run test:e2e
# Run a specific test file.
npm run test:e2e -- login.e2e.js
# Watch mode for development.
npm run test:e2e:watch
# See browser (debugging).
npm run test:e2e:headed

Key Takeaways

  1. Page Object Pattern: Keep selectors and actions organized
  2. Wait properly: Use explicit waits, not arbitrary timeouts
  3. Mock APIs: Faster, more reliable tests
  4. Screenshot on failure: Debug failed tests easily
  5. Parallel execution: Jest runs files in parallel by default
  6. Clean state: Reset between tests for isolation

E2E tests with Puppeteer catch integration issues that unit tests miss—essential for confidence in user-facing functionality.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive