О нас Руководства Проекты Контакты
Админка
пожалуйста подождите

Puppeteer — это библиотека Node.js от Google для управления браузерами Chrome и Chromium в режиме headless. Она отлично подходит для end-to-end-тестирования, web scraping и автоматизации задач в браузере. В этом руководстве рассматривается реализация комплексного E2E-тестирования с Puppeteer с точки зрения senior-разработчика.

Почему Puppeteer

Puppeteer предлагает убедительные преимущества:

  1. Chrome DevTools Protocol: прямое управление браузером
  2. Headless/Headed: запуск незаметно или с наблюдением за выполнением тестов
  3. Screenshot/PDF: фиксация визуальных доказательств
  4. Network Interception: мокирование ответов API
  5. Performance Metrics: измерение реального пользовательского опыта

Установка

# Установить Puppeteer (скачивает Chromium)
npm install puppeteer
# Или puppeteer-core (используйте свой Chrome)
npm install puppeteer-core
# С Jest в качестве тестового фреймворка
npm install --save-dev jest puppeteer jest-puppeteer

Базовое использование

Простой скрипт

const puppeteer = require('puppeteer');
async function run() {
// Запустить браузер
const browser = await puppeteer.launch({
headless: 'new', // Использовать новый headless-режим
// headless: false, // Смотреть браузер во время тестирования
// slowMo: 100, // Замедлить действия для отладки
});
// Создать страницу
const page = await browser.newPage();
// Задать viewport
await page.setViewport({ width: 1280, height: 800 });
// Перейти на страницу
await page.goto('https://example.com', {
waitUntil: 'networkidle2' // Дождаться, пока сеть станет idle
});
// Получить заголовок страницы
const title = await page.title();
console.log('Page title:', title);
// Скриншот
await page.screenshot({ path: 'screenshot.png', fullPage: true });
// Закрыть браузер
await browser.close();
}
run();

Селекторы и взаимодействия

Поиск элементов

// CSS-селекторы
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();
// Дождаться селектора
await page.waitForSelector('.loaded-content');
await page.waitForSelector('.modal', { visible: true });
await page.waitForSelector('.spinner', { hidden: true });
// Проверить, существует ли элемент
const exists = await page.$('.optional-element') !== null;

Пользовательские взаимодействия

// Клик
await page.click('#submit-btn');
await page.click('a.nav-link', { clickCount: 2 }); // Двойной клик
// Ввод текста
await page.type('#email', '[email protected]');
await page.type('#password', 'secret123', { delay: 100 }); // Как у человека
// Очистить и ввести
await page.click('#search', { clickCount: 3 }); // Выделить всё
await page.type('#search', 'new search term');
// Или очистить программно
await page.$eval('#search', el => el.value = '');
await page.type('#search', 'new value');
// Клавиатура
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');
// Выбрать значение в dropdown
await page.select('#country', 'US');
await page.select('#colors', 'red', 'blue'); // Множественный выбор
// Checkbox/Radio
await page.click('#agree-terms');
// Или сначала проверить состояние
const isChecked = await page.$eval('#agree-terms', el => el.checked);
if (!isChecked) await page.click('#agree-terms');
// Загрузка файла
const fileInput = await page.$('input[type="file"]');
await fileInput.uploadFile('/path/to/file.pdf');
// Наведение
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();

Извлечение данных

// Получить текстовое содержимое
const heading = await page.$eval('h1', el => el.textContent);
// Получить атрибут
const href = await page.$eval('a.main-link', el => el.getAttribute('href'));
// Получить несколько элементов
const links = await page.$$eval('nav a', elements =>
elements.map(el => ({
text: el.textContent,
href: el.href
}))
);
// Выполнить в контексте страницы
const data = await page.evaluate(() => {
return {
url: window.location.href,
title: document.title,
itemCount: document.querySelectorAll('.item').length
};
});

Интеграция с Jest

Настройка

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'
};

Пример теста

login.e2e.js:

describe('Login Flow', () => {
beforeAll(async () => {
await page.goto('http://localhost:3000/login');
});
beforeEach(async () => {
// Очищать форму между тестами
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

Организуйте тесты для удобства сопровождения:

pages/LoginPage.js:

class LoginPage {
constructor(page) {
this.page = page;
this.url = 'http://localhost:3000/login';
// Селекторы
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

Мокирование ответов API

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();
}
});
// Переход после настройки interception
await page.goto('http://localhost:3000/users');

Блокировка ресурсов

await page.setRequestInterception(true);
page.on('request', request => {
const resourceType = request.resourceType();
// Блокировать изображения и шрифты для ускорения тестов
if (['image', 'font', 'stylesheet'].includes(resourceType)) {
request.abort();
} else {
request.continue();
}
});

Ожидание ответа API

// Дождаться конкретного запроса
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);

Скриншоты и визуальное тестирование

// Скриншот всей страницы
await page.screenshot({
path: 'screenshots/full-page.png',
fullPage: true
});
// Скриншот элемента
const element = await page.$('.hero-section');
await element.screenshot({ path: 'screenshots/hero.png' });
// Генерация PDF
await page.pdf({
path: 'output.pdf',
format: 'A4',
printBackground: true
});
// Visual regression с 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'
});
});

Аутентификация

Выполнить логин один раз

// 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();
// Сохранить cookies
const cookies = await page.cookies();
fs.writeFileSync('cookies.json', JSON.stringify(cookies));
await browser.close();
}
module.exports = globalSetup;
// В тестах
beforeAll(async () => {
const cookies = JSON.parse(fs.readFileSync('cookies.json'));
await page.setCookie(...cookies);
});

Советы по отладке

// Запуск с DevTools
const browser = await puppeteer.launch({
headless: false,
devtools: true
});
// Добавить оператор debugger в evaluate
await page.evaluate(() => {
debugger; // Пауза в DevTools
});
// Console logs со страницы
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
// Ошибки со страницы
page.on('pageerror', error => console.error('PAGE ERROR:', error));
// Сделать скриншот при падении
afterEach(async () => {
if (jasmine.currentTest.failedExpectations.length > 0) {
await page.screenshot({
path: `screenshots/failure-${Date.now()}.png`
});
}
});

Запуск тестов

{
"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"
}
}
# Запустить все E2E-тесты
npm run test:e2e
# Запустить конкретный файл теста
npm run test:e2e -- login.e2e.js
# Watch-режим для разработки
npm run test:e2e:watch
# Показать браузер (отладка)
npm run test:e2e:headed

Ключевые выводы

  1. Паттерн Page Object: держите селекторы и действия организованными
  2. Ожидайте корректно: используйте явные ожидания, а не произвольные таймауты
  3. Мокируйте API: более быстрые и надёжные тесты
  4. Скриншот при падении: легко отлаживать упавшие тесты
  5. Параллельное выполнение: Jest по умолчанию запускает файлы параллельно
  6. Чистое состояние: сбрасывайте состояние между тестами для изоляции

E2E-тесты с Puppeteer выявляют интеграционные проблемы, которые пропускают unit-тесты, — это критически важно для уверенности в пользовательской функциональности.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив