Puppeteer — большой гайд

Что такое Puppeteer

Статистика

Проект команды Google Chrome, Node-библиотека для управления Chrome и любым другим браузером, поддерживающим протокол Chrome DevTools (то есть практически все современные браузеры). В Puppeteer автоматизируются частые действия пользователя, есть неплохое API. Простой полезный инструмент автоматизации, QA-тестирования, и парсинга веб-страниц в обычном или headless-режиме.

Puppeteer
Что такое Puppeteer — суть в одном рисунке

«Кукловодом» сейчас пользуются треть опрошенных тестировщиков:

Популярность Puppeteer
Популярность Puppeteer

Довольны около 80%:

Puppeteer и тестировщики
Puppeteer и тестировщики

Что касается показателей более конкретных, чем «нравится», а именно количества npm-загрузок, то здесь безраздельно властвуют Cypress — и да, Puppeteer:

Фреймворки тестирования сравнение
Фреймворки тестирования — сравнение

Звезды на Гитхабе:

Puppeteer звезды на Гитхабе
Puppeteer — звезды на Гитхабе

Сравнение Selenium, Cypress, Puppeteer и Playwright

Сравнение по:SeleniumCypressPuppeteerPlaywright
Кроссбраузерное тестированиеПоддерживает все браузерыТолько Chrome и FirefoxТолько Chrome и FirefoxChrome, Firefox, Safari
Множественные вкладки и фреймыЧерез API, проблемноПлохоИнтуитивное APIИнтуитивное API
Параллельный запуск, Grid, InfraПоддерживаетТолько в платной версии, в облакеНужно создавать и конфигурироватьНужно создавать и конфигурировать
СкоростьПриличнаяБыстроБыстроНормально
Дебаг автотестовЗависит от grid-провайдераЧерез DOMЧерез IDEЧерез IDE
Документация и FAQОбширнейшаяОтличноЕсть документация и туториалыНе очень, к тому же часто меняется API
КомьюнитиОгромноеНебольшоеНебольшоеНебольшое
Сравнение Selenium, Cypress, Puppeteer и Playwright
Тестовые фреймворки - загрузки
Тестовые фреймворки — загрузки

Puppeteer и Selenium

Чем отличаетсяPuppeteerSelenium
Нужны ли скиллы программирования (то есть знание ЯП на высоком уровне)Да, нужныТребования ниже, чем с Puppeteer — ЯП нужны для работы с WebDriver, но не очень нужны для Selenium IDE
Какие поддерживает языкиТолько JSPython, Java, Node.js, C#
Какие поддерживает браузерыChrome и (частично) FirefoxChrome, Firefox, Safari, IE, Opera
КомьюнитиНе очень, на фоне Selenium. GitHub, StackOverflowШирочайшее
СкоростьОчень высокая в ChromeМожет быть медленнее чем Puppeteer
Сложность установки и настройкиОчень легко, одной командойДовольно сложно, особенно нубам
Кроссплатформенное тестированиеНет и не скоро будетДа, отлично
ЗаписьНетДа, в Selenium IDE
Снятие скриншотовРисунки и PDFкиТолько рисунки
Что с мобильным тестированиемНет, только вебДа, в связке с Appium
Сравнение Puppeteer и Selenium

Что лучше, Playwright или Puppeteer?

Playwright появился позже чем Puppeteer, в некотором смысле является его «расширением» (что упрощает миграцию) с более продуманными функциями.

PuppeteerPlaywright
Создан и принадлежитGoogleMicrosoft
Поддерживает браузерыChromium (включая Edge) и Firefox (экспериментальная функция)Chromium (Chrome, Edge, Opera), Firefox and WebKit (Safari)
Поддерживает языкиJavaScript, TypeScriptJavaScript, TypeScript, Python, .NET, C#, Java
Поддерживается кемChrome DevTools TeamMicrosoft
Открытый кодДа Да 
Рантайм-окружение Node.jsNode.js, Python, Java, C#
Кроссплатформенность Windows, Linux, MacWindows, Linux, Mac
Фреймворки Mocha и JestMocha, Jest, Jasmine, AVA
Use-кейсыВеб-тестирование и автоматизацияВеб-тестирование и автоматизация
Появился когда20172020
Сколько звезд на GitHub80К+43К+

Чем Playwright лучше:

  • Поддерживает больше браузеров (особенно их свежие версии)
  • Больше языков программирования
  • Крупнее комьюнити
  • Разработчики быстрее реагируют на замечания

