Локаторы в UI-автотестах

Локатор (locator) - описание того, как автотест пользовательского интерфейса находит элемент: кнопку, на которую он нажимает, поле, которое заполняет, сообщение, которое проверяет. Локатор состоит из стратегии и значения: «кнопка с доступным именем Sign in», «элемент с атрибутом data-testid, равным cart-count», «CSS-селектор form > button.primary». Тест обращается к элементам только через локаторы, поэтому именно в локаторах тест зависит от разметки страницы.

Стандарт автоматизации браузера W3C WebDriver определяет пять стратегий поиска; его первый уровень (Level 1) стал рекомендацией W3C 5 июня 2018 года, а Level 2 - рабочий черновик (версия от 2 июля 2026 года). Selenium поддерживает восемь традиционных стратегий, среди них id, CSS-селектор и XPath. Playwright, начиная с версии 1.27, и Testing Library предлагают локаторы по атрибутам, которые видит пользователь: роли, подписи, тексту. Документация Playwright рекомендует их вместо CSS и XPath. Роли и доступные имена, на которые они опираются, определяет WAI-ARIA 1.2 - рекомендация W3C от 6 июня 2023 года.

Какую задачу решают локаторы

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

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

typescript
page.locator('#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input');

Любой новый div-обёртка или переименованный класс ломает этот локатор, хотя само поле ввода по-прежнему на странице и работает. Выбор вида локатора определяет, какие изменения страницы ломают тест, а какие нет.

Виды локаторов

Примеры в этом разделе относятся к одной форме входа.

html
<form>
  <h1>Sign in</h1>
  <label for="email">Email</label>
  <input id="email" type="email" placeholder="name@example.com">
  <label for="password">Password</label>
  <input id="password" type="password">
  <img src="/logo.svg" alt="Shop logo">
  <span title="Items in cart" data-testid="cart-count">0</span>
  <button type="submit" class="btn btn-primary">Sign in</button>
</form>

По роли

Локатор по роли находит элемент по его роли ARIA и доступному имени (accessible name). Роль говорит, чем элемент является: button, link, textbox, checkbox, heading. У элементов HTML есть неявные роли: по спецификации W3C ARIA in HTML элемент button имеет роль button, элемент a с href - роль link, а h1-h6 - роль heading. WAI-ARIA определяет доступное имя как имя элемента пользовательского интерфейса; оно может браться из видимого текста, подписи или атрибута, например aria-label.

В Playwright page.getByRole('button', { name: 'Sign in' }) находит кнопку формы. По умолчанию name сравнивается как подстрока без учёта регистра; параметр exact: true требует совпадения всей строки с учётом регистра. Другие параметры фильтруют по состоянию и уровню: checked, disabled, expanded, pressed, selected, level. Документация Playwright отмечает, что локаторы по роли следуют спецификациям W3C для ролей, атрибутов ARIA и доступных имён, но не заменяют аудит доступности.

По подписи

Локатор по подписи находит поле формы по тексту его подписи (label). page.getByLabel('Email') в Playwright сравнивает текст связанного элемента label или элемента, на который ссылается aria-labelledby, либо значение aria-label. В Testing Library аналог называется getByLabelText; его рекомендуют для полей форм, потому что пользователи находят поля по подписям.

По плейсхолдеру

Локатор по плейсхолдеру находит поле ввода по атрибуту placeholder: page.getByPlaceholder('name@example.com'). Testing Library считает его запасным вариантом, потому что плейсхолдер не заменяет подпись.

По тексту

Локатор по тексту находит элемент по его текстовому содержимому: page.getByText('Sign in'). При сравнении текста Playwright нормализует пробелы, даже при точном совпадении, и принимает подстроку, целую строку или регулярное выражение. Документация рекомендует локаторы по тексту для неинтерактивных элементов, таких как div, span и p, а для интерактивных, таких как button, a и input, - локаторы по роли. В форме выше getByText('Sign in') совпадает и с заголовком, и с кнопкой. Локаторы Playwright строгие: действие над локатором, который совпадает больше чем с одним элементом, вызывает исключение. Локатор по роли разделяет их: getByRole('heading', { name: 'Sign in' }) и getByRole('button', { name: 'Sign in' }).

По альтернативному тексту и title

Локатор по альтернативному тексту (alt text) находит элемент, обычно изображение, по его текстовой альтернативе: page.getByAltText('Shop logo'). Локатор по title находит элемент по атрибуту title: page.getByTitle('Items in cart'). Testing Library предупреждает, что программы чтения с экрана читают title непоследовательно и по умолчанию он не виден.

