← назад к разделу

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

Почему CSS и XPath делают тесты хрупкими

Привычный способ найти элемент — по CSS-селектору или XPath:

await page.locator(".btn-primary > span:nth-child(2)").click();
await page.locator("//div[@class='form']//input[2]").fill("text");

Такие селекторы описывают устройство DOM — классы, положение в дереве, индексы. Как только вёрстка меняется (даже без изменения поведения), локатор перестаёт работать.

Кнопка «Оформить заказ» никуда не делась. Она по-прежнему нажимается. Но .btn-primary > span:nth-child(2) её больше не находит — и тест красный. Разработчик тратит время на починку теста вместо работы над фичей.

User-facing локаторы: искать как пользователь

Playwright предлагает другой подход: искать элементы так, как их видит пользователь — по роли, подписи, тексту. Такие локаторы не зависят от вёрстки.

getByRole — по роли и имени

await page.getByRole("button", { name: "Оформить заказ" }).click();
await page.getByRole("textbox", { name: "Email" }).fill("user@example.com");
await page.getByRole("heading", { name: "Корзина" }).isVisible();

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

Это самый устойчивый и рекомендуемый локатор в Playwright. Используйте его в первую очередь.

getByLabel — поля формы по подписи

await page.getByLabel("Email").fill("user@example.com");
await page.getByLabel("Пароль").fill("secret");

Ищет поле ввода по тексту рядом стоящего <label>. Привязан к семантике формы, а не к атрибутам id или name.

getByText — по видимому тексту

await page.getByText("Заказ оформлен").isVisible();
await page.getByText("Нет товаров в корзине").waitFor();

Ищет любой элемент по видимому тексту. Удобно для проверок состояния: появился нужный текст — значит, действие выполнено.

getByPlaceholder — по тексту-подсказке

await page.getByPlaceholder("Введите email").fill("user@example.com");

Подходит для полей, у которых нет видимой подписи, но есть placeholder.

getByTestId — запасной вариант

await page.getByTestId("checkout-button").click();

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

Как выбрать нужный локатор

Простой порядок приоритетов:

  1. getByRole — для интерактивных элементов (кнопки, ссылки, поля, чекбоксы);
  2. getByLabel / getByPlaceholder — для полей формы;
  3. getByText — для проверки появления текста на странице;
  4. getByTestId — когда другие варианты не подходят.

CSS и XPath в Playwright тоже работают (page.locator("css=...")), но они снижают устойчивость тестов. Используйте их только в крайнем случае.

Как сузить поиск: фильтры и цепочки

Если на странице несколько похожих элементов, локаторы можно уточнить.

filter — оставить нужный из нескольких

const orderRow = page.getByRole("row").filter({ hasText: "Кофемолка" });
await orderRow.getByRole("button", { name: "Удалить" }).click();

filter({ hasText }) оставляет только строки таблицы с текстом «Кофемолка», а дальше внутри неё ищем кнопку «Удалить». Это гораздо устойчивее, чем .nth(2) — порядок строк может меняться.

Вложенные локаторы

const form = page.getByRole("form", { name: "Регистрация" });
await form.getByLabel("Email").fill("user@example.com");
await form.getByLabel("Пароль").fill("secret");

Если на странице несколько форм, сначала находим нужную форму, а уже внутри неё ищем поля. Контекст сужает область поиска.

Связь с доступностью

Это не совпадение, что устойчивый локатор — это доступный элемент. getByRole("button", { name: "Оформить заказ" }) находит элемент так же, как его воспринимает скринридер.

Если тест не может найти кнопку по роли и имени — её, скорее всего, не найдёт и вспомогательная технология. Это одновременно проблема теста и дефект доступности.

Хорошие локаторы и доступная разметка растут из одного корня: семантического HTML. Кнопка должна быть <button>, поле должно иметь <label>, заголовки — использовать <h1><h6>.

Коротко

  • CSS и XPath привязывают тест к устройству DOM, а не к поведению — поэтому они делают тесты нестабильными.
  • Playwright предлагает user-facing локаторы: getByRole, getByLabel, getByText, getByPlaceholder, getByTestId.
  • Главный локатор — getByRole: ищет по семантической роли и имени, переживает рефакторинг вёрстки.
  • getByTestId — запасной вариант для случаев, когда семантика не помогает.
  • Для уточнения поиска используют filter() и вложенные локаторы — вместо хрупких индексов.
  • Устойчивый локатор = доступный элемент: если скринридер его не найдёт, тест тоже не найдёт.

Что почитать дальше

  • Нестабильные тесты: причины и лечение — локаторы — главная причина нестабильности.
  • Структура тестов: fixtures и page objects — как организовать локаторы в растущем наборе тестов.
  • Доступность — семантическая разметка как основа и хороших тестов, и хорошего UX.