Что лучше в проекте, зависит от его приоритетов:

  • Скорость — в целом оба фреймворка достаточно хороши, тут нет однозначного решения.
  • Надежность — Playwright вероятно лучше. Ранее писали о проблемах в некоторых версиях браузеров Firefox и WebKit, но их достаточно быстро устраняли. 
  • Функциональность — тут нужно помнить, что команда Playwright во всем учитывала опыт Puppeteer, особенно что касается API и пр. Например, в Playwright page.click уже по дефолту ожидает видимость и доступность DOM-элемента; в Puppeteer это было реализовано сложнее и появилось позже. Playwright умеет автоматизировать повторяемый код (ожидание появления кнопок например) и лучше возможности это настроить; также в Playwright лучше функции выбора элементов; и в Playwright есть генератор кода.
  • Языки. Тут все просто — если нужен Python, Java, C#, то Puppeteer не вариант.
  • Браузеры — тут Playwright тоже выигрывает. Тесты пишутся для всех браузеров, не нужна конфигурация отдельно. Если же нужно просто пройти браузерные тесты и сделать это быстро и выкатить продукт, то Puppeteer годится, так как Chrome в любом случае «основной браузер» по всему миру. 

Что умеет Puppeteer

  • Автоматизация стандартных пользовательских действий в интерфейсе, включая ввод с клавиатуры (и мыши), заполнение и отправка форм, и подобные операции в QA; в общем, 90% того что нужно веб-тестировщикам
  • Делает скриншоты страниц и сохраняет страницы в PDF-ки
  • Парсит одностраничные веб-приложения
  • Создает удобное (и всегда свежее) тестовое окружение для веба, в последней версии Chrome, с последними JS-фичами
  • Записывает временнУю шкалу (таймлайн профилировщика) контроля производительности сайта
  • Можно тестировать Chrome-расширения 
  • А также существует Puppeteer для Firefox, и ожидается для Safari.

Архитектура Puppeteer

Архитектура Puppeteer
Архитектура Puppeteer

На рисунке:

  • Puppeteer работает с браузером через DevTools-протокол
  • Browser открывает несколько контекстов
  • BrowserContext — сессия; в сессии открыта страница Page, или несколько
  • В Page есть хотя бы один главный фрейм Frame, доолнительные создаются тегами iframe/frame
  • В Фрейме дефолтный контекст выполнения ExecutionContext, в нем выполняется JS-код
  • Worker’ы для запуска скрипта в фоновом потоке, отделенном от главного, в котором выполняются операции с UI.
  • SharedWorkers/ServiceWorkers/ExecutionContext(extensions) — функции (еще/уже) не реализованы.

Очевидные плюсы

Кроме упомянутых выше,

  • Быстрая установка и настройка
  • Архитектура построена «на событиях» (event-driven architecture), что делает тесты стабильными и более надежными насчет утечек памяти
  • Headless — дефолтный режим работы, а это значит скорость
  • Контексты браузера дают возможность параллельного запуска тестов и в нескольких вкладках
  • Запускается в Docker-контейнере или в serverless-окружении

О недостатках

  • Поддерживает только JavaScript
  • Кроссбраузерное тестирование будет нормально реализовано еще не скоро
  • Не поддерживает HLS (Live-стриминг по HTTP) 
  • И не поддерживает медиаформаты AAC и H.264

Оружие хакеров

Кроме добросовестных тестировщиков и скрейперов, инструментом очень широко пользуются злоумышленники — форки Puppeteer Extra Stealth и Puppeteer Stealth Fork позволяют обходить капчу (защиту от накруток). 

Практикум

Установка

Ставим один из пакетов Puppeteer.

Library-пакет

Маленький пакет puppeteer-core, библиотека работы с любым браузером с поддержкой DevTools без отдельной установки Chromium. Удобно, например если в проекте все взаимодействия с браузером удаленные. Чтобы установить, пишем:

npm install puppeteer-core

Product-пакет

Основной пакет puppeteer для автоматизации браузера, поверх puppeteer-core. После установки последняя версия Chromium ставится внутри node_modules, что гарантирует совместимость загруженной версии с операционной системой. Для установки пишем:

npm install puppeteer

Взаимодействие с браузером

Итак, Puppeteer- это, грубо говоря, API поверх протокола Chrome DevTools, поэтому он должен запускать экземпляр Chromium для взаимодействия с ним; экосистема Puppeteer обладает методами запуска нового экземпляра Chromium, или подключения к существующему.

Запускаем Chromium

Проще запустить экземпляр Chromium через Puppeteer:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  console.info(browser);
  await browser.close();
})();

Метод launch инициализирует экземпляр и подключает Puppeteer к нему. Следует учесть, что этот метод асинхронный (как и большинство методов Puppeteer), то есть он возвращает Promise. После этого получаем экземпляр браузера, представляющий инициализированный экземпляр.

Подключение к Chromium

Иногда нужно работать с существующим экземпляром Chromium, или через puppeteer-core, или подключаясь к remote-экземпляру:

const chromeLauncher = require('chrome-launcher');
const axios = require('axios');
const puppeteer = require('puppeteer');

