Puppetaria: сценарии Puppeteer, ориентированные на доступность

Йохан Бэй
Johan Bay

«Кукловод» и его подход к селекционерам

Puppeteer — это библиотека автоматизации браузера для Node: она позволяет управлять браузером с помощью простого и современного API JavaScript.

Самая важная задача браузера — это, конечно же, просмотр веб-страниц. Автоматизация этой задачи по сути сводится к автоматизации взаимодействия с веб-страницей.

В Puppeteer это достигается путем запроса элементов DOM с использованием строковых селекторов и выполнения таких действий, как щелчок или ввод текста на элементах. Например, открывающийся скрипт открывает сайт Developer.google.com , находит поле поиска и выполняет поиск по puppetaria может выглядеть так:

(async () => {
   const browser = await puppeteer.launch({ headless: false });
   const page = await browser.newPage();
   await page.goto('https://developers.google.com/', { waitUntil: 'load' });
   // Find the search box using a suitable CSS selector.
   const search = await page.$('devsite-search > form > div.devsite-search-container');
   // Click to expand search box and focus it.
   await search.click();
   // Enter search string and press Enter.
   await search.type('puppetaria');
   await search.press('Enter');
 })();

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

Синтаксические и семантические селекторы

Селекторы CSS имеют синтаксический характер; они тесно связаны с внутренней работой текстового представления дерева DOM в том смысле, что они ссылаются на идентификаторы и имена классов из DOM. По сути, они предоставляют веб-разработчикам интегрированный инструмент для изменения или добавления стилей к элементу на странице, но в этом контексте разработчик имеет полный контроль над страницей и ее деревом DOM.

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

В результате такие сценарии могут оказаться хрупкими и подверженными изменениям исходного кода. Предположим, например, что кто-то использует сценарии Puppeteer для автоматического тестирования веб-приложения, содержащего узел <button>Submit</button> в качестве третьего дочернего элемента элемента body . Один фрагмент тестового примера может выглядеть так:

const button = await page.$('body:nth-child(3)'); // problematic selector
await button.click();

Здесь мы используем селектор 'body:nth-child(3)' для поиска кнопки отправки, но она тесно привязана именно к этой версии веб-страницы. Если позже над кнопкой будет добавлен элемент, этот селектор больше не будет работать!

Для авторов тестов это не новость: пользователи Puppeteer уже пытаются выбирать селекторы, устойчивые к таким изменениям. С Puppetaria мы даем пользователям новый инструмент в этом квесте.

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

Мы называем такие селекторы «селекторами ARIA » и поддерживаем запрос вычисленного доступного имени и роли дерева доступности. По сравнению с селекторами CSS эти свойства носят семантический характер. Они не привязаны к синтаксическим свойствам DOM, а являются дескрипторами того, как страница просматривается с помощью вспомогательных технологий, таких как программы чтения с экрана.

В приведенном выше примере тестового сценария мы могли бы вместо этого использовать селектор aria/Submit[role="button"] для выбора нужной кнопки, где Submit относится к доступному имени элемента:

const button = await page.$('aria/Submit[role="button"]');
await button.click();

Теперь, если позже мы решим изменить текстовое содержимое нашей кнопки с Submit на Done , тест снова завершится неудачно, но в данном случае это желательно; изменяя имя кнопки, мы изменяем содержимое страницы, а не ее визуальное представление или то, как она структурирована в DOM. Наши тесты должны предупреждать нас о таких изменениях, чтобы гарантировать, что такие изменения являются преднамеренными.

Возвращаясь к более крупному примеру с панелью поиска, мы могли бы использовать новый обработчик aria и заменить

const search = await page.$('devsite-search > form > div.devsite-search-container');

с

const search = await page.$('aria/Open search[role="button"]');

чтобы найти строку поиска!

В более общем плане мы считаем, что использование таких селекторов ARIA может предоставить пользователям Puppeteer следующие преимущества:

  • Сделайте селекторы в тестовых сценариях более устойчивыми к изменениям исходного кода.
  • Сделайте тестовые сценарии более читабельными (доступные имена — это семантические дескрипторы).
  • Мотивируйте передовой опыт назначения элементам свойств доступности.

