Локаторы
Введение
[Локаторы] являются центральной частью автоматического ожидания и возможности повторных попыток в Playwright. Вкратце, локаторы представляют собой способ найти элемент(ы) на странице в любой момент времени.
Быстрый гид
Это рекомендуемые встроенные локаторы.
- page.getByRole() для поиска по явным и неявным атрибутам доступности.
- page.getByText() для поиска по текстовому содержимому.
- page.getByLabel() для поиска элемента управления формой по тексту связанной метки.
- page.getByPlaceholder() для поиска ввода по заполнителю.
- page.getByAltText() для поиска элемента, обычно изображения, по его текстовой альтернативе.
- page.getByTitle() для поиска элемента по его атрибуту title.
- page.getByTestId() для поиска элемента на основе его атрибута
data-testid
(другие атрибуты могут быть настроены).
await page.getByLabel('User Name').fill('John');
await page.getByLabel('Password').fill('secret-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Welcome, John!')).toBeVisible();
Поиск элементов
Playwright предоставляет несколько встроенных локаторов. Чтобы сделать тесты устойчивыми, мы рекомендуем отдавать приоритет атрибутам, ориентированным на пользователя, и явным контрактам, таким как page.getByRole().
Например, рассмотрим следующую структуру DOM.
<button>Sign in</button>
Найдите элемент по его роли button
с именем "Sign in".
await page.getByRole('button', { name: 'Sign in' }).click();
Используйте генератор кода для создания локатора, а затем отредактируйте его по своему усмотрению.
Каждый раз, когда локатор используется для действия, актуальный элемент DOM находится на странице. В приведенном ниже фрагменте кода базовый элемент DOM будет найден дважды, один раз перед каждым действием. Это означает, что если DOM изменится между вызовами из-за повторного рендеринга, будет использован новый элемент, соответствующий локатору.
const locator = page.getByRole('button', { name: 'Sign in' });
await locator.hover();
await locator.click();
Обратите внимание, что все методы, создающие локатор, такие как page.getByLabel(), также доступны в классах [Locator] и [FrameLocator], так что вы можете их объединять и итеративно сужать ваш локатор.
const locator = page
.frameLocator('#my-frame')
.getByRole('button', { name: 'Sign in' });
await locator.click();
Поиск по роли
Локатор page.getByRole() отражает, как пользователи и вспомогательные технологии воспринимают страницу, например, является ли какой-то элемент кнопкой или флажком. При поиске по роли обычно следует также передавать доступное имя, чтобы локатор точно указывал на нужный элемент.
Например, рассмотрим следующую структуру DOM.
Sign up
<h3>Sign up</h3>
<label>
<input type="checkbox" /> Subscribe
</label>
<br/>
<button>Submit</button>
Вы можете найти каждый элемент по его неявной роли:
await expect(page.getByRole('heading', { name: 'Sign up' })).toBeVisible();
await page.getByRole('checkbox', { name: 'Subscribe' }).check();
await page.getByRole('button', { name: /submit/i }).click();
Локаторы ролей включают кнопки, флажки, заголовки, ссылки, списки, таблицы и многое другое и следуют спецификациям W3C для роли ARIA, атрибутов ARIA и доступного имени. Обратите внимание, что многие HTML-элементы, такие как <button>
, имеют неявно определенную роль, которая распознается локатором роли.
Обратите внимание, что локаторы ролей не заменяют аудиты доступности и тесты на соответствие, но дают раннюю обратную связь о руководствах ARIA.
Мы рекомендуем отдавать приоритет локаторам ролей для поиска элементов, так как это наиболее близкий способ к тому, как пользователи и вспомогательные технологии воспринимают страницу.
Поиск по метке
Большинство элементов управления формами обычно имеют выделенные метки, которые можно удобно использовать для взаимодействия с формой. В этом случае вы можете найти элемент управления по его связанной метке, используя page.getByLabel().
Например, рассмотрим следующую структуру DOM.
<label>Password <input type="password" /></label>
Вы можете заполнить ввод после его поиска по тексту метки:
await page.getByLabel('Password').fill('secret');
Используйте этот локатор при поиске полей формы.
Поиск по заполнителю
Вводы могут иметь атрибут заполнителя, чтобы подсказать пользователю, какое значение следует ввести. Вы можете найти такой ввод, используя page.getByPlaceholder().
Например, рассмотрим следующую структуру DOM.
<input type="email" placeholder="name@example.com" />
Вы можете заполнить ввод после его поиска по тексту заполнителя:
await page
.getByPlaceholder('name@example.com')
.fill('playwright@microsoft.com');
Используйте этот локатор при поиске элементов формы, у которых нет меток, но есть текст заполнителя.
Поиск по тексту
Найдите элемент по тексту, который он содержит. Вы можете сопоставить по подстроке, точной строке или регулярному выражению, используя page.getByText().
Например, рассмотрим следующую структуру DOM.
<span>Welcome, John</span>
Вы можете найти элемент по тексту, который он содержит:
await expect(page.getByText('Welcome, John')).toBeVisible();
Установите точное совпадение:
await expect(page.getByText('Welcome, John', { exact: true })).toBeVisible();
Совпадение с регулярным выражением:
await expect(page.getByText(/welcome, [A-Za-z]+$/i)).toBeVisible();
Совпадение по тексту всегда нормализует пробелы, даже при точном совпадении. Например, оно превращает несколько пробелов в один, превращает разрывы строк в пробелы и игнорирует начальные и конечные пробелы.
Мы рекомендуем использовать локаторы текста для поиска неинтерактивных элементов, таких как div
, span
, p
и т.д. Для интерактивных элементов, таких как button
, a
, input
и т.д., используйте локаторы ролей.
Вы также можете фильтровать по тексту, что может быть полезно при попытке найти конкретный элемент в списке.
Поиск по альтернативному тексту
Все изображения должны иметь атрибут alt
, который описывает изображение. Вы можете найти изображение на основе текстовой альтернативы, используя page.getByAltText().
Например, рассмотрим следующую структуру DOM.
<img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />
Вы можете щелкнуть по изображению после его поиска по текстовой альтернативе:
await page.getByAltText('playwright logo').click();
Используйте этот локатор, когда ваш элемент поддерживает альтернативный текст, такой как элементы img
и area
.
Поиск по заголовку
Найдите элемент с совпадающим атрибутом title, используя page.getByTitle().
Например, рассмотрим следующую структуру DOM.
<span title='Issues count'>25 issues</span>
Вы можете проверить количество проблем после его поиска по тексту заголовка:
await expect(page.getByTitle('Issues count')).toHaveText('25 issues');
Используйте этот локатор, когда ваш элемент имеет атрибут title
.
Поиск по тестовому идентификатору
Тестирование по тестовым идентификаторам является наиболее устойчивым способом тестирования, так как даже если ваш текст или роль атрибута изменится, тест все равно пройдет. QA и разработчики должны определять явные тестовые идентификаторы и запрашивать их с помощью page.getByTestId(). Однако тестирование по тестовым идентификаторам не ориентировано на пользователя. Если роль или текстовое значение важны для вас, рассмотрите возможность использования локаторов, ориентированных на пользователя, таких как локаторы ролей и локаторы текста.
Например, рассмотрим следующую структуру DOM.
<button data-testid="directions">Itinéraire</button>
Вы можете найти элемент по его тестовому идентификатору:
await page.getByTestId('directions').click();
Установите пользовательский атрибут тестового идентификатора
По умолчанию, page.getByTestId() будет находить элементы на основе атрибута data-testid
, но вы можете настроить его в конфигурации теста или вызвав selectors.setTestIdAttribute().
Установите тестовый идентификатор для использования пользовательского атрибута данных в ваших тестах.
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
testIdAttribute: 'data-pw'
}
});
В вашем HTML теперь вы можете использовать data-pw
в качестве тестового идентификатора вместо стандартного data-testid
.
<button data-pw="directions">Itinéraire</button>
А затем найдите элемент, как вы обычно это делаете:
await page.getByTestId('directions').click();
Поиск по CSS или XPath
Если вам абсолютно необходимо использовать локаторы CSS или XPath, вы можете использовать page.locator() для создания локатора, который принимает селектор, описывающий, как найти элемент на странице. Playwright поддерживает селекторы CSS и XPath и автоматически определяет их, если вы опускаете префикс css=
или xpath=
.
await page.locator('css=button').click();
await page.locator('xpath=//button').click();
await page.locator('button').click();
await page.locator('//button').click();
Селекторы XPath и CSS могут быть привязаны к структуре DOM или реализации. Эти селекторы могут сломаться, когда структура DOM изменится. Длинные цепочки CSS или XPath ниже являются примером плохой практики, которая приводит к нестабильным тестам:
await page.locator(
'#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input'
).click();
await 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 roots.
- Закрытые shadow roots не поддерживаются.
Рассмотрим следующий пример с пользовательским веб-компонентом:
<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 root вообще не присутствовал.
Чтобы щелкнуть <div>Details</div>
:
await 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>
:
await page.locator('x-details', { hasText: '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":
await expect(page.locator('x-details')).toContainText('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(). Он будет искать определенную строку где-то внутри элемента, возможно, в дочернем элементе, без учета регистра. Вы также можете передать регулярное выражение.
await page
.getByRole('listitem')
.filter({ hasText: 'Product 2' })
.getByRole('button', { name: 'Add to cart' })
.click();
Используйте регулярное выражение:
await page
.getByRole('listitem')
.filter({ hasText: /Product 2/ })
.getByRole('button', { name: 'Add to cart' })
.click();
Фильтрация по отсутствию текста
Альтернативно, фильтрация по отсутствию текста:
// 5 товаров в наличии
await expect(page.getByRole('listitem').filter({ hasNotText: 'Out of stock' })).toHaveCount(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>
await page
.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) })
.getByRole('button', { name: 'Add to cart' })
.click();
Мы также можем проверить карточку продукта, чтобы убедиться, что она только одна:
await expect(page
.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) }))
.toHaveCount(1);
Фильтрующий локатор должен быть относительным к исходному локатору и запрашивается, начиная с совпадения исходного локатора, а не с корня документа. Поэтому следующее не сработает, потому что фильтрующий локатор начинает совпадать с элементом списка <ul>
, который находится за пределами элемента списка <li>
, совпадающего с исходным локатором:
// ✖ НЕПРАВИЛЬНО
await expect(page
.getByRole('listitem')
.filter({ has: page.getByRole('list').getByText('Product 2') }))
.toHaveCount(1);
Фильтрация по отсутствию дочернего/потомка
Мы также можем фильтровать по отсутствию совпадающего элемента внутри.
await expect(page
.getByRole('listitem')
.filter({ hasNot: page.getByText('Product 2') }))
.toHaveCount(1);
Обратите внимание, что внутренний локатор совпадает, начиная с внешнего, а не с корня документа.
Операторы локаторов
Совпадение внутри локатора
Вы можете объединять методы, создающие локатор, такие как page.getByText() или locator.getByRole(), чтобы сузить поиск до определенной части страницы.
В этом примере мы сначала создаем локатор, называемый product, находя его роль listitem
. Затем мы фильтруем по тексту. Мы можем снова использовать локатор product, чтобы получить по роли кнопку и нажать ее, а затем использовать утверждение, чтобы убедиться, что есть только один продукт с текстом "Product 2".
const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });
await product.getByRole('button', { name: 'Add to cart' }).click();
await expect(product).toHaveCount(1);
Вы также можете объединить два локатора вместе, например, чтобы найти кнопку "Сохранить" внутри определенного диалога:
const saveButton = page.getByRole('button', { name: 'Save' });
// ...
const dialog = page.getByTestId('settings-dialog');
await dialog.locator(saveButton).click();
Совпадение двух локаторов одновременно
Метод locator.and() сужает существующий локатор, совпадая с дополнительным локатором. Например, вы можете объединить page.getByRole() и page.getByTitle(), чтобы совпадать как по роли, так и по заголовку.
const button = page.getByRole('button').and(page.getByTitle('Subscribe'));
Совпадение одного из двух альтернативных локаторов
Если вы хотите нацелиться на один из двух или более элементов, и вы не знаете, какой из них это будет, используйте locator.or(), чтобы создать локатор, который совпадает с любым из альтернатив.
Например, рассмотрим сценарий, где вы хотите нажать на кнопку "Новое письмо", но иногда вместо этого появляется диалог настроек безопасности. В этом случае вы можете подождать либо кнопку "Новое письмо", либо диалог и действовать соответственно.
Если и кнопка "Новое письмо", и диалог безопасности появляются на экране, локатор "или" будет совпадать с обоими из них, возможно, вызывая ошибку "нарушение строгого режима". В этом случае вы можете использовать locator.first(), чтобы совпадать только с одним из них.
const newEmail = page.getByRole('button', { name: 'New' });
const dialog = page.getByText('Confirm security settings');
await expect(newEmail.or(dialog).first()).toBeVisible();
if (await dialog.isVisible())
await page.getByRole('button', { name: 'Dismiss' }).click();
await newEmail.click();
Совпадение только видимых элементов
Обычно лучше найти более надежный способ уникально идентифицировать элемент вместо проверки видимости.
Рассмотрим страницу с двумя кнопками, первая из которых невидима, а вторая видима.
<button style='display: none'>Invisible</button>
<button>Visible</button>
-
Это найдет обе кнопки и вызовет ошибку нарушения строгого режима:
await page.locator('button').click();
-
Это найдет только вторую кнопку, потому что она видима, и затем нажмет на нее.
await page.locator('button').filter({ visible: true }).click();
Списки
Подсчет элементов в списке
Вы можете утверждать локаторы, чтобы подсчитать элементы в списке.
Например, рассмотрим следующую структуру DOM:
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
Используйте утверждение count, чтобы убедиться, что в списке 3 элемента.
await expect(page.getByRole('listitem')).toHaveCount(3);
Утверждение всего текста в списке
Вы можете утверждать локаторы, чтобы найти весь текст в списке.
Например, рассмотрим следующую структуру DOM:
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
Используйте expect(locator).toHaveText(), чтобы убедиться, что в списке есть текст "apple", "banana" и "orange".
await expect(page
.getByRole('listitem'))
.toHaveText(['apple', 'banana', 'orange']);
Получение конкретного элемента
Существует множество способов получить конкретный элемент в списке.
Получение по тексту
Используйте метод page.getByText(), чтобы найти элемент в списке по его текстовому содержимому, а затем нажмите на него.
Например, рассмотрим следующую структуру DOM:
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
Найдите элемент по его текстовому содержимому и нажмите на него.
await page.getByText('orange').click();
Фильтрация по тексту
Используйте locator.filter(), чтобы найти конкретный элемент в списке.
Например, рассмотрим следующую структуру DOM:
- apple
- banana
- orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
Найдите элемент по роли "listitem", затем отфильтруйте по тексту "orange" и нажмите на него.
await page
.getByRole('listitem')
.filter({ hasText: '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" и нажмите на него.
await page.getByTestId('orange').click();
Получение по n-му элементу
Если у вас есть список идентичных элементов, и единственный способ отличить их - это порядок, вы можете выбрать конкретный элемент из списка с помощью locator.first(), locator.last() или locator.nth().
const banana = await page.getByRole('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":
const rowLocator = page.getByRole('listitem');
await rowLocator
.filter({ hasText: 'Mary' })
.filter({ has: page.getByRole('button', { name: 'Say goodbye' }) })
.screenshot({ path: 'screenshot.png' });
Теперь у вас должен быть файл "screenshot.png" в корневом каталоге вашего проекта.
Редкие случаи использования
Сделать что-то с каждым элементом в списке
Итерация элементов:
for (const row of await page.getByRole('listitem').all())
console.log(await row.textContent());
Итерация с использованием обычного цикла for:
const rows = page.getByRole('listitem');
const count = await rows.count();
for (let i = 0; i < count; ++i)
console.log(await rows.nth(i).textContent());
Оценка на странице
Код внутри locator.evaluateAll() выполняется на странице, вы можете вызывать любые DOM API там.
const rows = page.getByRole('listitem');
const texts = await rows.evaluateAll(
list => list.map(element => element.textContent));
Строгость
Локаторы строгие. Это означает, что все операции с локаторами, которые подразумевают некоторый целевой элемент DOM, вызовут исключение, если будет найдено более одного элемента. Например, следующий вызов вызовет ошибку, если в DOM будет несколько кнопок:
Вызывает ошибку, если более одного
await page.getByRole('button').click();
С другой стороны, Playwright понимает, когда вы выполняете операцию с несколькими элементами, поэтому следующий вызов работает отлично, когда локатор разрешается в несколько элементов.
Работает отлично с несколькими элементами
await page.getByRole('button').count();
Вы можете явно отказаться от проверки строгости, указав Playwright, какой элемент использовать, когда совпадает несколько элементов, через locator.first(), locator.last() и locator.nth(). Эти методы не рекомендуются, потому что когда ваша страница изменяется, Playwright может нажать на элемент, который вы не намеревались. Вместо этого следуйте лучшим практикам выше, чтобы создать локатор, который уникально идентифицирует целевой элемент.
Больше локаторов
Для менее часто используемых локаторов ознакомьтесь с руководством по другим локаторам.