(async () => {
  // Initializing a Chrome instance manually
  const chrome = await chromeLauncher.launch({
    chromeFlags: ['--headless']
  });
  const response = await axios.get(`http://localhost:${chrome.port}/json/version`);
  const { webSocketDebuggerUrl } = response.data;

  // Connecting the instance using `browserWSEndpoint`
  const browser = await puppeteer.connect({ browserWSEndpoint: webSocketDebuggerUrl });
  console.info(browser);

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

Используем chrome-launcher для запуска экземпляра Chrome вручную. Затем просто подтягиваем webSocketDebuggerUrl-значение созданного экземпляра.

Метод connect прикрепляет созданный экземпляр к Puppeteer. Все что надо дальше сделать — предоставить WebSocket-эндпойнт нашему экземпляру.

Примечание: Chrome-instance только для демонстрации создания экземпляра; можно подключаться к экземпляру другими путями, имея соответствующий WebSocket-эндпойнт.

Запуск Firefox

Раньше можно было просто:

npm install puppeteer-firefox

Puppeteer, а именно пакет puppeteer-firefox умеет работать с (одним из форков) Firefox в экспериментальном режиме, но к сожалению, этот проект официально командой уже не поддерживается. Остался такой путь: поставить переменную окружения PUPPETEER_PRODUCT в значение “firefox”:

PUPPETEER_PRODUCT=firefox npm install puppeteer

Или можно через BrowserFetcher, подтянуть бинарник Firefox Nightly во время инсталляции.

Итак, у нас есть бинарник, теперь ставим product в значение “firefox”, а остальные строки оставляем:

// Deprecated package
// const puppeteer = require('puppeteer-firefox');
const puppeteer = require('puppeteer');

(async () => {
  // FireFox's binary is needed to be fetched before
  const browser = await puppeteer.launch({ product: 'firefox' });
  console.info(browser);
  await browser.close();
})();

Следует иметь в виду, что интеграция API не полностью имплементирована. Можно проверять статус имплементации здесь.

Browser Context

Вместо повторного создания экземпляра браузера (инстанса) каждый раз, что накладно, можно вызывать тот же экземпляр в отдельных сессиях, принадлежащих общему браузеру. Такие сессии в Puppeteer называются контекстами браузера, подробнее о них здесь.

Дефолтный контекст браузера создается при создании экземпляра браузера, а можем создать еще дополнительные контексты:

const puppeteer = require('puppeteer');

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

  // A reference for the default browser context
  const defaultContext = browser.defaultBrowserContext();
  console.info(defaultContext.isIncognito()); // False

  // Creates a new browser context
  const newContext = await browser.createIncognitoBrowserContext();
  console.info(newContext.isIncognito()); // True

  // Closes the created browser context
  await newContext.close();

  // Closes the browser with the default context
  await browser.close();
})();

Итак, работаем с контекстами, при этом помним, что единственный путь закрыть дефолтный контекст — закрыть экземпляр браузера, тем самым закрывая все контексты, принадлежащие этому браузеру.

Контекст браузера удобен тем, что можно создать отдельные тестовые конфигурации для каждой сессии, например с дополнительными правами.

Headful-режим

Противоположность Headless-режима в котором не используется командная строка. Headful-режим открывает браузер с GUI-интерфейсом:

const puppeteer = require('puppeteer');

(async () => {
  // Makes the browser to be launched in a headful way
  const browser = await puppeteer.launch({ headless: false });
  console.info(browser);
  await browser.close();
})();

По умолчанию в Puppeteer браузер запускается в headless-режиме.

Headless-режим применяется в окружениях, которым сейчас не нужен интерфейс, или не имеют интерфейса. В Puppeteer доступны почти все QA-задачи в headless-режиме. В следующих примерах браузер будет запускаться в основном в headful-режиме, так более наглядные результаты.

Дебаг

При написании кода следует учитывать дальнейший дебаг автотестов. В документации Puppeteer есть советы по дебагу. Важные нюансы приведены ниже.

  1. Проверка браузера

Может быть нужно проверить, как скрипты работают с браузером, и что он отображает. В headful-режиме это делается примерно так:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false, slowMo: 200 });
  
  // Browser operations
  
  await browser.close();
})();

Можно еще раз проверить инструкции браузеру, благодаря функции slowMo, которая «замедляет» Puppeteer.

  1. Дебаг кода приложения в браузере

Если нужен дебаг приложения в открытом браузере, то открываем DevTools и запускаем дебаг обычным способом:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ devtools: true });

  // Browser operations

  // Holds the browser until we terminate the process explicitly
  await browser.waitForTarget(() => false);
  
  await browser.close();
})();

Мы работаем с DevTools, который запускает браузер в дефолтном headful-режиме, автоматически открывая DevTools. Кроме этого применяем waitForTarget (не закрывать процесс браузера, пока не будет прямой команды закрытия).