Оставшаяся часть этой статьи посвящена подробностям того, как мы реализовали проект Puppetaria.

Процесс проектирования

Фон

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

Из спецификации для вычисления доступного имени становится ясно, что вычисление имени для элемента — нетривиальная задача, поэтому с самого начала мы решили, что хотим повторно использовать для этого существующую инфраструктуру Chromium.

Как мы подошли к реализации

Даже ограничившись использованием дерева доступности Chromium, существует немало способов реализации ARIA-запросов в Puppeteer. Чтобы понять почему, давайте сначала посмотрим, как Puppeteer управляет браузером.

Браузер предоставляет интерфейс отладки через протокол Chrome DevTools Protocol (CDP) . Это предоставляет такие функции, как «перезагрузить страницу» или «выполнить этот фрагмент JavaScript на странице и вернуть результат» через независимый от языка интерфейс.

И интерфейс DevTools, и Puppeteer используют CDP для взаимодействия с браузером. Для реализации команд CDP существует инфраструктура DevTools внутри всех компонентов Chrome: в браузере, в рендерере и так далее. CDP заботится о маршрутизации команд в нужное место.

Действия Puppeteer, такие как запрос, нажатие и оценка выражений, выполняются с помощью команд CDP, таких как Runtime.evaluate , которые оценивают JavaScript непосредственно в контексте страницы и возвращают результат. Другие действия Puppeteer, такие как эмуляция дефицита цветового зрения, создание снимков экрана или захват следов, используют CDP для прямой связи с процессом рендеринга Blink.

CDP

Это уже оставляет нам два пути реализации нашей функциональности запросов; мы можем:

  • Напишите нашу логику запросов на JavaScript и внедрите ее на страницу с помощью Runtime.evaluate или
  • Используйте конечную точку CDP, которая может получать доступ к дереву доступности и запрашивать его непосредственно в процессе Blink.

Мы реализовали 3 прототипа:

  • Обход JS DOM — на основе внедрения JavaScript на страницу.
  • Обход Puppeteer AXTree — на основе использования существующего доступа CDP к дереву доступности.
  • Обход CDP DOM — использование новой конечной точки CDP, специально созданной для запроса дерева доступности.

Обход JS DOM

Этот прототип выполняет полный обход DOM и использует element.computedName и element.computedRole , привязанные к флагу запуска ComputedAccessibilityInfo , для получения имени и роли для каждого элемента во время обхода.

Кукловод AXTОбход дерева

Здесь мы вместо этого получаем полное дерево доступности через CDP и просматриваем его в Puppeteer. Полученные узлы доступности затем сопоставляются с узлами DOM.

CDP обход DOM

Для этого прототипа мы реализовали новую конечную точку CDP специально для запроса дерева доступности. Таким образом, запросы могут выполняться на серверной стороне через реализацию C++, а не в контексте страницы через JavaScript.

Тестирование модульных тестов

На следующем рисунке сравнивается общее время выполнения запроса четырех элементов 1000 раз для трех прототипов. Тест проводился в трех различных конфигурациях, в зависимости от размера страницы и включения или отключения кэширования элементов доступности.

Тест: общее время выполнения запроса к четырем элементам 1000 раз.

Совершенно очевидно, что существует значительный разрыв в производительности между механизмом запросов, поддерживаемым CDP, и двумя другими, реализованными исключительно в Puppeteer, и относительная разница, похоже, резко увеличивается с размером страницы. Несколько интересно видеть, что прототип обхода JS DOM так хорошо реагирует на включение кэширования специальных возможностей. Если кэширование отключено, дерево доступности вычисляется по требованию и удаляет его после каждого взаимодействия, если домен отключен. Включение домена заставляет Chromium кэшировать вычисленное дерево.

