Playwright: Поиск, фильтрация и ожидание элементов на странице

Друзья, приветствую! Для тех, кто не в теме, Playwright — это инструмент для автоматизации и тестирования веб-приложений, который, по моему мнению, уже обошел своего предшественника Selenium, долгое время лидировавшего в автоматизации и тестировании браузеров.

О том, почему Playwright лучше Selenium и зачем вам стоит перейти на Playwright, я подробно писал в предыдущей статье. Там же, хотя и поверхностно, я затронул тему поиска и ожидания элементов на странице.

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

Чем мы займемся сегодня?

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

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

Для чего искать элементы на странице?

На всякий случай решил ответить на этот вопрос.

Необходимо понимать, что целей поиска, фильтрации и ожидания элементов на странице может быть множество.

Например, вы можете искать элементы для банального парсинга. Подключаемся к месту, где хранится цена — сохраняем цену, к блоку с фотографиями — сохраняем ссылки на фото и так далее.

Другой пример — это ожидание элемента с целью дальнейших действий с ним. Например, находим поле для ввода логина — вводим логин, поле для пароля — вводим пароль, после чего кликаем на кнопку «Войти».

Еще один частый пример — это ожидание определенных условий на странице с последующими действиями. Например, мы ждем, пока цена определенного товара станет ниже определенного значения, и совершаем покупку.

Вы можете придумать и другие примеры самостоятельно.

Неразрывно с поиском идет фильтрация найденных элементов и их ожидание. Об этом остановимся отдельно.

Зачем нужна фильтрация найденных элементов?

Фильтрация найденных элементов необходима для более точного поиска. Например, у нас есть список значений (HTML-список, а не питоновский). В этом списке может появляться много ненужных значений для дальнейшей логики или парсинга.

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

Все станет более понятно, когда мы перейдем к практике.

Зачем ждать элементы на странице?

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

В Selenium это ожидание было реализовано неудобно, и если вы работали с ним, то наверняка часто сталкивались с ошибками из-за того, что действие начиналось раньше, чем элемент загружался.

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

Теперь, когда мы рассмотрели теорию, можем приступать к написанию кода. И прежде чем мы это сделаем, хочу напомнить, что исходники кода к каждой своей статье (в том числе и этот), как и эксклюзивный контент, который я не публикую на Хабре, вы найдете в моем телеграм-канале «Легкий путь в Python». Приступим!

Подготовка

Для начала у вас должен быть установлен pytest-playwright — о том, как это сделать и как работать в синхронном и асинхронном режимах, я писал в статье «Playwright: Лучшая альтернатива Selenium. Первое знакомство». Сейчас на этом подробно останавливаться не буду.

Поиск в Playwright осуществляется с помощью так называемых локаторов. Локатор — это элемент на странице, который имеет свой тег, title, id, CSS-селектор, какую-то надпись или любой другой атрибут, который позволяет, используя инструменты Playwright, осуществлять поиск.

В прошлой статье я упоминал, что поиск, как и все действия с найденными элементами, осуществляется через страницу — Page.

Для поиска элементов существуют следующие методы объекта Page:

  • page.get_by_role() — для поиска элемента по явному признаку элемента и надписи на нем.

  • page.get_by_text() — для поиска элемента по текстовому содержимому внутри него.

  • page.get_by_label() — для поиска элемента по тексту связанной с ним метки.

  • page.get_by_placeholder() — для поиска элемента по плейсхолдеру.

  • page.get_by_alt_text() — для поиска элемента по альтернативному тексту (например, alt у изображений).

  • page.get_by_title() — для поиска элемента по его тайтлу.

  • page.get_by_test_id() — для поиска элемента по его data-testid (очень полезный метод для тестировщиков).

  • page.locator() — для поиска элемента по его CSS или XPath.

Даже сейчас, если вы знакомы с Selenium, но не были знакомы с Playwright, вы видите, насколько удобные тут методы для поиска элементов на странице. А когда вы ознакомитесь с ними на практике, то будете в полном восторге.

Далее, как и в первой статье, все примеры будут показаны на языке Python и в асинхронном режиме. Однако, если вы программист на JavaScript, информация, которую я буду представлять, также может быть вам интересна. Playwright изначально был разработан на JavaScript, а затем адаптирован и для других языков, в том числе Python.

Метод get_by_role

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

На этом методе я хочу сосредоточить особое внимание, так как после прошлой статьи к нему было много вопросов.

Локаторы по роли позволяют тестам находить элементы на веб-странице так, как их видят пользователи и вспомогательные технологии. Эти локаторы помогают находить кнопки, флажки, заголовки и другие элементы, исходя из их роли на странице.

Допустим, у вас есть HTML-код с двумя кнопками и чекбоксом:

Заголовок


Мы можем найти эти элементы на странице с помощью get_by_role следующим образом:

# Находим элемент заголовка
el_heading = page.get_by_role("heading", name="Заголовок")

# Находим элемент с чекбоксом
el_checkbox = page.get_by_role("checkbox", name="Разрешить уведомления")

# Находим кнопку с надписью «Сохранить»
btn1 = page.get_by_role("button", name="Сохранить").click()

# Находим кнопку с надписью "Отменить"
btn2 = page.get_by_role("button", name="Отменить")

Вот список всех доступных ролей для данного метода:

"alert" | "alertdialog" | "application" | "article" | "banner" | "blockquote" | 
"button" | "caption" | "cell" | "checkbox" | "code" | "columnheader" | "combobox" | 
"complementary" | "contentinfo" | "definition" | "deletion" | "dialog" | "directory" | 
"document" | "emphasis" | "feed" | "figure" | "form" | "generic" | "grid" | "
"gridcell" | "group" | "heading" | "img" | "insertion" | "link" | "list" | "listbox" | 
"listitem" | "log" | "main" | "marquee" | "math" | "meter" | "menu" | "menubar" | 
"menuitem" | "menuitemcheckbox" | "menuitemradio" | "navigation" | "none" | "note" | 
"option" | "paragraph" | "presentation" | "progressbar" | "radio" | "radiogroup" | 
"region" | "row" | "rowgroup" | "rowheader" | "scrollbar" | "search" | "searchbox" | 
"separator" | "slider" | "spinbutton" | "status" | "strong" | "subscript" | 
"superscript" | "switch" | "tab" | "table" | "tablist" | "tabpanel" | "term" | 
"textbox" | "time" | "timer" | "toolbar" | "tooltip" | "tree" | "treegrid" | 
"treeitem"

Думаю, теперь вы точно уловили общую суть данного метода.

Атрибут name — это любое доступное имя элемента (например, текст внутри кнопки) либо некое регулярное выражение (паттерн), например, указание на то что внутри конкретного элемента есть определенный текст (далее покажу как использовать на практике).

Настоятельно рекомендую хорошо понять данный метод, так как он и такой метод как locator вы будете использовать чаще всего для писка элементов на странице!

Действия для получения элементов

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

  • inner_text: Возвращает текстовое содержимое элемента, как оно отображается на экране, игнорируя HTML-теги и скрытый текст.

  • inner_html: Возвращает HTML-код внутри элемента, включая все теги.

  • text_content: Возвращает текстовое содержимое элемента, включая скрытый текст, но игнорируя HTML-теги.

  • all: Возвращает массив локаторов, указывающих на все соответствующие элементы на странице.

  • get_attribute: Получаем значение атрибута в найденном локаторе.

Это нам необходимо для проверки того нашли ли мы элемент.

Теперь, чтоб закрепить этот блок, предлагаю взять сайт https://whatmyuseragent.com/, который мы рассматривали в прошлой статье, и достать с него элементы через метод get_by_role.

Для того чтоб мы удедились что элемент найден именно тот что нам нужен — просто будем доставать с него текст после того как нашли.

На странице, в месте где выводится UserAgent можно заметить, что данные указаны в теге h5 (heading).

f3d09839512042d5cf79b6af1aa6f0f7.png

И, он всегда будет содержать слово Mozilla. Давайте заберем это значение через get_by_role и регулярное выражание в атрибуте name.

import asyncio
import re

from playwright.async_api import async_playwright, expect


async def async_work():
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            channel='chrome',
            headless=False,
            args=["--start-maximized"]
        )
        context = await browser.new_context(no_viewport=True)
        page = await context.new_page()
        await page.goto('https://whatmyuseragent.com/')

        el = page.get_by_role("heading", name=re.compile("Mozilla", re.IGNORECASE))
        ua = await el.text_content()
        print(ua)
        await asyncio.sleep(600)
        await browser.close()

        
asyncio.run(async_work())

Обратите внимание на использование паттерна регулярного выражения. Данная конструкция:

name=re.compile(find_text, re.IGNORECASE)

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

Сейчас я получил такой результат:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36

Для закрепления я получу ещё некоторые данные через метод get_by_role и перейдем к другому популярному методу для поиска элементов — locator.

import asyncio
import re

from playwright.async_api import async_playwright, expect