Можно «усыпить» браузер на время:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ devtools: true });

  // Browser operations

  // Option 1 - resolving a promise when `setTimeout` finishes
  const sleep = duration => new Promise(resolve => setTimeout(resolve, duration));
  await sleep(3000);

  // Option 2 - if we have a page instance, just using `waitFor`
  await page.waitFor(3000);

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

Первый способ — просто функция, возвращающая promise при окончании тайм-аута setTimeout.

Второй способ намного проще, но требует наличия экземпляра страницы (об этом позже).

  1. Дебаг процесса, запустившего Puppeteer

Мы знаем, что Puppeteer выполняется в процессе Node.js, отдельно от процесса с браузером. Поэтому работаем с Puppeteer, как с обычным Node.js-приложением.

Независимо, подключаемся ли к inspector client или через ndb, ставим брейкпойнты перед запуском Puppeteer. 

Можно также делать это программно, вставляя debugger; .

Взаимодействие Puppeteer со страницей

Теперь Puppeteer «прикреплен» к экземпляру браузера Chromium. Например, создаются страницы:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  
  // Creates a new page on the default browser context
  const page = await browser.newPage();
  console.info(page);
  
  await browser.close();
})();

В примере выше мы просто создаем новую страницу методом newPage. Она создается в дефолтном контексте браузера.

Обычно Page — это класс, представляющий одну вкладку в браузере (или extension background). Этот класс дает удобные методы и события взаимодействия со страницей — выбор элементов, запрос каких-то данных, ожидания, и т.п.

Далее посмотрим практические примеры. Будем «скрейпить» данные с официального сайта Puppeteer.

Навигация по URL

Самый простой пример — переход с пустой страницы к указанному URL:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  // Instructs the blank page to navigate a URL
  await page.goto('https://pptr.dev');
  
  // Fetches page's title
  const title = await page.title();
  console.info(`The title is: ${title}`);

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

Через goto перешли c созданной пустой страницы на сайт Puppeteer. Дальше мы взяли тайтл из главного фрейма страницы, распечатали его:

Puppeteer-fail

Но тайтла нет.

На этом примере видим, что нет гарантии, что страница отрендерит нужный элемент в нужное время. Может быть потому что страница загрузилась медленно, или часть ее с багами, или на странице есть перенаправление.

Поэтому в Puppeteer предусмотрены wait-методы ожидания: элементов, навигации, функций, запросов, ответов, или просто предикаты для асинхронных операций.

Как видим, на официальном сайте Puppeteer есть «проходная» страница, из которой идет перенаправление на главную. На проходной странице нет мета-элемента title:

Puppeteer console

При заходе на сайт title-элемент обрабатывается как пустая строка. Однако затем идет переход на главную и там рендерится уже с тайтлом.

Это значит, что метод title вызывается слишком рано, на проходной странице, а не на главной. Проходная страница обработана в Puppeteer как первый главный фрейм, и передан тайтл, а он пустая строка. Это можно пофиксить так:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  await page.goto('https://pptr.dev');

  // Waits until the `title` meta element is rendered
  await page.waitForSelector('title');
  
  const title = await page.title();
  console.info(`The title is: ${title}`);

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

Что мы сделали: указали Puppeteer ждать загрузки мета-элемента title, вызвав waitForSelector — этот метод ожидает появления выбранного элемента на странице.

Таким образом можно отрабатывать асинхронный рендеринг.

Эмуляция девайсов

Библиотека Puppeteer обладает инструментами отображения страницы на разных девайсах, что полезно при тестировании скорости отображения сайта на девайсе.

Попробуем эмулировать мобильный девайс; перейдем на официальный сайт Puppeteer:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  // Emulates an iPhone X
  await page.setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1');
  await page.setViewport({ width: 375, height: 812 });

  await page.goto('https://pptr.dev');

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

Здесь мы эмулировали iPhone Х, изменив user agent; далее изменили viewport-пропорции в соответствии с экраном (здесь справочник по экранам).

setUserAgent задает user agent для страницы, a setViewport задает viewport. В случае нескольких страниц каждая из них будет иметь свой user agent и viewport. 

Результат выполнения кода:

Puppeteer

В консоли видим, что страница открылась с нужным user agent и viewport size.

Нам не нужно всякий раз прописывать характеристики iPhone Х, потому что в библиотеке есть встроенный список девайсов с характеристиками. А также у нас есть метод emulate — вызывающий setUserAgent и затем setViewport.

Пробуем:

const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  await page.emulate(devices['iPhone X']);
  await page.goto('https://pptr.dev');

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

Выше передан boilerplate-дескриптор в метод emulate (а не объявлен эксплицитно). При этом импортированы дескрипторы из puppeteer/DeviceDescriptors.

Обработка событий

Класс Page поддерживает обработку событий через расширение объекта EventEmitter. То есть мы работаем через нативные методы (их список) on, once, removeListener.

