Цепочки и фильтрация локаторов

Когда на странице много похожих элементов, локаторы нужно уточнять — сужать поиск до одного нужного.

Фильтрация локатора — это уточнение, какой именно из похожих элементов нам нужен: по тексту внутри, по вложенности или по позиции.

Проблема: много одинаковых элементов

Представьте список товаров, где у каждого есть кнопка «В корзину». Простой локатор getByRole('button', { name: 'В корзину' }) найдёт сразу несколько кнопок — Playwright не поймёт, какую кликать, и тест упадёт с ошибкой «strict mode violation» (нашлось больше одного элемента). Это не баг, а защита: лучше явная ошибка, чем клик по случайной кнопке.

// ошибка, если таких кнопок несколько
await page.getByRole('button', { name: 'В корзину' }).click();

Нужно сузить поиск. Есть несколько способов.

Вложенность: ищем внутри родителя

Если у каждого товара есть контейнер с названием, можно сначала найти нужный товар, а внутри него — кнопку. Локаторы соединяются цепочкой, и поиск второго идёт уже только внутри первого.

// карточка товара «Ноутбук», внутри неё — кнопка
await page
  .getByRole('listitem')
  .filter({ hasText: 'Ноутбук' })
  .getByRole('button', { name: 'В корзину' })
  .click();

filter с hasText

filter({ hasText: '...' }) оставляет только те элементы, внутри которых есть нужный текст. Это самый частый способ выбрать нужную строку из списка или таблицы.

const row = page.getByRole('row').filter({ hasText: '[email protected]' });
await expect(row).toBeVisible();
await row.getByRole('button', { name: 'Удалить' }).click();

Можно фильтровать и по наличию вложенного локатораfilter({ has: ... }), и даже исключать через hasNotText:

// строки, в которых есть отмеченный чекбокс
const selected = page
  .getByRole('row')
  .filter({ has: page.getByRole('checkbox', { checked: true }) });

// строки без слова «архив»
const active = page.getByRole('row').filter({ hasNotText: 'архив' });

По позиции: first, last, nth

Когда нужен элемент по порядку, есть first(), last() и nth(i) (нумерация с нуля). Этим пользуются осторожно — позиция менее устойчива, чем текст: если порядок элементов изменится, тест начнёт проверять не то.

await page.getByRole('listitem').first().click();   // первый
await page.getByRole('listitem').last().click();    // последний
await page.getByRole('listitem').nth(2).click();    // третий (индекс 2)

Проверка количества

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

await expect(page.getByRole('listitem')).toHaveCount(5);
ПриёмЗачем
filter({ hasText })выбрать по тексту внутри — предпочтительно
filter({ has })выбрать по вложенному элементу
цепочка локаторовискать внутри родителя
nth/first/lastпо позиции — как крайний случай

Итог

  • Если локатор находит несколько элементов, Playwright требует уточнения (strict mode) — это защита от клика по случайному.
  • filter({ hasText }) — главный способ выбрать нужный элемент по тексту внутри.
  • Цепочка локаторов ищет элемент внутри найденного родителя.
  • first/last/nth — по позиции, применять осторожно.
Проверьте себя
1. Что произойдёт, если локатор найдёт несколько элементов, а вы вызовете click?
AКликнется первый из них
BТест упадёт с ошибкой strict mode — нужно уточнить локатор
CКликнутся все элементы по очереди
DPlaywright выберет случайный элемент
2. Какой способ выбрать нужную строку из списка предпочтительнее?
Anth(i) по индексу
Bfilter({ hasText: '...' }) по тексту внутри
Clast() — взять последнюю
Dобращение по CSS-классу строки
3. Что делает цепочка page.getByRole('listitem').filter({ hasText: 'Ноутбук' }).getByRole('button')?
AНаходит все кнопки на странице
BНаходит кнопку внутри элемента списка, содержащего текст «Ноутбук»
CНаходит элемент списка и удаляет его
DСоздаёт новый элемент списка
Поддержать проект