async def example_get_by_role():
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            channel='chrome',
            headless=False,
            args=["--start-maximized"]
        )
        context = await browser.new_context(no_viewport=True)
        page = await context.new_page()
        await page.goto('https://whatmyuseragent.com/')

        # получаем User Agent
        ua = await page.get_by_role("heading", name=re.compile("Mozilla", re.IGNORECASE)).text_content()
        print(ua)

        # получаю кнопку для копирования
        copy_button = await page.get_by_role('link', name='copy').inner_html()
        print(copy_button)

        # получаю текст логотипа
        logo_text = await page.get_by_role('link', name=re.compile("WhatMy", re.IGNORECASE)).inner_text()
        print(logo_text)

        # пробежимся по всем значениям списка что есть на странице
        for li in await page.get_by_role('listitem').all():
            text = await li.inner_text()
            print(text)

        await browser.close()

Прежде чем пойдете далее — настоятельно рекомендую самостоятельно написать код и получить при помощи метода get_by_role данные.

Метод locator

В практике этот метод используется так же часто, как и get_by_role, и зачастую они идут вместе. На данном методе поиска элементов на странице я также остановлюсь более подробно.

Метод locator позволяет осуществлять поиск как по CSS-селектору, так и через XPATH. Его можно уверенно назвать универсальным для поиска любого элемента на странице. Если вы научитесь его использовать, то проблем с Playwright у вас не возникнет.

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

Давайте заберем User Agent с нашей страницы, но уже используя locator:

ua = await page.locator('#ua').text_content()
print(ua)

Как видите, запись достаточно лаконичная. В случае с поиском User Agent удобнее использовать метод locator, а не get_by_role, хотя так бывает не всегда.

В данном случае поиск я осуществил через селектор ID = ua. В контексте описания селекторов запись идет через #, что означает ID. Точка, например, означает указание класса, символ > — вложенность и так далее.

Теперь повторим трюк со всеми значениями li со страницы:

# пробежимся по всем значениям списка, что есть на странице
for li in await page.locator('li').all():
    text = await li.inner_text()
    print(text)

Как вы могли заметить, существенного отличия от метода get_by_role здесь нет.

Теперь я покажу, как работать с классом:

# получим текст параграфов с классом card-text
paragraph = await page.locator('p.card-text').all()
for p in paragraph:
    print(await p.inner_text())

В данном случае мы получили текст параграфов с классом card-text.

Теперь получим ссылку на фото:

img = await page.locator('img').get_attribute('src')
print(img)

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

Остальные методы для получения данных

Такие методы получения элемента на странице как:

Я предлагаю вам освоить самостоятельно, так как уже из названия понятно, что делает тот или иной метод. Подробное описание каждого метода вы найдете в официальной документации: Playwright API Документация.

Фильтрация и комбинация методов поиска

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

Фильтрация нужна для того, чтобы в рамках полученного локатора (или локаторов) получить отфильтрованные данные по одному или нескольким фильтрам.

Вот наиболее частые методы фильтрации, которые вы будете использовать в проектах:

Фильтрация по наличию текста

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

# получаем текст из элемента списка, содержащего "Country Name"
el_1 = await page.get_by_role("listitem").filter(has_text="Country Name").inner_text()
print(el_1)

# получаем текст из элемента списка, содержащего "Country Name"
el_2 = await page.locator("li").filter(has_text="Country Name").inner_text()
print(el_2)

Фильтрация по отсутствию текста

# получаем 4-й элемент списка, не содержащий "Country Name"
el_3 = await page.locator("li").filter(has_not_text="Country Name").nth(4).inner_text()
print(el_3)

Здесь важно отметить: если мы используем методы типа inner_text(), inner_html() и так далее, то должны быть уверены, что работаем с одним элементом, так как при наличии нескольких элементов эти методы дадут ошибку что логично.

Для таких случаев в Playwright предусмотрены методы, позволяющие получить один элемент из общего массива. Методов для получения конкретного элемента, когда локатор выдал несколько, всего три:

  • locator.first: получить первый элемент массива.

  • locator.nth(index): получить элемент по индексу. Отсчет индексов идет с нуля.

  • locator.last: получить последний элемент массива.

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

Фильтрация по дочерним элементам / потомкам

Часто возникает необходимость найти элемент с определенными характеристиками, которые находятся внутри его потомков. Для таких целей в Playwright предусмотрен атрибут фильтра has.

Значением данного атрибута необходимо указывать другой, вложенный локатор, тем самым проверяя, есть ли в данном элементе такой локатор. Так же, как и в случае с текстом, есть обратный атрибут has_not, который проверяет на отсутствие вложенного элемента внутри локатора.

Особую мощь все методы фильтрации и методы для получения элементов достигают при их комбинации:

el_4 = await page.locator('nav').get_by_role('link').filter(has_text=".com").inner_text()
print(el_4)

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

Ожидания в Playwright

С недавнего времени основную логику ожидания в Playwright выполняет класс expect. Импортируется он следующим образом:

from playwright.async_api import expect
from playwright.sync_api import expect

В случае использования асинхронного API Playwright метод всегда будет асинхронным.