Список поддерживаемых событий:

const puppeteer = require('puppeteer');

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

  // Emitted when the DOM is parsed and ready (without waiting for resources)
  page.once('domcontentloaded', () => console.info('✅ DOM is ready'));

  // Emitted when the page is fully loaded
  page.once('load', () => console.info('✅ Page is loaded'));

  // Emitted when the page attaches a frame
  page.on('frameattached', () => console.info('✅ Frame is attached'));

  // Emitted when a frame within the page is navigated to a new URL
  page.on('framenavigated', () => console.info('? Frame is navigated'));

  // Emitted when a script within the page uses `console.timeStamp`
  page.on('metrics', data => console.info(`? Timestamp added at ${data.metrics.Timestamp}`));

  // Emitted when a script within the page uses `console`
  page.on('console', message => console[message.type()](`? ${message.text()}`));

  // Emitted when the page emits an error event (for example, the page crashes)
  page.on('error', error => console.error(`❌ ${error}`));

  // Emitted when a script within the page has uncaught exception
  page.on('pageerror', error => console.error(`❌ ${error}`));

  // Emitted when a script within the page uses `alert`, `prompt`, `confirm` or `beforeunload`
  page.on('dialog', async dialog => {
    console.info(`? ${dialog.message()}`);
    await dialog.dismiss();
  });

  // Emitted when a new page, that belongs to the browser context, is opened
  page.on('popup', () => console.info('? New page is opened'));

  // Emitted when the page produces a request
  page.on('request', request => console.info(`? Request: ${request.url()}`));

  // Emitted when a request, which is produced by the page, fails
  page.on('requestfailed', request => console.info(`❌ Failed request: ${request.url()}`));

  // Emitted when a request, which is produced by the page, finishes successfully
  page.on('requestfinished', request => console.info(`? Finished request: ${request.url()}`));

  // Emitted when a response is received
  page.on('response', response => console.info(`? Response: ${response.url()}`));

  // Emitted when the page creates a dedicated WebWorker
  page.on('workercreated', worker => console.info(`? Worker: ${worker.url()}`));

  // Emitted when the page destroys a dedicated WebWorker
  page.on('workerdestroyed', worker => console.info(`? Destroyed worker: ${worker.url()}`));

  // Emitted when the page detaches a frame
  page.on('framedetached', () => console.info('✅ Frame is detached'));

  // Emitted after the page is closed
  page.once('close', () => console.info('✅ Page is closed'));

  await page.goto('https://pptr.dev');
  
  await browser.close();
})();

Видим, что поддерживаются все нужные в QA события: загрузка, фреймы, метрики, консоль, ошибки, запросы и ответы.

Попробуем на практике, такой скрипт:

// Triggers `metrics` event
await page.evaluate(() => console.timeStamp());

// Triggers `console` event
await page.evaluate(() => console.info('A console message within the page'));

// Triggers `dialog` event
await page.evaluate(() => alert('An alert within the page'));

// Triggers `error` event
page.emit('error', new Error('An error within the page'));

// Triggers `close` event
await page.close();

Метод evaluate просто выполняет скрипт в контексте страницы. В выводе — события:

Puppeteer Bash

Можно слушать кастомные события на странице, которые активируются по триггеру. То есть можно описать обработчик событий в окне страницы через метод exposeFunction. Пример по ссылке.

Операции с мышью

Это управление указателем по двум осям внутри viewport. Puppeteer работает с мышью через класс Mouse. А также каждый экземпляр Page может вызывать Mouse, что позволяет менять положение указателя мыши и кликать в окне.

Для начала пробуем менять положение указателя:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  await page.setViewport({ width: 1920, height: 1080 });
  await page.goto('https://pptr.dev');
  
  // Waits until the API sidebar is rendered
  await page.waitForSelector('sidebar-component');

  // Hovers the second link inside the API sidebar
  await page.mouse.move(40, 150);

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

Эмулируем сценарий наведения мыши на вторую ссылку в левой панели на сайте Puppeteer. Задаем размер viewport и время ожидания загрузки компонента. Далее вызываем move, который позиционирует указатель в нужном месте, по центру второй ссылки. На экране видим:

Puppeteer results

Все ок, указатель наведен на вторую ссылку. Далее эмулируем нажатие на ссылку в нужном месте:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  await page.setViewport({ width: 1920, height: 1080 });
  await page.goto('https://pptr.dev');
  await page.waitForSelector('sidebar-component');

  // Clicks the second link and triggers `mouseup` event after 1000ms
  await page.mouse.click(40, 150, { delay: 1000 });

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

Чтобы не указывать координаты явно (эксплицитно), вызовем click, который триггерит последовательные события: mousemove, mousedown и mouseup.

Примечание: мы откладываем нажатие, чтобы продемонстрировать, как изменить поведение при клике. Можно эмулировать кнопки мыши (включая центральную), и двойной клик.

