Локаторы
Введение
Locators являются центральной частью автоматического ожидания и возможности повторных попыток в Playwright. Вкратце, локаторы представляют собой способ найти элемент(ы) на странице в любой момент времени.
Быстрый гид
Это рекомендуемые встроенные локаторы.
- Page.getByRole() для поиска по явным и неявным атрибутам доступности.
- Page.getByText() для поиска по текстовому содержимому.
- Page.getByLabel() для поиска элемента управления формой по тексту связанной метки.
- Page.getByPlaceholder() для поиска ввода по заполнителю.
- Page.getByAltText() для поиска элемента, обычно изображения, по его текстовой альтернативе.
- Page.getByTitle() для поиска элемента по его атрибуту title.
- Page.getByTestId() для поиска элемента на основе его атрибута
data-testid
(другие атрибуты могут быть настроены).
page.getByLabel("User Name").fill("John");
page.getByLabel("Password").fill("secret-password");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in"))
.click();
assertThat(page.getByText("Welcome, John!")).isVisible();
Поиск элементов
Playwright предоставляет несколько встроенных локаторов. Чтобы сделать тесты устойчивыми, мы рекомендуем отдавать приоритет атрибутам, ориентированным на пользователя, и явным контрактам, таким как Page.getByRole().
Например, рассмотрим следующую структуру DOM.
<button>Sign in</button>
Найдите элемент по его роли button
с именем "Sign in".
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in"))
.click();
Используйте генератор кода для создания локатора, а затем отредактируйте его по своему усмотрению.
Каждый раз, когда локатор используется для действия, актуальный элемент DOM находится на странице. В приведенном ниже фрагменте кода базовый элемент DOM будет найден дважды, один раз перед каждым действием. Это означает, что если DOM изменится между вызовами из-за повторного рендеринга, будет использован новый элемент, соответствующий локатору.
Locator locator = page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Sign in"));
locator.hover();
locator.click();
Обратите внимание, что все методы, создающие локатор, такие как Page.getByLabel(), также доступны в классах Locator и FrameLocator, поэтому вы можете их объединять и итеративно сужать ваш локатор.
Locator locator = page
.frameLocator("#my-frame")
.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in"));
locator.click();
Поиск по роли
Локатор Page.getByRole() отражает, как пользователи и вспомогательные технологии воспринимают страницу, например, является ли какой-то элемент кнопкой или флажком. При поиске по роли обычно следует также передавать доступное имя, чтобы локатор точно указывал на нужный элемент.
Например, рассмотрим следующую структуру DOM.
Sign up
<h3>Sign up</h3>
<label>
<input type="checkbox" /> Subscribe
</label>
<br/>
<button>Submit</button>
Вы можете найти каждый элемент по его неявной роли:
assertThat(page
.getByRole(AriaRole.HEADING,
new Page.GetByRoleOptions().setName("Sign up")))
.isVisible();
page.getByRole(AriaRole.CHECKBOX,
new Page.GetByRoleOptions().setName("Subscribe"))
.check();
page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName(
Pattern.compile("submit", Pattern.CASE_INSENSITIVE)))
.click();
Локаторы ролей включают кнопки, флажки, заголовки, ссылки, списки, таблицы и многое другое и следуют спецификациям W3C для роли ARIA, атрибутов ARIA и доступного имени. Обратите внимание, что многие HTML-элементы, такие как <button>
, имеют неявно определенную роль, которая распознается локатором роли.
Обратите внимание, что локаторы ролей не заменяют аудиты доступности и тесты на соответствие, но дают раннюю обратную связь о руководствах ARIA.
Мы рекомендуем отдавать приоритет локаторам ролей для поиска элементов, так как это наиболее близкий способ к тому, как пользователи и вспомогательные технологии воспринимают страницу.
Поиск по метке
Большинство элементов управления формами обычно имеют выделенные метки, которые можно удобно использовать для взаимодействия с формой. В этом случае вы можете найти элемент управления по его связанной метке, используя Page.getByLabel().
Например, рассмотрим следующую структуру DOM.
<label>Password <input type="password" /></label>
Вы можете заполнить ввод после его нахождения по тексту метки:
page.getByLabel("Password").fill("secret");
Используйте этот локатор при поиске полей формы.
Поиск по заполнителю
Вводы могут иметь атрибут заполнителя, чтобы подсказать пользователю, какое значение следует ввести. Вы можете найти такой ввод, используя Page.getByPlaceholder().
Например, рассмотрим следующую структуру DOM.
<input type="email" placeholder="name@example.com" />
Вы можете заполнить ввод после его нахождения по тексту заполнителя:
page.getByPlaceholder("name@example.com").fill("playwright@microsoft.com");
Используйте этот локатор при поиске элементов формы, у которых нет меток, но есть текст заполнителя.
Поиск по тексту
Найдите элемент по тексту, который он содержит. Вы можете сопоставить по подстроке, точной строке или регулярному выражению, используя Page.getByText().
Например, рассмотрим следующую структуру DOM.
<span>Welcome, John</span>
Вы можете найти элемент по тексту, который он содержит:
assertThat(page.getByText("Welcome, John")).isVisible();
Установите точное совпадение:
assertThat(page
.getByText("Welcome, John", new Page.GetByTextOptions().setExact(true)))
.isVisible();
Совпадение с регулярным выражением:
assertThat(page
.getByText(Pattern.compile("welcome, john$", Pattern.CASE_INSENSITIVE)))
.isVisible();
Сопоставление по тексту всегда нормализует пробелы, даже при точном совпадении. Например, оно превращает несколько пробелов в один, превращает разрывы строк в пробелы и игнорирует начальные и конечные пробелы.
Мы рекомендуем использовать локаторы текста для поиска неинтерактивных элементов, таких как div
, span
, p
и т.д. Для интерактивных элементов, таких как button
, a
, input
и т.д., используйте локаторы ролей.
Вы также можете фильтровать по тексту, что может быть полезно при попытке найти конкретный элемент в списке.
Поиск по альтернативному тексту
Все изображения должны иметь атрибут alt
, который описывает изображение. Вы можете найти изображение на основе текстовой альтернативы, используя Page.getByAltText().
Например, рассмотрим следующую структуру DOM.
<img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />
Вы можете нажать на изображение после его нахождения по текстовой альтернативе:
page.getByAltText("playwright logo").click();
Используйте этот локатор, когда ваш элемент поддерживает альтернативный текст, такой как элементы img
и area
.
Поиск по заголовку
Найдите элемент с совпадающим атрибутом title, используя Page.getByTitle().
Например, рассмотрим следующую структуру DOM.
<span title='Issues count'>25 issues</span>
Вы можете проверить количество проблем после его нахождения по тексту заголовка:
assertThat(page.getByTitle("Issues count")).hasText("25 issues");
Используйте этот локатор, когда ваш элемент имеет атрибут title
.
Поиск по тестовому идентификатору
Тестирование по тестовым идентификаторам является наиболее устойчивым способом тестирования, так как даже если ваш текст или роль атрибута изменится, тест все равно пройдет. QA и разработчики должны определять явные тестовые идентификаторы и запрашивать их с помощью Page.getByTestId(). Однако тестирование по тестовым идентификаторам не ориентировано на пользователя. Если роль или текстовое значение важны для вас, рассмотрите возможность использования локаторов, ориентированных на пользователя, таких как локаторы ролей и локаторы текста.
Например, рассмотрим следующую структуру DOM.
<button data-testid="directions">Itinéraire</button>
Вы можете найти элемент по его тестовому идентификатору:
page.getByTestId("directions").click();
Установите пользовательский атрибут тестового идентификатора
По умолчанию, Page.getByTestId() будет находить элементы на основе атрибута data-testid
, но вы можете настроить его в конфигурации теста или вызвав Selectors.setTestIdAttribute().
Установите тестовый идентификатор для использования пользовательского атрибута данных для ваших тестов.
playwright.selectors().setTestIdAttribute("data-pw");
В вашем HTML вы теперь можете использовать data-pw
в качестве вашего тестового идентификатора вместо стандартного data-testid
.
<button data-pw="directions">Itinéraire</button>
А затем найдите элемент, как вы обычно это делаете:
page.getByTestId("directions").click();
Поиск по CSS или XPath
Если вам абсолютно необходимо использовать локаторы CSS или XPath, вы можете использовать Page.locator() для создания локатора, который принимает селектор, описывающий, как найти элемент на странице. Playwright поддерживает селекторы CSS и XPath и автоматически определяет их, если вы опускаете префикс css=
или xpath=
.
page.locator("css=button").click();
page.locator("xpath=//button").click();
page.locator("button").click();
page.locator("//button").click();
Селекторы XPath и CSS могут быть привязаны к структуре DOM или реализации. Эти селекторы могут ломаться, когда структура DOM изменяется. Длинные цепочки CSS или XPath ниже являются примером плохой практики, которая приводит к нестабильным тестам:
page.locator(
"#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input"
).click();
page.locator("//*[@id='tsf']/div[2]/div[1]/div[1]/div/div[2]/input").click();
CSS и XPath не рекомендуются, так как DOM часто может изменяться, что приводит к неустойчивым тестам. Вместо этого попробуйте придумать локатор, который близок к тому, как пользователь воспринимает страницу, например, локаторы ролей или определите явный тестовый контракт с использованием тестовых идентификаторов.
Поиск в Shadow DOM
Все локаторы в Playwright по умолчанию работают с элементами в Shadow DOM. Исключения составляют:
- Поиск по XPath не проникает в корни shadow.
- Закрытые корни shadow не поддерживаются.
Рассмотрим следующий пример с пользовательским веб-компонентом:
<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>Title</div>
#shadow-root
<div id=inner-details>Details</div>
</x-details>
Вы можете найти так же, как если бы корень shadow вообще не присутствовал.
Чтобы нажать <div>Details</div>
:
page.getByText("Details").click();
<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>Title</div>
#shadow-root
<div id=inner-details>Details</div>
</x-details>
Чтобы нажать <x-details>
:
page.locator("x-details", new Page.LocatorOptions().setHasText("Details"))
.click();
<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>Title</div>
#shadow-root
<div id=inner-details>Details</div>
</x-details>
Чтобы убедиться, что <x-details>
содержит текст "Details":
assertThat(page.locator("x-details")).containsText("Details");
Фильтрация локаторов
Рассмотрим следующую структуру DOM, где мы хотим нажать на кнопку покупки второй карточки продукта. У нас есть несколько вариантов, чтобы отфильтровать локаторы и получить нужный.
Product 1
Product 2
<ul>
<li>
<h3>Product 1</h3>
<button>Add to cart</button>
</li>
<li>
<h3>Product 2</h3>
<button>Add to cart</button>
</li>
</ul>
Фильтрация по тексту
Локаторы могут быть отфильтрованы по тексту с помощью метода Locator.filter(). Он будет искать определенную строку где-то внутри элемента, возможно, в дочернем элементе, без учета регистра. Вы также можете передать регулярное выражение.
page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHasText("Product 2"))
.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Add to cart"))
.click();
Используйте регулярное выражение:
page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions()
.setHasText(Pattern.compile("Product 2")))
.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Add to cart"))
.click();
Фильтрация по отсутствию текста
В качестве альтернативы, фильтрация по отсутствию текста:
// 5 in-stock items
assertThat(page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHasNotText("Out of stock")))
.hasCount(5);
Фильтрация по дочернему/потомку
Локаторы поддерживают опцию выбора только тех элементов, которые имеют или не имеют потомка, соответствующего другому локатору. Таким образом, вы можете фильтровать по любому другому локатору, такому как Locator.getByRole(), Locator.getByTestId(), Locator.getByText() и т.д.
Product 1
Product 2
<ul>
<li>
<h3>Product 1</h3>
<button>Add to cart</button>
</li>
<li>
<h3>Product 2</h3>
<button>Add to cart</button>
</li>
</ul>
page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions()
.setHas(page.GetByRole(AriaRole.HEADING, new Page.GetByRoleOptions()
.setName("Product 2"))))
.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Add to cart"))
.click();
Мы также можем проверить карточку продукта, чтобы убедиться, что она только одна:
assertThat(page
.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions()
.setHas(page.GetByRole(AriaRole.HEADING,
new Page.GetByRoleOptions().setName("Product 2")))))
.hasCount(1);
Фильтрующий локатор должен быть относительным к исходному локатору и запрашивается, начиная с совпадения исходного локатора, а не с корня документа. Поэтому следующее не сработает, потому что фильтрующий локатор начинает совпадать с элементом списка <ul>
, который находится за пределами элемента списка <li>
, совпадающего с исходным локатором:
// ✖ НЕПРАВИЛЬНО
assertThat(page
.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions()
.setHas(page.GetByRole(AriaRole.LIST)
.GetByRole(AriaRole.HEADING,
new Page.GetByRoleOptions().setName("Product 2")))))
.hasCount(1);
Фильтрация по отсутствию дочернего/потомка
Мы также можем фильтровать по отсутствию совпадающего элемента внутри.
assertThat(page
.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHasNot(page.getByText("Product 2"))))
.hasCount(1);
Обратите внимание, что внутренний локатор сопоставляется, начиная с внешнего, а не с корня документа.
Операторы локаторов
Сопоставление внутри локатора
Вы можете объединять методы, создающие локатор, такие как Page.getByText() или Locator.getByRole(), чтобы сузить поиск до определенной части страницы.
В этом примере мы сначала создаем локатор под названием product, находя его по роли listitem
. Затем мы фильтруем по тексту. Мы можем снова использовать локатор product, чтобы получить элемент по роли кнопки и нажать на него, а затем использовать утверждение, чтобы убедиться, что есть только один продукт с текстом "Product 2".
Locator product = page
.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHasText("Product 2"));
product
.getByRole(AriaRole.BUTTON,
new Locator.GetByRoleOptions().setName("Add to cart"))
.click();
Вы также можете объединить два локатора, например, чтобы найти кнопку "Save" внутри определенного диалога:
Locator saveButton = page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Save"));
// ...
Locator dialog = page.getByTestId("settings-dialog");
dialog.locator(saveButton).click();
Сопоставление двух локаторов одновременно
Метод Locator.and() сужает существующий локатор, сопоставляя дополнительный локатор. Например, вы можете объединить Page.getByRole() и Page.getByTitle(), чтобы сопоставить по роли и заголовку.
Locator button = page.getByRole(AriaRole.BUTTON).and(page.getByTitle("Subscribe"));
Сопоставление одного из двух альтернативных локаторов
Если вы хотите нацелиться на один из двух или более элементов и не знаете, какой из них это будет, используйте Locator.or(), чтобы создать локатор, который соответствует любому из альтернатив.
Например, рассмотрим сценарий, в котором вы хотите нажать на кнопку "New email", но иногда вместо этого появляется диалоговое окно настроек безопасности. В этом случае вы можете подождать либо кнопку "New email", либо диалог и действовать соответственно.
Если на экране появляются и кнопка "New email", и диалог безопасности, локатор "or" будет соответствовать обоим из них, возможно, вызывая ошибку "strict mode violation". В этом случае вы можете использовать Locator.first(), чтобы соответствовать только одному из них.
Locator newEmail = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("New"));
Locator dialog = page.getByText("Confirm security settings");
assertThat(newEmail.or(dialog).first()).isVisible();
if (dialog.isVisible())
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Dismiss")).click();
newEmail.click();
Сопоставление только видимых элементов
Обычно лучше найти более надежный способ уникально идентифицировать элемент, вместо проверки видимости.
Рассмотрим страницу с двумя кнопками, первая из которых невидима, а вторая видима.
<button style='display: none'>Invisible</button>
<button>Visible</button>
-
Это найдет обе кнопки и вызовет ошибку strictness:
page.locator("button").click();
-
Это найдет только вторую кнопку, потому что она видима, и затем нажмет на нее.
page.locator("button").filter(new Locator.FilterOptions.setVisible(true)).click();
Списки
Подсчет элементов в списке
Вы можете утверждать локаторы, чтобы подсчитать элементы в списке.
Например, рассмотрим следующую структуру DOM:
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
Используйте утверждение count, чтобы убедиться, что в списке 3 элемента.
assertThat(page.getByRole(AriaRole.LISTITEM)).hasCount(3);
Утверждение всего текста в списке
Вы можете утверждать локаторы, чтобы найти весь текст в списке.
Например, рассмотрим следующую структуру DOM:
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
Используйте assertThat(locator).hasText(), чтобы убедиться, что в списке есть текст "apple", "banana" и "orange".
assertThat(page
.getByRole(AriaRole.LISTITEM))
.hasText(new String[] { "apple", "banana", "orange" });
Получение конкретного элемента
Существует множество способов получить конкретный элемент в списке.
Получение по тексту
Используйте метод Page.getByText(), чтобы найти элемент в списке по его текстовому содержимому, а затем нажмите на него.
Например, рассмотрим следующую структуру DOM:
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
Найдите элемент по его текстовому содержимому и нажмите на него.
page.getByText("orange").click();
Фильтрация по тексту
Используйте Locator.filter(), чтобы найти конкретный элемент в списке.
Например, рассмотрим следующую структуру DOM:
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
Найдите элемент по роли "listitem", затем отфильтруйте по тексту "orange" и нажмите на него.
page.getByRole(AriaRole.LISTITEM)
.filter(new Locator.FilterOptions().setHasText("orange"))
.click();
Получение по тестовому идентификатору
Используйте метод Page.getByTestId(), чтобы найти элемент в списке. Возможно, вам потребуется изменить html и добавить тестовый идентификатор, если у вас его еще нет.
Например, рассмотрим следующую структуру DOM:
- apple
- banana
- orange
<ul>
<li data-testid='apple'>apple</li>
<li data-testid='banana'>banana</li>
<li data-testid='orange'>orange</li>
</ul>
Найдите элемент по его тестовому идентификатору "orange" и нажмите на него.
page.getByTestId("orange").click();
Получение по порядковому номеру
Если у вас есть список идентичных элементов, и единственный способ отличить их - это порядок, вы можете выбрать конкретный элемент из списка с помощью Locator.first(), Locator.last() или Locator.nth().
Locator banana = page.getByRole(AriaRole.LISTITEM).nth(1);
Однако используйте этот метод с осторожностью. Часто страница может измениться, и локатор будет указывать на совершенно другой элемент, чем вы ожидали. Вместо этого постарайтесь придумать уникальный локатор, который пройдет критерии строгого режима.
Объединение фильтров
Когда у вас есть элементы с различными сходствами, вы можете использовать метод Locator.filter(), чтобы выбрать нужный. Вы также можете объединить несколько фильтров, чтобы сузить выбор.
Например, рассмотрим следующую структуру DOM:
- John
- Mary
- John
- Mary
<ul>
<li>
<div>John</div>
<div><button>Say hello</button></div>
</li>
<li>
<div>Mary</div>
<div><button>Say hello</button></div>
</li>
<li>
<div>John</div>
<div><button>Say goodbye</button></div>
</li>
<li>
<div>Mary</div>
<div><button>Say goodbye</button></div>
</li>
</ul>
Чтобы сделать скриншот строки с "Mary" и "Say goodbye":
Locator rowLocator = page.getByRole(AriaRole.LISTITEM);
rowLocator
.filter(new Locator.FilterOptions().setHasText("Mary"))
.filter(new Locator.FilterOptions()
.setHas(page.getByRole(
AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Say goodbye"))))
.screenshot(new Page.ScreenshotOptions().setPath("screenshot.png"));
Теперь у вас должен быть файл "screenshot.png" в корневом каталоге вашего проекта.
Редкие случаи использования
Выполнение действий с каждым элементом в списке
Итерация по элементам:
for (Locator row : page.getByRole(AriaRole.LISTITEM).all())
System.out.println(row.textContent());
Итерация с использованием обычного цикла for:
Locator rows = page.getByRole(AriaRole.LISTITEM);
int count = rows.count();
for (int i = 0; i < count; ++i)
System.out.println(rows.nth(i).textContent());
Оценка на странице
Код внутри Locator.evaluateAll() выполняется на странице, вы можете вызывать любые DOM API там.
Locator rows = page.getByRole(AriaRole.LISTITEM);
Object texts = rows.evaluateAll(
"list => list.map(element => element.textContent)");
Строгость
Локаторы являются строгими. Это означает, что все операции с локаторами, которые подразумевают некоторый целевой элемент DOM, вызовут исключение, если будет найдено более одного элемента. Например, следующий вызов вызовет ошибку, если в DOM несколько кнопок:
Вызывает ошибку, если более одного
page.getByRole(AriaRole.BUTTON).click();
С другой стороны, Playwright понимает, когда вы выполняете операцию с несколькими элементами, поэтому следующий вызов работает отлично, когда локатор разрешается в несколько элементов.
Работает отлично с несколькими элементами
page.getByRole(AriaRole.BUTTON).count();
Вы можете явно отказаться от проверки строгости, указав Playwright, какой элемент использовать, когда совпадают несколько элементов, с помощью Locator.first(), Locator.last() и Locator.nth(). Эти методы не рекомендуются, потому что когда ваша страница изменяется, Playwright может нажать на элемент, который вы не намеревались. Вместо этого следуйте лучшим практикам выше, чтобы создать локатор, который уникально идентифицирует целевой элемент.
Больше локаторов
Для менее часто используемых локаторов, посмотрите руководство по другим локаторам.