Основное назначение объекта expect — это проверка появления элемента или выполнение события на странице. Например, если на веб-странице элемент появляется динамически, нужно:

  1. Найти этот элемент через локатор.

  2. Через expect и его методы проверить, появился ли элемент.

  3. Убедившись, что элемент найден и готов к дальнейшей работе, совершить действие, например клик по элементу.

Это называется пользовательскими ожиданиями, и синтаксис выглядит так:

# получаем User Agent
ua = page.get_by_role("heading", name=re.compile("Mozilla", re.IGNORECASE))

# проверяем, что элемент виден на странице
await expect(ua).to_be_visible()

# достаем текст
ua_text = await ua.inner_text()
print(ua_text)

Синтаксис достаточно простой. Всегда первым аргументом в expect нужно передавать локатор, который будем проверять, а затем применять способ проверки. В примере выше это метод to_be_visible(), который проверяет, стал ли виден элемент на странице. После того как элемент найден, можно извлечь из него текст и распечатать.

Методов проверки, как вы догадались, несколько. Вот короткое описание каждого:

  • expect (locator).to_be_attached () — Проверяет, что элемент находится в DOM (прикреплен к странице).

  • expect (locator).to_be_checked () — Проверяет, что флажок (checkbox) установлен.

  • expect (locator).to_be_disabled () — Проверяет, что элемент отключен.

  • expect (locator).to_be_editable () — Проверяет, что элемент доступен для редактирования.

  • expect (locator).to_be_empty () — Проверяет, что контейнер пуст.

  • expect (locator).to_be_enabled () — Проверяет, что элемент включен.

  • expect (locator).to_be_focused () — Проверяет, что элемент в фокусе.

  • expect (locator).to_be_hidden () — Проверяет, что элемент не видим.

  • expect (locator).to_be_in_viewport () — Проверяет, что элемент находится в пределах видимой области экрана (viewport).

  • expect (locator).to_be_visible () — Проверяет, что элемент видим.

  • expect (locator).to_contain_text () — Проверяет, что элемент содержит определенный текст.

  • expect (locator).to_have_accessible_description () — Проверяет, что у элемента есть доступное описание, соответствующее ожидаемому.

  • expect (locator).to_have_accessible_name () — Проверяет, что у элемента есть доступное имя, соответствующее ожидаемому.

  • expect (locator).to_have_attribute () — Проверяет, что у элемента есть указанный атрибут DOM.

  • expect (locator).to_have_class () — Проверяет, что у элемента есть указанное значение свойства class.

  • expect (locator).to_have_count () — Проверяет, что список содержит точное количество дочерних элементов.

  • expect (locator).to_have_css () — Проверяет, что у элемента есть определенное CSS-свойство.

  • expect (locator).to_have_id () — Проверяет, что у элемента есть определенный идентификатор (ID).

  • expect (locator).to_have_js_property () — Проверяет, что у элемента есть указанное JavaScript-свойство.

  • expect (locator).to_have_role () — Проверяет, что у элемента есть определенная ARIA-роль.

  • expect (locator).to_have_text () — Проверяет, что элемент соответствует заданному тексту.

  • expect (locator).to_have_value () — Проверяет, что у элемента ввода (input) есть определенное значение.

  • expect (locator).to_have_values () — Проверяет, что у select-элемента выбраны определенные опции.

  • expect (page).to_have_title () — Проверяет, что у страницы есть определенный заголовок.

  • expect (page).to_have_url () — Проверяет, что у страницы есть определенный URL.

  • expect (response).to_be_ok () — Проверяет, что ответ имеет статус «OK».

Тут, обратите внимание что expect работает не только с локатором, но и со страницей и запросами (отдельно как-то разберем).

Как вы понимаете, каждый такой метод может включать свои определенные аргументы или не включать их. К примеру to_be_visible () может включать только атрибут timeout, тогда как to_have_count () обязательно должен включать в себя число для проверки.

В остальном, думаю, логику общую вы уловили. Сначала в expect мы передаем локатор, страницу или запрос, а после над ним уже выполняется определенная проверка, подходящая под конкретное условие.

Это, вместе с автматическими таймаутами, позволяет нам делать стабильную автматизацию веб-приложений.

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

Заключение

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

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

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

В планах — написать еще несколько детализированных статей на тему Playwright. Если я увижу ваш интерес и поддержку, то с удовольствием продолжу делиться своими знаниями и опытом.

В завершение хочу напомнить, что исходный код из этой статьи, а также эксклюзивные материалы, которые я не публикую на Хабре, вы сможете найти на моем Telegram-канале «Легкий путь в Python».

Спасибо за ваше внимание и до новых встреч!

© Habrahabr.ru