В Puppeteer можно симулировать перетаскивание мышкой (drag-n-drop):

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  await page.setViewport({ width: 1920, height: 1080 });
  await page.goto('https://pptr.dev');
  await page.waitForSelector('sidebar-component');

  // Drags the mouse from a point
  await page.mouse.move(0, 0);
  await page.mouse.down();
  
  // Drops the mouse to another point
  await page.mouse.move(100, 100);
  await page.mouse.up();

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

Все это мы делали методами из Mouse, который перехватывает управление мышкой, последовательно выполняет операции, и освобождает мышку.

Операции с клавиатурой

Нужны в QA для эмуляции ввода данных. Puppeteer умеет работать с клавиатурой через класс Keyboard в каждом экземпляре Page

Пробуем вводить текст в поиске:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  await page.setViewport({ width: 1920, height: 1080 });
  await page.goto('https://pptr.dev');
  
  // Waits until the toolbar is rendered
  await page.waitForSelector('toolbar-component');

  // Focuses the search input
  await page.focus('[type="search"]');

  // Types the text into the focused element
  await page.keyboard.type('Keyboard', { delay: 100 });

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

Здесь мы ожидаем загрузку панели инструментов (а не боковой панели как в предыдущем примере). Далее переводим фокус в поле поиска, и вводим текст.

Кроме ввода, можно триггерить события клавиатуры:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  await page.setViewport({ width: 1920, height: 1080 });
  await page.goto('https://pptr.dev');
  await page.waitForSelector('toolbar-component');

  await page.focus('[type="search"]');
  await page.keyboard.type('Keyboard', { delay: 100 });

  // Choosing the third result
  await page.keyboard.press('ArrowDown', { delay: 200 });
  await page.keyboard.press('ArrowDown', { delay: 200 });
  await page.keyboard.press('Enter');

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

Здесь мы дважды нажали кнопку Вниз (ArrowDown) и далее Enter, чтобы выбрать третий элемент в списке:

Puppeteer send Keys

Список кодов клавиш.

Скриншоты

Есть специальный метод для скриншотов:

const puppeteer = require('puppeteer');

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

  await page.setViewport({ width: 1920, height: 1080 });
  await page.goto('https://pptr.dev');
  await page.waitForSelector('title');

  // Takes a screenshot of the whole viewport
  await page.screenshot({ path: 'screenshot.png' });

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

В методе screenshot нужно лишь прописать путь для сохранения скринов. Можно менять тип, качество, и даже обрезать скриншоты:

const puppeteer = require('puppeteer');

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

  await page.setViewport({ width: 1920, height: 1080 });
  await page.goto('https://pptr.dev');
  await page.waitForSelector('title');

  // Takes a screenshot of an area within the page
  await page.screenshot({
    path: 'screenshot.jpg',
    type: 'jpeg',
    quality: 80,
    clip: { x: 220, y: 0, width: 630, height: 360 }
  });

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

Что увидим:

Puppeteer скриншот

Генерация PDF

Контент на странице конвертируется в pdf-ку:

const puppeteer = require('puppeteer');

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

  // Navigates to the project README file
  await page.goto('https://github.com/GoogleChrome/puppeteer/blob/master/README.md');

  // Generates a PDF from the page content
  await page.pdf({ path: 'overview.pdf' });

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

Метод pdf генерит файл:

Puppeteer PDF

Изменение геолокации

Многие веб-сайты меняют выдачу в зависимости от геолокации пользователя. Поменять геолокацию страницы:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ devtools: true });
  const page = await browser.newPage();

  // Grants permission for changing geolocation
  const context = browser.defaultBrowserContext();
  await context.overridePermissions('https://pptr.dev', ['geolocation']);

  await page.goto('https://pptr.dev');
  await page.waitForSelector('title');

  // Changes to the north pole's location
  await page.setGeolocation({ latitude: 90, longitude: 0 });

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

Сначала даем контексту браузера разрешение на доступ к геолокации. Далее вызываем setGeolocation и переопределяем текущую геолокацию координатами Северного Полюса.

На выходе (через navigator) имеем:

Доступность

Дерево доступности — это часть DOM, включающая элементы с релевантной информацией по вспомогательным технологиям повышения доступности, типа скринридеров, чтения вслух, экранной лупы и так далее. Можем проанализировать (и протестировать) доступность контента на странице.

Puppeteer фиксирует состояние дерева доступности:

const puppeteer = require('puppeteer');

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

  await page.goto('https://pptr.dev');
  await page.waitForSelector('title');

  // Captures the current state of the accessibility tree
  const snapshot = await page.accessibility.snapshot();
  console.info(snapshot);

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

Здесь, может, не все нужные элементы, но самые часто применяемые. Полное дерево можно посмотреть, поставив interestingOnly в положение false.

Покрытие кода

