Что такое Puppeteer
Статистика
Проект команды Google Chrome, Node-библиотека для управления Chrome и любым другим браузером, поддерживающим протокол Chrome DevTools (то есть практически все современные браузеры). В Puppeteer автоматизируются частые действия пользователя, есть неплохое API. Простой полезный инструмент автоматизации, QA-тестирования, и парсинга веб-страниц в обычном или headless-режиме.
«Кукловодом» сейчас пользуются треть опрошенных тестировщиков:
Довольны около 80%:
Что касается показателей более конкретных, чем «нравится», а именно количества npm-загрузок, то здесь безраздельно властвуют Cypress — и да, Puppeteer:
Звезды на Гитхабе:
Сравнение Selenium, Cypress, Puppeteer и Playwright
Сравнение по: | Selenium | Cypress | Puppeteer | Playwright |
---|---|---|---|---|
Кроссбраузерное тестирование | Поддерживает все браузеры | Только Chrome и Firefox | Только Chrome и Firefox | Chrome, Firefox, Safari |
Множественные вкладки и фреймы | Через API, проблемно | Плохо | Интуитивное API | Интуитивное API |
Параллельный запуск, Grid, Infra | Поддерживает | Только в платной версии, в облаке | Нужно создавать и конфигурировать | Нужно создавать и конфигурировать |
Скорость | Приличная | Быстро | Быстро | Нормально |
Дебаг автотестов | Зависит от grid-провайдера | Через DOM | Через IDE | Через IDE |
Документация и FAQ | Обширнейшая | Отлично | Есть документация и туториалы | Не очень, к тому же часто меняется API |
Комьюнити | Огромное | Небольшое | Небольшое | Небольшое |
Puppeteer и Selenium
Чем отличается | Puppeteer | Selenium |
---|---|---|
Нужны ли скиллы программирования (то есть знание ЯП на высоком уровне) | Да, нужны | Требования ниже, чем с Puppeteer — ЯП нужны для работы с WebDriver, но не очень нужны для Selenium IDE |
Какие поддерживает языки | Только JS | Python, Java, Node.js, C# |
Какие поддерживает браузеры | Chrome и (частично) Firefox | Chrome, Firefox, Safari, IE, Opera |
Комьюнити | Не очень, на фоне Selenium. GitHub, StackOverflow | Широчайшее |
Скорость | Очень высокая в Chrome | Может быть медленнее чем Puppeteer |
Сложность установки и настройки | Очень легко, одной командой | Довольно сложно, особенно нубам |
Кроссплатформенное тестирование | Нет и не скоро будет | Да, отлично |
Запись | Нет | Да, в Selenium IDE |
Снятие скриншотов | Рисунки и PDFки | Только рисунки |
Что с мобильным тестированием | Нет, только веб | Да, в связке с Appium |
Что лучше, Playwright или Puppeteer?
Playwright появился позже чем Puppeteer, в некотором смысле является его «расширением» (что упрощает миграцию) с более продуманными функциями.
Puppeteer | Playwright | |
---|---|---|
Создан и принадлежит | Microsoft | |
Поддерживает браузеры | Chromium (включая Edge) и Firefox (экспериментальная функция) | Chromium (Chrome, Edge, Opera), Firefox and WebKit (Safari) |
Поддерживает языки | JavaScript, TypeScript | JavaScript, TypeScript, Python, .NET, C#, Java |
Поддерживается кем | Chrome DevTools Team | Microsoft |
Открытый код | Да | Да |
Рантайм-окружение | Node.js | Node.js, Python, Java, C# |
Кроссплатформенность | Windows, Linux, Mac | Windows, Linux, Mac |
Фреймворки | Mocha и Jest | Mocha, Jest, Jasmine, AVA |
Use-кейсы | Веб-тестирование и автоматизация | Веб-тестирование и автоматизация |
Появился когда | 2017 | 2020 |
Сколько звезд на GitHub | 80К+ | 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 работает с браузером через 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 есть советы по дебагу. Важные нюансы приведены ниже.
- Проверка браузера
Может быть нужно проверить, как скрипты работают с браузером, и что он отображает. В headful-режиме это делается примерно так:
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ headless: false, slowMo: 200 }); // Browser operations await browser.close(); })();
Можно еще раз проверить инструкции браузеру, благодаря функции slowMo, которая «замедляет» Puppeteer.
- Дебаг кода приложения в браузере
Если нужен дебаг приложения в открытом браузере, то открываем 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.
Второй способ намного проще, но требует наличия экземпляра страницы (об этом позже).
- Дебаг процесса, запустившего 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 предусмотрены wait-методы ожидания: элементов, навигации, функций, запросов, ответов, или просто предикаты для асинхронных операций.
Как видим, на официальном сайте Puppeteer есть «проходная» страница, из которой идет перенаправление на главную. На проходной странице нет мета-элемента title:
При заходе на сайт 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.
Результат выполнения кода:
В консоли видим, что страница открылась с нужным 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 просто выполняет скрипт в контексте страницы. В выводе — события:
Можно слушать кастомные события на странице, которые активируются по триггеру. То есть можно описать обработчик событий в окне страницы через метод 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, который позиционирует указатель в нужном месте, по центру второй ссылки. На экране видим:
Все ок, указатель наведен на вторую ссылку. Далее эмулируем нажатие на ссылку в нужном месте:
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, чтобы выбрать третий элемент в списке:
Скриншоты
Есть специальный метод для скриншотов:
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(); })();
Что увидим:
Генерация 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 генерит файл:
Изменение геолокации
Многие веб-сайты меняют выдачу в зависимости от геолокации пользователя. Поменять геолокацию страницы:
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 развитая система оценки производительности страницы с метриками.
- Анализ времени загрузки страницы
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 — время от начала навигации до завершения загрузки страницы.
Подробнее о таймингах по ссылке.
- 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 (ссылка).
- Трассировка
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 — библиотека 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. Поддерживаются гибкие и мощные функции, которые в данном гайде не рассматривались, с ними можно ознакомиться в официальной документации. Подробные примеры — по ссылке.
Источник туториала
***