По test id

Test id - атрибут, который добавляют в разметку только для того, чтобы тесты могли найти элемент. page.getByTestId('cart-count') в Playwright по умолчанию ищет атрибут data-testid. Другое имя атрибута задаётся параметром testIdAttribute в конфигурации тестов или вызовом selectors.setTestIdAttribute().

typescript
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    testIdAttribute: 'data-pw',
  },
});

Документация Playwright называет тестирование по test id самым устойчивым способом: test id переживает изменения текста и роли. Она же отмечает, что test id не виден пользователю: тест, который находит элемент по test id, не проверяет то, что видит пользователь.

CSS-селекторы

CSS-селектор находит элементы по тегу, id, классам, атрибутам и положению в документе: button.btn-primary, #email, form > button. В Playwright CSS передаётся в page.locator() с префиксом css= или без него. CSS-селектором можно выразить всё, что другие виды выражают через атрибуты, например [data-testid='cart-count'] или [placeholder='name@example.com']. Устойчивость зависит от того, на что он ссылается: id или атрибут test id стабильны, классы и положение - нет.

XPath

XPath - язык выбора частей XML-документа; его версия 1.0 - рекомендация W3C от 16 ноября 1999 года. Выражение XPath выбирает по структуре, атрибутам и тексту: //button[normalize-space()='Sign in'], //form/input[2]. XPath умеет переходить от элемента к родителю и соседям. В Playwright XPath передаётся в page.locator() с префиксом xpath= или начинаясь с //. Документация Playwright отмечает, что XPath не проникает в теневые корни (shadow roots).

Локаторы в WebDriver и Selenium

Спецификация W3C WebDriver определяет пять стратегий поиска. Клиент передаёт стратегию и значение команде Find Element, POST /session/{session id}/element.

Ключевое слово Что находит
css selector Элементы, подходящие под CSS-селектор
link text Ссылки, видимый текст которых равен значению
partial link text Ссылки, видимый текст которых содержит значение
tag name Элементы с заданным именем тега
xpath Элементы, выбранные выражением XPath

Selenium поддерживает восемь традиционных стратегий: пять перечисленных и id, name, class name. В привязке для Python это константы класса By, и их строковые значения совпадают с ключевыми словами: By.CSS_SELECTOR - это "css selector", By.ID - "id". Стратегия class name не принимает составные имена классов.

python
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://shop.example.com/login")

driver.find_element(By.ID, "email").send_keys("user@example.com")
driver.find_element(By.CSS_SELECTOR, "input[type='password']").send_keys("secret")
driver.find_element(By.XPATH, "//button[normalize-space()='Sign in']").click()

Ни одна из восьми стратегий не ищет элемент по роли ARIA и доступному имени, поэтому в Selenium похожий на роль локатор выражается через CSS или XPath. Документация Selenium о рекомендуемых практиках даёт свой порядок: предпочтителен id, если он есть, уникален и предсказуем; иначе - хорошо написанный CSS-селектор; XPath работает не хуже CSS, но его синтаксис сложен и часто труден для отладки. Локатор по имени тега она называет опасным, потому что на странице обычно много элементов с одним тегом, и отмечает, что стратегии по тексту ссылки работают только со ссылками.

Локаторы в Playwright и Testing Library

Playwright перечисляет рекомендуемые встроенные локаторы в таком порядке: getByRole, getByText, getByLabel, getByPlaceholder, getByAltText, getByTitle, getByTestId. Его лучшие практики советуют отдавать приоритет атрибутам, которые видит пользователь, и явным контрактам, а CSS и XPath не рекомендуют, потому что DOM часто меняется. Тест формы входа использует только рекомендуемые локаторы:

typescript
import { test, expect } from '@playwright/test';

test('user signs in', async ({ page }) => {
  await page.goto('https://shop.example.com/login');

  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('secret');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page.getByRole('heading', { name: 'My orders' })).toBeVisible();
  await expect(page.getByTestId('cart-count')).toHaveText('0');
});

Локаторы можно соединять в цепочки и фильтровать, чтобы сузить поиск до части страницы. page.getByRole('listitem').filter({ hasText: 'Product 2' }) выбирает элемент списка, который содержит текст, а следующий getByRole('button', { name: 'Add to cart' }) выбирает кнопку внутри него.

Testing Library следует руководящему принципу: «The more your tests resemble the way your software is used, the more confidence they can give you.» - чем больше тесты похожи на то, как пользуются программой, тем больше уверенности они дают. Её документация делит запросы на три группы по приоритету. Таблица сравнивает два порядка.