Эта функция введена в Chrome начиная с v59. Измерение, сколько кода было задействовано, сравнивая с загруженным; так можно убрать мертвый код, уменьшая время загрузки страницы. Это делается программно:

const puppeteer = require('puppeteer');

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

  // Starts to gather coverage information for JS and CSS files
  await Promise.all([page.coverage.startJSCoverage(), page.coverage.startCSSCoverage()]);

  await page.goto('https://pptr.dev');
  await page.waitForSelector('title');

  // Stops the coverage gathering
  const [jsCoverage, cssCoverage] = await Promise.all([
    page.coverage.stopJSCoverage(),
    page.coverage.stopCSSCoverage()
  ]);

  // Calculates how many bytes are being used based on the coverage
  const calculateUsedBytes = (type, coverage) =>
    coverage.map(({ url, ranges, text }) => {
      let usedBytes = 0;

      ranges.forEach(range => (usedBytes += range.end - range.start - 1));

      return {
        url,
        type,
        usedBytes,
        totalBytes: text.length
      };
    });

  console.info([
    ...calculateUsedBytes('js', jsCoverage),
    ...calculateUsedBytes('css', cssCoverage)
  ]);

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

Puppeteer собирает данные по покрытию файлов JavaScript и CSS до завершения загрузки страницы. Указываем calculateUsedBytes, который проходит по собранным данным по покрытию и вычисляет, сколько байтов задействовано. Далее просто вызываем созданную функцию по обеим покрытиям.

[
   {
      url: 'https://pptr.dev/',
      type: 'js',
      usedBytes: 149,
      totalBytes: 150
   },
   {
      url: 'https://www.googletagmanager.com/gtag/js?id=UA-106086244-2',
      type: 'js',
      usedBytes: 21018,
      totalBytes: 66959
   },
   {
      url: 'https://pptr.dev/index.js',
      type: 'js',
      usedBytes: 108922,
      totalBytes: 141703
   },
   {
      url: 'https://www.google-analytics.com/analytics.js',
      type: 'js',
      usedBytes: 19665,
      totalBytes: 44287
   },
   {
      url: 'https://pptr.dev/style.css',
      type: 'css',
      usedBytes: 5135,
      totalBytes: 14326
   }
]

В выводе видим usedBytes и totalBytes по каждому файлу.

Производительность

В Puppeteer развитая система оценки производительности страницы с метриками.

  1. Анализ времени загрузки страницы

Navigation Timing — Web API, дающее метрики навигации на странице и события загрузки, доступные через window.performance.

const puppeteer = require('puppeteer');

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

  await page.goto('https://pptr.dev');
  await page.waitForSelector('title');

  // Executes Navigation API within the page context
  const metrics = await page.evaluate(() => JSON.stringify(window.performance));

  // Parses the result to JSON
  console.info(JSON.parse(metrics));

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

Метод evaluate получает функцию, возвращающую не сериализованное значение, далее evaluate  возвращает undefined. Поэтому мы превращаем в строку window.performance, тестируя контекст страницы.

Результат трансформируется в объект:

{
   timeOrigin: 1562785571340.2559,
   timing: {
      navigationStart: 1562785571340,
      unloadEventStart: 0,
      unloadEventEnd: 0,
      redirectStart: 0,
      redirectEnd: 0,
      fetchStart: 1562785571340,
      domainLookupStart: 1562785571347,
      domainLookupEnd: 1562785571348,
      connectStart: 1562785571348,
      connectEnd: 1562785571528,
      secureConnectionStart: 1562785571425,
      requestStart: 1562785571529,
      responseStart: 1562785571607,
      responseEnd: 1562785571608,
      domLoading: 1562785571615,
      domInteractive: 1562785571621,
      domContentLoadedEventStart: 1562785571918,
      domContentLoadedEventEnd: 1562785571926,
      domComplete: 1562785572538,
      loadEventStart: 1562785572538,
      loadEventEnd: 1562785572538
   },
   navigation: {
      type: 0,
      redirectCount: 0
   }
}

Можно комбинировать эти метрики, вычисляя длительность загрузки. Например loadEventEnd — navigationStart — время от начала навигации до завершения загрузки страницы.

Подробнее о таймингах по ссылке.

  1. Runtime-метрики

Есть такой API:

const puppeteer = require('puppeteer');

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

  await page.goto('https://pptr.dev');
  await page.waitForSelector('title');

  // Returns runtime metrics of the page
  const metrics = await page.metrics();
  console.info(metrics);

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

Вызываем метод metrics и получаем результат:

