Большинство нестабильных 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. Используйте, когда элемент семантически не выражен и добавить нормальную роль или подпись не получается. Это осознанный запасной вариант, а не первый выбор.
Как выбрать нужный локатор
Простой порядок приоритетов:
getByRole— для интерактивных элементов (кнопки, ссылки, поля, чекбоксы);getByLabel/getByPlaceholder— для полей формы;getByText— для проверки появления текста на странице;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.