QA Interview Hub · База знань · Тести
Playwright
Локатори
Локатори і Locator vs ElementHandle
Рекомендовані вбудовані локатори: getByRole, getByLabel, getByPlaceholder, getByText, getByAltText, getByTitle і getByTestId. Пріоритет — getByRole з name, бо він відображає, як сторінку бачить користувач і технології доступності. CSS і XPath через page.locator() — запасний варіант.
Locator лінивий: під час створення він нічого не шукає, пошук відбувається щоразу в момент дії чи перевірки. Тому він переживає перерендер і не дає stale element помилок. Перед дією Playwright автоматично чекає, поки елемент буде attached, visible, stable, enabled і прийматиме події.
Locator працює в strict mode: якщо селектор знаходить кілька елементів, дія кидає помилку, і треба уточнити через filter, first, nth або ланцюжок. ElementHandle — посилання на конкретний DOM-вузол, схоплений один раз, без auto-waiting і strict mode, тому він discouraged. Він потрібен рідко, наприклад для передачі вузла в evaluate.
getByRole глибше: опції і точність
getByRole шукає за ARIA-роллю та доступним ім'ям. Важливо знати, як працює name: за замовчуванням це пошук підрядка без урахування регістру. Тобто { name: 'Зберегти' } знайде і кнопку «Зберегти чернетку». Для точного збігу є exact: true, а для гнучкого — регулярний вираз.
page.getByRole('button', { name: 'Зберегти', exact: true });
page.getByRole('link', { name: /^Замовлення №\d+$/ });
page.getByRole('heading', { name: 'Кошик', level: 1 });
page.getByRole('checkbox', { name: 'Погоджуюсь', checked: true });
page.getByRole('button', { name: 'Меню', expanded: false });
page.getByRole('tab', { selected: true });
Додаткові опції фільтрують за станом: checked, pressed, expanded, selected, disabled, level для заголовків. За замовчуванням приховані елементи не враховуються; includeHidden: true це змінює.
Доступне ім'я береться з тексту елемента, aria-label, aria-labelledby або пов'язаного label. Якщо getByRole не знаходить поле, це часто справжній баг доступності: інпут без label не прочитає і скрінрідер. Я про такі випадки повідомляю розробникам, а не обходжу через CSS.
Для перевірки структури цілого блоку з версії 1.49 є toMatchAriaSnapshot: він порівнює дерево доступності з YAML-шаблоном. Це зручно для меню чи форм, де важливий склад і порядок елементів.
На співбесіді: У getByRole ім'я за замовчуванням шукається як підрядок без регістру, тому для однакових кнопок я використовую exact: true або regex. Також застосовую опції стану, як checked чи expanded. Якщо роль не знаходиться, це сигнал про проблему доступності.
Ланцюжки, filter, has, and, or
Strict mode змушує уточнювати локатори, і для цього є кілька інструментів.
Ланцюжок звужує область пошуку: page.getByTestId('cart').getByRole('button', { name: 'Видалити' }). Внутрішній локатор шукається лише всередині зовнішнього.
filter залишає елементи, що відповідають умові:
const row = page.getByRole('row')
.filter({ hasText: 'INV-1042' })
.filter({ has: page.getByRole('cell', { name: 'Оплачено' }) });
await row.getByRole('button', { name: 'Повернення' }).click();
const activeItems = page.getByRole('listitem').filter({ hasNot: page.getByText('Архів') });
const visibleToasts = page.getByRole('alert').filter({ visible: true });
Різниця важлива: ланцюжок шукає дочірній елемент, а filter({ has }) повертає сам зовнішній елемент, у якого є такий нащадок. Тобто «знайди рядок, де є клітинка Оплачено», а не саму клітинку. Є також hasText, hasNotText, hasNot і visible.
and перетинає два локатори для одного елемента: page.getByRole('button').and(page.getByTitle('Підписатися')). or дає альтернативу: «або діалог згоди, або кнопка Продовжити» — корисно, коли застосунок може показати один з двох станів.
first(), last(), nth() — останній варіант. Вони прив'язуються до порядку в DOM, тому ламаються від зміни сортування. Якщо пишу nth, то лише коли порядок — частина вимоги.
На співбесіді: Для таблиць я шукаю рядок через filter з hasText або has, а потім усередині рядка — потрібну кнопку. nth використовую лише коли порядок важливий за вимогами. or допомагає, коли застосунок легітимно може показати один з двох станів.
Test id, списки і пастка all()
getByTestId за замовчуванням шукає атрибут data-testid. Якщо в проєкті прийнято інший, наприклад data-qa, його задають у конфізі: use: { testIdAttribute: 'data-qa' }. Test id я використовую, коли немає стабільної ролі й тексту: іконки без підпису, динамічні контейнери, мультимовні тексти.
Для перевірки списку краще асерти по всьому локатору, а не цикли:
const items = page.getByRole('listitem');
await expect(items).toHaveCount(3);
await expect(items).toHaveText(['Хліб', 'Молоко', 'Сир']);
toHaveText з масивом перевіряє і кількість, і порядок, і ретраїть до таймауту.
Пастка — locator.all(). Він повертає масив локаторів для елементів, які є в DOM зараз, і не чекає. Якщо список ще вантажиться, отримаю порожній масив, і цикл просто нічого не перевірить — тест зелений, хоча нічого не протестовано. Тому перед all() спочатку чекаю стан, наприклад через toHaveCount.
await expect(items).toHaveCount(3);
for (const item of await items.all()) {
await expect(item.getByRole('img')).toBeVisible();
}
Так само count(), textContent() і allTextContents() повертають значення одномоментно, без ретраїв. Ними можна користуватися для логіки, але не для перевірок.
На співбесіді: Для списків я використовую toHaveCount і toHaveText з масивом, бо вони ретраяться. all() не чекає елементів, тому перед ним завжди ставлю асерт на кількість, інакше цикл може пройти по порожньому масиву і дати хибно зелений тест.