{
   Timestamp: 6400.768827, // When the metrics were taken
   Documents: 13, // Number of documents
   Frames: 7, // Number of frames
   JSEventListeners: 33, // Number of events
   Nodes: 51926, // Number of DOM elements
   LayoutCount: 6, // Number of page layouts
   RecalcStyleCount: 13, // Number of page style recalculations
   LayoutDuration: 0.545877, // Total duration of all page layouts
   RecalcStyleDuration: 0.011856, // Total duration of all page style recalculations
   ScriptDuration: 0.064591, // Total duration of JavaScript executions
   TaskDuration: 1.244381, // Total duration of all performed tasks by the browser
   JSHeapUsedSize: 17158776, // Actual memory usage by JavaScript
   JSHeapTotalSize: 33492992 // Total memory usage, including free allocated space, by JavaScript
}

Есть полезная метрика JSHeapUsedSize, показывающая реальное использование памяти страницей, фактически это вывод из метода Performance.getMetrics (ссылка).

  1. Трассировка

Chromium tracing — инструмент профилирования, записывающий действия браузера, собирая данные по каждому потоку, вкладке, и процессу. Все это отображается в Chrome DevTools в панели Timeline.

Следовательно, трассировка возможна и в Puppeteer, который работает через DevTools. Например, запишем действия браузера при навигации:

const puppeteer = require('puppeteer');

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

  // Starts to record a trace of the operations
  await page.tracing.start({ path: 'trace.json' });

  await page.goto('https://pptr.dev');
  await page.waitForSelector('title');

  // Stops the recording
  await page.tracing.stop();

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

После завершения записи создается файл trace.json, содержащий вывод:

{
   "traceEvents":[
      {
         "pid": 21975,
         "tid": 38147,
         "ts": 17376402124,
         "ph": "X",
         "cat": "toplevel",
         "name": "MessageLoop::RunTask",
         "args": {
            "src_file": "../../mojo/public/cpp/system/simple_watcher.cc",
            "src_func": "Notify"
         },
         "dur": 68,
         "tdur": 56,
         "tts": 26330
      },
      // More trace events
   ]
}

Открываем его в Chrome DevTools, в Timeline Viewer.

Панель производительности после импорта файла в DevTools:

Puppeteer performance

Закрепляем пройденное

  • Puppeteer — библиотека Node.js для автоматизации, тестирования и скрапинга веб-страниц, работающая поверх протокола Chrome DevTools.
  • Экосистема с puppeteer-core — библиотекой для автоматизации браузера, которая умеет работать с любым браузером, поддерживающим протокол DevTools без установки Chromium.
  • В экосистеме есть полный пакет, устанавливающий и Chromium помимо библиотеки автоматизации.
  • Puppeteer умеет запускать экземпляр браузера Chromium или подключаться к уже запущенному экземпляру.
  • Есть также экспериментальный пакет puppeteer-firefox для работы с Firefox.
  • Контекст браузера позволяет работать в разных сессиях с одним экземпляром браузера.
  • Puppeteer запускает браузер по умолчанию в headless-режиме. Также есть headful-режим для работы через GUI.
  • Есть несколько способов дебага приложения в браузере. Дебаг процесса, выполняющего Puppeteer, проходит так же как обычного процесса Node.js.
  • Puppeteer умеет выполнять навигацию на странице по URL и работать с мышкой и клавиатурой.
  • Проверяет юзабилити страницы, видимость элементов, их поведение, и быстроту реакции интерфейса на разных устройствах.
  • Умеет делать скриншоты страницы и генерировать PDF.
  • Есть возможности анализа и тестирования доступности контента.
  • Умеет ускорять страницу, помогая удалять мертвый код, поддерживаются метрики и трассировка.
  • Puppeteer — мощный инструмент автоматизации браузеров с простым API. Поддерживаются гибкие и мощные функции, которые в данном гайде не рассматривались, с ними можно ознакомиться в официальной документации. Подробные примеры — по ссылке.

Источник туториала

***

Какой была ваша первая зарплата в QA и как вы искали первую работу?

Мега обсуждение в нашем телеграм-канале о поиске первой работы. Обмен опытом и мнения.

1 КОММЕНТАРИЙ

Подписаться
Уведомить о
guest

1 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
B
B
1 месяц назад

Отличная статья!

Мы в Telegram

Наш официальный канал
Полезные материалы и тесты
Готовимся к собеседованию
Project- и Product-менеджмент

? Популярное

? Telegram-обсуждения

Наши подписчики обсуждают, как искали первую работу в QA. Некоторые ищут ее прямо сейчас.
Наши подписчики рассказывают о том, как не бояться задавать тупые вопросы и чувствовать себя уверенно в новой команде.
Обсуждаем, куда лучше податься - в менеджмент или по технической ветке?
Говорим о конфликтных ситуациях в команде и о том, как их избежать
$1100*
медианная зарплата в QA в июне 2023

*по результатам опроса QA-инженеров в нашем телеграм-канале

Собеседование

19%*
IT-специалистов переехало или приняло решение о переезде из России по состоянию на конец марта 2022

*по результатам опроса в нашем телеграм-канале

live

Обсуждают сейчас