Приоритет Testing Library Playwright
1 Доступные всем: getByRole, getByLabelText, getByPlaceholderText, getByText, getByDisplayValue getByRole, getByText, getByLabel, getByPlaceholder
2 Семантические: getByAltText, getByTitle getByAltText, getByTitle
3 Test id: getByTestId getByTestId
Не в списке CSS и XPath CSS и XPath, не рекомендуются

Оба порядка ставят роль первой, а test id последним среди собственных методов. Различие - внутри первой группы: Testing Library ставит подпись перед текстом, Playwright - текст перед подписью.

Устойчивость к изменениям

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

Вид локатора Устойчивость к изменениям разметки Читаемость Зависит от текстов
Роль и доступное имя Высокая Высокая Да, через имя
Подпись Высокая Высокая Да
Плейсхолдер Средняя Высокая Да
Текст Средняя Высокая Да
Альтернативный текст, title Средняя Средняя Да
Test id Высокая Средняя Нет
Id Высокая, если id стабилен Средняя Нет
CSS по классам и структуре Низкая Низкая Нет
XPath по структуре Низкая Низкая Нет
XPath или CSS по тексту или атрибуту Средняя Низкая Зависит от атрибута

Оценка следует из того, на что ссылается каждый локатор. Роль и доступное имя меняются, только когда элемент становится другим элементом управления или получает другое имя, - такое изменение замечает и пользователь. Test id меняется, только если его кто-то правит, но пользователю он не виден, поэтому тест, который на него опирается, не заметит, что кнопка потеряла подпись. Классы и положение меняются при рефакторинге и правке стилей без всякого изменения поведения.

Локатор, который зависит от текстов, ломается при правке текста. Это желательно, когда текст - часть того, что проверяет тест, и нежелательно, когда текст - только способ найти элемент. Id, которые генерируются во время работы, например из счётчика, делают локаторы по id неустойчивыми: значение может отличаться между загрузками страницы.

Как выбрать локатор

Рекомендации Playwright, Testing Library и Selenium сводятся к общему порядку предпочтения:

  1. Роль и доступное имя - для интерактивных элементов: кнопок, ссылок, полей, флажков.
  2. Подпись - для полей форм; текст - для неинтерактивных элементов.
  3. Плейсхолдер, альтернативный текст, title - когда у элемента нет роли, подписи или текста.
  4. Test id - когда видимых пользователю атрибутов нет, они неоднозначны или меняются по причинам, не связанным с тестом.
  5. CSS или XPath - когда ничего другого не подходит, например в сторонней разметке, которую нельзя изменить; лучше по стабильным атрибутам, чем по структуре.

Локатор, который совпадает с несколькими элементами, сужают цепочкой от уникального контейнера, а не по положению. Если локатор по роли написать нельзя, это сигнал о самой странице: элементом без доступного имени трудно пользоваться и с программой чтения с экрана. Генератор кода Playwright, npx playwright codegen, выбирает локаторы по тому же приоритету: роль, текст и test id.

Ожидание элементов

Локатор описывает элемент, которого может ещё не быть. После нажатия страница отправляет запрос, получает ответ и отрисовывает новое содержимое, и тест должен этого дождаться. Способ ожидания - одна из причин тестов, которые падают без дефекта; такие тесты описаны в статье Нестабильные (flaky) тесты.

Автоожидание (auto-waiting) в Playwright означает, что перед каждым действием инструмент ждёт, пока элемент пройдёт проверки готовности к действию (actionability checks). Для click это проверки: элемент видим, стабилен, получает события и включён; для fill - видим, включён и редактируем. Если проверки не проходят за время тайм-аута, действие завершается ошибкой TimeoutError. Локатор Playwright разрешается в элемент при каждом использовании. Проверки вида expect(locator).toBeVisible() ждут выполнения условия, а locator.isVisible() возвращает результат сразу.

В Selenium два механизма. Неявное ожидание (implicit wait) - глобальная настройка на всю сессию: каждый поиск элемента повторяется до тайм-аута; значение по умолчанию 0, поэтому отсутствующий элемент сразу вызывает ошибку. Явное ожидание (explicit wait) - цикл в коде теста, который опрашивает конкретное условие, пока оно не станет истинным или не истечёт тайм-аут. Документация Selenium предупреждает, что смешивать их нельзя: время ожидания становится непредсказуемым.

python
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait

wait = WebDriverWait(driver, timeout=5)
heading = wait.until(
    lambda d: d.find_element(By.XPATH, "//h1[normalize-space()='My orders']")
)
assert heading.is_displayed()