Для обхода JS DOM мы запрашиваем доступное имя и роль для каждого элемента во время обхода, поэтому, если кеширование отключено, Chromium вычисляет и отбрасывает дерево доступности для каждого посещаемого нами элемента. С другой стороны, для подходов, основанных на CDP, дерево отбрасывается только между каждым вызовом CDP, то есть для каждого запроса. Эти подходы также выигрывают от включения кэширования, поскольку дерево доступности затем сохраняется при вызовах CDP, но прирост производительности поэтому сравнительно меньше.

Несмотря на то, что включение кэширования здесь выглядит желательным, оно сопряжено с затратами на дополнительное использование памяти. Для сценариев Puppeteer, которые, например, записывают файлы трассировки , это может быть проблематично. Поэтому мы решили не включать кэширование дерева доступности по умолчанию. Пользователи могут самостоятельно включить кэширование, включив домен доступности CDP.

Тест набора тестов DevTools

Предыдущий тест показал, что реализация нашего механизма запросов на уровне CDP дает прирост производительности в сценарии клинического модульного тестирования.

Чтобы увидеть, достаточно ли выражена разница, чтобы сделать ее заметной в более реалистичном сценарии запуска полного набора тестов, мы исправили набор сквозных тестов DevTools, чтобы использовать прототипы на основе JavaScript и CDP, и сравнили время выполнения. . В этом тесте мы изменили в общей сложности 43 селектора с [aria-label=…] на собственный обработчик запросов aria/… , который затем реализовали с использованием каждого из прототипов.

Некоторые селекторы используются в тестовых сценариях несколько раз, поэтому фактическое количество выполнений обработчика запросов aria составило 113 за один запуск пакета. Общее количество выборок запросов составило 2253, поэтому только часть выборок запросов произошла через прототипы.

Тест: набор тестов e2e

Как видно на рисунке выше, существует заметная разница в общем времени выполнения. Данные слишком зашумлены, чтобы сделать какой-либо конкретный вывод, но ясно, что разрыв в производительности между двумя прототипами проявляется и в этом сценарии.

Новая конечная точка CDP

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

Для нашего варианта использования в Puppeteer нам нужно, чтобы конечная точка принимала в качестве аргумента так называемые RemoteObjectIds , а чтобы мы могли впоследствии найти соответствующие элементы DOM, она должна возвращать список объектов, которые содержат backendNodeIds для элементов DOM.

Как видно на диаграмме ниже, мы опробовали довольно много подходов, удовлетворяющих этому интерфейсу. Исходя из этого, мы обнаружили, что размер возвращаемых объектов, то есть возвращали ли мы узлы полной доступности или только backendNodeIds , не имел заметной разницы. С другой стороны, мы обнаружили, что использование существующего NextInPreOrderIncludingIgnored было плохим выбором для реализации здесь логики обхода, поскольку приводило к заметному замедлению работы.

Тест: сравнение прототипов обхода AXTree на основе CDP.

Подводя итоги

Теперь, когда конечная точка CDP установлена, мы реализовали обработчик запросов на стороне Puppeteer . Основная работа здесь заключалась в реструктуризации кода обработки запросов, чтобы запросы могли обрабатываться непосредственно через CDP, а не через JavaScript, оцениваемый в контексте страницы.

Что дальше?

Новый обработчик aria поставляется с Puppeteer v5.4.0 в качестве встроенного обработчика запросов. Мы с нетерпением ждем возможности увидеть, как пользователи внедрят его в свои тестовые сценарии, и нам не терпится услышать ваши идеи о том, как мы можем сделать это еще более полезным!

Загрузите предварительный просмотр каналов

Рассмотрите возможность использования Chrome Canary , Dev или Beta в качестве браузера для разработки по умолчанию. Эти каналы предварительного просмотра дают вам доступ к новейшим функциям DevTools, тестируют передовые API-интерфейсы веб-платформы и находят проблемы на вашем сайте раньше, чем это сделают ваши пользователи!

Связь с командой Chrome DevTools

Используйте следующие параметры, чтобы обсудить новые функции и изменения в публикации или что-либо еще, связанное с DevTools.