WebDriverWait.until вызывает функцию, пока она не вернёт истинное значение, и выбрасывает TimeoutException, когда истекает тайм-аут; по умолчанию между попытками он игнорирует NoSuchElementException. WebElement, найденный Selenium, ссылается на один конкретный элемент: если страница перерисует его, ссылка устаревает, и элемент нужно найти заново. В Testing Library похожее разделение: запросы getBy возвращают результат сразу, запросы findBy возвращают промис и повторяют поиск до тайм-аута по умолчанию 1000 мс.

Локаторы и локализация

Локаторы по роли, подписи, плейсхолдеру, тексту, альтернативному тексту и title зависят от текстов интерфейса, а тексты в разных языках разные. Тест с getByRole('button', { name: 'Sign in' }) упадёт на немецкой версии страницы, где у кнопки другое имя. Собственный пример Playwright для test id показывает этот случай: кнопка с текстом Itinéraire и атрибутом data-testid="directions", которую getByTestId('directions') находит независимо от языка.

У набора, который выполняется на нескольких локалях, есть несколько вариантов:

  • одна локаль для большинства тестов: Playwright задаёт локаль браузера параметром locale в конфигурации, например locale: 'en-GB';
  • тексты из ресурсов перевода: тест берёт ожидаемое имя из тех же файлов перевода, что и приложение, и локатор следует за переводом;
  • test id для элементов, текст которых меняется: локатор не зависит от языка, а тексты проверяет отдельная проверка;
  • регулярные выражения в name или getByText, когда вариантов немного и они известны.

Выбор зависит и от того, что проверяет тест. Тест самого перевода должен опираться на тексты; тест бизнес-сценария - не обязан.

Локаторы в репозитории объектов

Когда один элемент используется во многих тестах, его локатор записывают один раз и обращаются к нему по имени. Изменение разметки тогда исправляется в одном месте, а не в каждом тесте. Два способа так организовать локаторы - объекты страниц (page objects) и репозиторий объектов со структурой «страницы - элементы» - описаны в статье Репозиторий объектов и Page Object. Каким уровням тестов локаторы нужны вообще, объясняет статья Уровни автоматизации и пирамида тестов.

Локатор, селектор и ссылка на элемент

Три слова обозначают связанные, но разные вещи.

Селектор Локатор Ссылка на элемент
Что это Строка в синтаксисе CSS или XPath Стратегия и значение для поиска элемента Ссылка на один найденный элемент
Пример button.btn-primary getByRole('button', { name: 'Sign in' }) WebElement в Selenium, ElementHandle в Playwright
Когда разрешается При использовании При каждом использовании (Playwright) Один раз, при поиске
После перерисовки Не применимо Находит новый элемент Устаревает

Селектор - одно из возможных значений локатора: CSS- или XPath-локатор содержит селектор, локатор по роли - нет. Документация Playwright прямо формулирует разницу между двумя последними: ElementHandle указывает на конкретный элемент, а Locator содержит логику того, как элемент получить. Использовать ElementHandle она не рекомендует и предлагает вместо него локаторы и веб-ориентированные проверки (web-first assertions).

Термины

Термин Значение
Локатор (locator) Описание того, как UI-автотест находит элемент: стратегия и значение
Стратегия поиска (location strategy) Способ найти элемент: по роли, подписи, тексту, test id, CSS, XPath и другие
Доступное имя (accessible name) Имя элемента пользовательского интерфейса, которое получают вспомогательные технологии
Test id Атрибут, который добавляют в разметку только для того, чтобы тесты могли найти элемент, по умолчанию data-testid
Автоожидание (auto-waiting) Ожидание инструментом перед каждым действием, пока элемент не пройдёт проверки готовности к действию
Неявное ожидание (implicit wait) Глобальная настройка сессии, при которой каждый поиск элемента повторяется до тайм-аута
Явное ожидание (explicit wait) Цикл в коде теста, который опрашивает конкретное условие, пока оно не станет истинным или не истечёт тайм-аут
Устаревшая ссылка на элемент (stale element reference) Ссылка на элемент, который был найден раньше, но больше не находится в документе

Источники

Серия

Автоматизация тестирования
  1. Уровни автоматизации и пирамида тестов
  2. Локаторы в UI-автотестахТекущая
  3. Репозиторий объектов и Page Object
  4. Нестабильные (flaky) тесты
  5. Автотесты в CI

Статьи Wiki пишет и поддерживает команда ComplexQA.