07 · Інструменти автоматизації
Playwright: локатори і дії з елементами
Зміст
Тест падає з «element not found», і півгодини йде на пошук бага, якого немає: кнопка на місці, просто (locator) описував її ланцюжком CSS-класів, а фронтендер перейменував один клас. Це класична поломка в UI-автоматизації — і саме тому Playwright має щодо локаторів позицію: у нього є перелік рекомендованих способів пошуку, він відмовляється працювати з неоднозначним збігом і сам перевіряє, чи елемент готовий до дії. Джун, який знає тільки page.locator('.btn-primary'), зазвичай пише набір, який доводиться ремонтувати після кожної помітної зміни верстки.
Вузлів тут три: пріоритет локаторів, strict mode і force: true. Ця глава — про механіку: чим локатор відрізняється від знайденого елемента, як звужувати пошук і що інструмент робить між викликом click() і справжнім кліком. CSS-селектори, XPath і DOM як явище — у главі «DOM, селектори та події»; ієрархія браузер → контекст → сторінка — у главі «Playwright: архітектура, браузери і контексти»; ретрайні перевірки й — у наступній главі «Playwright: перевірки (assertions) і автоочікування».
Локатор — це рецепт пошуку, а не знайдений елемент
Офіційна дока формулює це прямо: локатори — «спосіб знайти елемент(и) на сторінці в будь-який момент», і саме на них тримаються (auto-waiting) та ретрайність інструмента. Різниця з інтуїцією принципова: локатор не зберігає посилання на вузол DOM — він зберігає опис, за яким вузол шукається заново в момент дії, а не в момент оголошення.
Три наслідки:
- локатор можна створити один раз — у змінній, — і перевикористовувати скільки треба: тримати він нічого не тримає;
- сторінка може перемалюватися між оголошенням локатора і дією, і це нормальний сценарій, а не гонка: пошук усе одно відбудеться пізніше;
- два звертання до одного локатора — це два незалежні пошуки, тож на сторінці, що довантажується, результати можуть відрізнятися.
import { test } from '@playwright/test';
test('оплата картою', async ({ page }) => {
await page.goto('/checkout');
// локатор оголошено один раз; пошук відбудеться в момент click()
const pay = page.getByRole('button', { name: 'Оплатити' });
await page.getByLabel('Номер картки').fill('4111111111111111');
await pay.click();
});
Сім вбудованих локаторів і чому роль перша
Дока називає рекомендованими сім вбудованих локаторів:
| Локатор | Що шукає |
|---|---|
getByRole() | за явними й неявними атрибутами доступності |
getByText() | за текстовим вмістом |
getByLabel() | елемент керування формою — за текстом повʼязаного підпису |
getByPlaceholder() | поле введення — за його placeholder |
getByAltText() | елемент (зазвичай зображення) — за текстовою альтернативою |
getByTitle() | за атрибутом title |
getByTestId() | за атрибутом data-testid (сам атрибут налаштовний) |
Перелік не є жорстким рейтингом, але окрема рекомендація в доці є: щоб тести були стійкими, пріоритезувати треба user-facing атрибути й явні контракти — «наприклад, page.getByRole()». Причина не в естетиці. Роль відображає, як сторінку сприймають користувач і допоміжні технології: кнопка це чи чекбокс. А отже, локатор за роллю не залежить від верстки — рефакторинг і перейменування класів його не ламають, а .btn-primary.mt-3 > span ламають.
Механізм під цим — дерево доступності (accessibility tree): браузер будує його на основі DOM окремим, паралельним поданням сторінки, і саме звідти допоміжні технології беруть пару «роль + доступне імʼя» (для кнопки «OK» читач екрана може вимовити «push-button OK»). Тому разом із роллю зазвичай слід передавати й доступне імʼя — інакше локатор не прицілюється в конкретний елемент.
Звідси побічний ефект, який варто називати на співбесіді: якщо «кнопка» насправді клікабельний <div> без ролі, пошук за роллю її не знайде — і це не незручність інструмента, а знайдений дефект. Елемент без доступного імені або з неправильною роллю — одночасно дефект доступності й майбутній нестабільний локатор.
Поділ, який дока подає як рекомендацію, а не заборону: інтерактивні елементи — за роллю; неінтерактивні (div, span, p) — за текстом.
// інтерактивне — за роллю з доступним імʼям
await page.getByRole('button', { name: 'Надіслати' }).click();
await page.getByRole('link', { name: 'Умови використання' }).click();
// поле форми — за підписом
await page.getByLabel('Електронна пошта').fill('qa@example.com');
// неінтерактивний текст — за текстом
await page.getByText('Замовлень не знайдено');
Філософія під цим не Playwright-специфічна — її сформулювала Testing Library: що більше твої тести схожі на те, як користуються твоїм ПЗ, то більше довіри вони дають.
getByTestId: найстійкіший, але не user-facing
(test id) — це явний контракт між тестом і кодом. Дока не називає його «найкращим»: вона називає його найстійкішим — навіть якщо зміниться текст або роль, тест лишиться зеленим — і одразу застерігає, що він не user-facing. Якщо роль чи текст для перевірки важливі, брати треба user-facing локатори.
Що ще каже джерело поіменно:
- визначати явні тест-ідентифікатори мають QA й розробники разом — це домовленість, а не приватна ініціатива автотестера;
- за замовчуванням
getByTestId()шукає за атрибутомdata-testid, і атрибут можна переналаштувати в конфігу; - доречний він тоді, коли ти обрав цю методологію — або коли локатор за роллю чи текстом побудувати не вдається.
І data-* тут не «засмічення розмітки»: це штатний механізм HTML — атрибут, чиє імʼя починається з data-, тримає на семантичному елементі додаткову інформацію без хаків, і сам HTML спроєктований із розрахунком на такі дані: привʼязані до елемента, але без визначеного значення для самої розмітки.
Далі — дельта, про яку легко забути: «пріоритет локаторів» не є універсальним законом. Cypress ставить data-* основною стратегією й дає явну шкалу:
| Селектор у Cypress | Вердикт доки |
|---|---|
cy.get('button'), cy.get('.btn.btn-large') | Never |
cy.get('#main'), cy.get('[name="submission"]') | Sparingly |
cy.contains('Submit') | Depends — «все ще зчеплено з текстом, який може змінитися» |
cy.get('[data-cy="submit"]') | Always — «ізольовано від усіх змін» |
Тобто текст тут нижче тест-атрибута, тоді як Playwright і Testing Library ставлять user-facing атрибути вище. Це не «хтось помиляється», а різні акценти: стійкість проти близькості до користувача.
Коли CSS і XPath ще виправдані
Дока не забороняє їх, але й не помʼякшує формулювання: «якщо тобі абсолютно необхідно» використати CSS або XPath — є page.locator(), який приймає селектор. Причина обережності названа: вони не рекомендовані, бо DOM часто змінюється й тест перестає бути стійким. CSS добирає елементи за типом, класом, ідентифікатором, атрибутом і станом — і навіть за позицією в дереві, а позиційний добір (:nth-child) крихкий за побудовою: додали рядок вище — і селектор указує вже на інший елемент. Але два випадки, де CSS і XPath лишаються робочим інструментом, є.
Перший — часткове зіставлення за атрибутом. Коли генерує класи з випадковим хешем (Button_root__a1b2c), рятує саме воно: ^= — «починається з», $= — «закінчується на», *= — «містить входження підрядка». Тут же живе пастка: ~= до цієї трійки не належить — він добирає елементи, чиє значення атрибута є списком слів через пробіл, серед яких є рівно таке слово. Тому [class~="logo"] знайде class="header logo", але не class="logotype".
Другий — рух угору по дереву й зіставлення за повним текстом. XPath має осі, яких у наборі комбінаторів CSS немає: parent, ancestor, preceding-sibling (а .. — це рівно parent::node()). Частину цих випадків CSS покриває реляційним псевдокласом :has(), який дає дібрати предка або попереднього сусіда, — але за статусом Baseline він доступний в усіх браузерах лише з грудня 2023 року.
Платня за XPath — багатослівність і дві класичні пастки:
//button[1]— це не «перша кнопка на сторінці», а всі кнопки, які є першими серед кнопок-дітей свого батька: специфікація застерігає, що//para[1]не те саме, що/descendant::para[1]. Справді перший збіг беруть у дужки:(//button)[1].contains(text(), 'Оплата картою')не знайде кнопку, боtext()повертає множину вузлів, а конвертація такої множини в рядок бере вузол, перший у порядку документа: для<button>Оплата <b>картою</b></button>перевіряється лише «Оплата ». Ліки —.замістьtext(): рядкове значення елемента конкатенує всіх його текстових нащадків.
І окремо про межу, де XPath перестає працювати: локатори Playwright типово працюють з елементами всередині Shadow DOM, але з двома винятками — XPath межу не пробиває, а закриті (closed) shadow root локатори не підтримують. Тож у застосунку на web components XPath як стратегія відпадає.
Головне: якщо в елемента немає ні ролі, ні доступного імені, ні стабільного тексту — правильна відповідь не «довший CSS-шлях», а домовленість про тест-ідентифікатор.
Chaining і filter(): звужувати, а не ускладнювати
Коли на сторінці двадцять однакових карток, спокуса одна — дописати до селектора ще один рівень вкладеності. Правильний хід інший: локатори можна зчіплювати, звужуючи пошук до конкретної частини сторінки. Спершу контейнер, у якому елемент унікальний, потім елемент уже в ньому.
Тут є правило, на якому спотикаються всі: фільтрувальний локатор мусить бути відносним до вихідного й шукається від його збігу, а не від кореня документа.
// картка потрібного замовлення: контейнер, у якому є цей текст
const card = page
.getByRole('listitem')
.filter({ has: page.getByText('Замовлення 1042') });
// кнопка шукається вже всередині картки, а не по всій сторінці
await card.getByRole('button', { name: 'Скасувати' }).click();
В іншого інструмента той самий прийом — головний патерн, а не трюк. CodeceptJS розрізняє строгий локатор (обʼєкт із явно названою стратегією — один запит) і семантичний (рядок, який зіставляється так, як сторінку читає користувач), а рекомендованим за замовчуванням називає семантичний локатор у контексті. Мотив названо прямо: дублікати підписів деінде на сторінці більше не дають нестабільних збігів. Ціна теж названа чесно — семантичний пошук робить кілька запитів, і саме контекст робить цю ціну незначущою.
// CodeceptJS: підпис + контекст, у якому він унікальний
I.click('Скасувати', '.order-card');
Строгість локатора: «знайшло два» — це фейл, а не дрібниця
Локатори Playwright строгі: будь-яка операція, що передбачає цільовий елемент DOM, кидає виняток, якщо збігів більше за один. Джуна це спершу дратує — насправді це одна з найкорисніших властивостей інструмента.
Подумай, що означає альтернатива. Якщо фреймворк на неоднозначному збігу мовчки бере перший елемент, тест зелений не тому, що продукт працює, а тому, що інструмент угадав. Строгість перетворює «угадав» на явний фейл — і не випадково перша ж перевірка перед кліком це те, що локатор вказує рівно на один елемент.
Вийти зі строгості можна явно: first(), last(), nth() кажуть, який зі збігів брати. Але дока супроводжує це застереженням, і воно не про стиль — сторінка змінюється, і локатор укаже на елемент, якого ти не мав на думці. Порада джерела: не глушити строгість, а придумати локатор, який проходить критерій однозначності.
Практичний бік: strict mode violation у логу — часто не проблема тесту, а знахідка: два однакові доступні імена на одній сторінці варто перевірити як можливий дефект.
Списки: коли елементів справді багато
Локатор, що дає двадцять збігів, — це нормально. Ненормально — виконувати над ним дію. Тому робочих шляхів рівно два: звузити до одного елемента зчепленням і фільтром за контекстом (той спосіб, який дока радить) або явно вийти зі строгості через nth(), first(), last(), свідомо взявши , що на зміненій сторінці індекс поїде.
Питання «а скільки їх?» — це вже не питання дії, а питання перевірки, і тут різниця між гетером і ретрайним критична. Ретрайна перевірка повторює умову, доки вона не виконається, а гетер isVisible() не чекає жодної секунди: дивиться, чи локатор там, і повертає результат одразу. Кількість, знята гетером у випадковий момент, — це гонка з рендерингом. Механіку ретрайних перевірок і таймаутів розбирає наступна глава «Playwright: перевірки (assertions) і автоочікування».
Actionability: що відбувається між click() і кліком
Найважливіше в цій главі — те, чого не видно в коді. Перед кожною дією Playwright виконує набір перевірок придатності до дії (actionability), чекає, поки всі релевантні пройдуть, і лише тоді діє. Не дочекався в межах таймауту — падає з TimeoutError.
Для кліку перевіряється пʼять умов, і кожна має точне означення:
| Перевірка | Що саме означає |
|---|---|
| Однозначність | локатор вказує рівно на один елемент |
| Visible | непорожній bounding box і без обчисленого visibility:hidden |
| Stable | той самий bounding box щонайменше два послідовні кадри анімації |
| Receives Events | елемент є ціллю вказівникової події в точці дії |
| Enabled | не задизейблений |
Деталі, які й дають більшість «незрозумілих» падінь:
- елементи нульового розміру й
display:noneневидимі, аopacity:0вважається видимим; - стабільність означає, що елемент уже не рухається: поки триває анімація, дія перечікується;
- «отримує події» ловить рівно ту ситуацію, коли клік перехоплює накладений зверху елемент — оверлей, модалка, тост;
- «увімкнений» — не лише відсутність атрибута
disabled: задизейбленим робить і батьківськийfieldset, іaria-disabled="true". Для дій уведення до переліку додається перевірка редагований: увімкнений і неreadonly.
Зі схеми видно головне для читання фейлів: TimeoutError на кліку не означає «елемент не знайдено» — він однаково добре означає «елемент є, але його перекриває оверлей» або «елемент є, але досі їде».
force: true — не фікс, а глушник
force вимикає необовʼязкові перевірки придатності. Конкретно: передавши правдиве force у клік, ти перестаєш контролювати, чи справді елемент отримує клік — інструмент клікне по координатах, навіть якщо в цій точці подію забирає оверлей.
Тепер прочитай це очима тестувальника. Тест був червоний, бо клік перехоплював накладений елемент. Після force: true він зелений — але користувач, який зробить те саме руками, у кнопку не влучить. Це не виправлення, а глушник: дефект лишився в продукті, а сигнал про нього вимкнули власноруч.
Тому падіння «елемент перехоплює вказівникові події» варто вважати корисним: воно фіксує або реальну проблему UX, або власну помилку тесту — не закритий cookie-банер, невидимий бекдроп модалки, тост, що не встиг зникнути. Лікується це (закрити оверлей, дочекатися зникнення тоста), а не force. Сюди ж пастка з opacity: 0: прозорий елемент за означенням видимий, тож клік по практично невидимій кнопці пройде і без force. Інструментальні засоби боротьби з нестабільністю — у главі «Боротьба з флаком засобами інструмента»; межа між легітимним і маскуванням — тема розділу про стратегію автоматизації.
Типові помилки
- «Локатор не знаходить елемент — треба точніший селектор». Насправді перша підозра інша: інший контекст. iframe — це окремий документ, а XPath не пробиває shadow root. Симптом «руками в DevTools бачу, а тест ні» перевіряють на iframe і Shadow DOM, а вже потім правлять селектор.
- «Додали
force: true— і тест став стабільним». Насправді вимкнено перевірку «отримує події»: клік тепер проходить там, де реальний користувач у кнопку не влучив би. Стабільним став не продукт, а звіт. - «
strict mode violation— це причіпка інструмента». Насправді це вимога однозначності: два збіги означають, що тест не знає, що клікає, а без строгості він зеленів би на першому збігу, яким би той не був. - «
opacity: 0— елемент невидимий, дія впаде». Насправді за означенням придатності прозорий елемент видимий, і клік по ньому пройде. - «
data-testid— найкращий локатор, беремо тільки його». Насправді дока називає його найстійкішим, але не user-facing; там, де важлива роль або текст, user-facing локатор ще й ловить дефекти доступності. - «XPath я написав правильно». Дві класики:
//button[1]— це не перша кнопка на сторінці, а всі кнопки, які є першими серед кнопок-дітей свого батька (перший збіг —(//button)[1]); аcontains(text(), 'Оплата картою')не знайде<button>Оплата <b>картою</b></button>, бо конвертація множини вузлів у рядок бере лише перший текстовий вузол («Оплата ») — требаcontains(., ...).
Підсумок
- Локатор — опис, за яким елемент шукається заново в момент дії, а не збережене посилання на вузол; саме тому на ньому тримаються автоочікування й ретраї.
- Пріоритет за докою Playwright: user-facing атрибути й явні контракти. Інтерактивне — за роллю з доступним імʼям, неінтерактивне — за текстом;
data-testid— найстійкіший, але не user-facing варіант; CSS і XPath —not recommended. - Строгість — фіча, а не примха: неоднозначний збіг падає замість того, щоб угадувати. Штатний спосіб її пройти — звузити пошук зчепленням і фільтром, відносним до вихідного локатора.
- Перед кліком інструмент перевіряє: рівно один збіг, видимий, стабільний, отримує події, увімкнений. Тому
TimeoutErrorна кліку — не завжди «не знайшов». force: trueвимикає необовʼязкові перевірки — зокрема «отримує події»: зелений тест після нього означає, що дефект перекритого кліку поїхав у прод разом із вимкненим сигналом.
Можливі питання
- «Який пріоритет локаторів у Playwright і чому?» Перевіряють, чи розумієш ти причину, а не порядок напамʼять. Сильна відповідь: user-facing атрибути й явні контракти, бо роль відображає те, як сторінку сприймають користувач і допоміжні технології, і не залежить від верстки; CSS і XPath дока не рекомендує, бо DOM часто змінюється. Плюс: у Cypress ієрархія інша.
- «Що таке
strict modeі що робити, коли локатор знайшов два елементи?» Дивляться, чи не почнеш ти одразу зfirst(). Правильно: строгість — вимога однозначності, тож спершу звужуємо пошук зчепленням і фільтром. - «Що Playwright робить перед кліком?» Питання на actionability: рівно один збіг, видимий, стабільний, отримує події, увімкнений; для введення додається «редагований». Хто називає точні означення — непорожній , два послідовні кадри анімації,
opacity:0вважається видимим — той показує, що читав доку. - «Коли доречний
force: true?» Питання-фільтр. Очікувана відповідь: майже ніколи, бо він перестає контролювати, чи елемент справді отримує клік, — тобто робить зеленим тест поверх реального дефекту. - «Чим
data-testidкращий і чим гірший за локатор за роллю?» Кращий стійкістю: змінився текст або роль — тест лишається зеленим. Гірший тим, що не user-facing: він не перевіряє, чи елемент доступний користувачеві. - «Чому
expect(await locator.isVisible()).toBe(true)— погана перевірка?» Гетер повертає моментальний зріз і не чекає жодної секунди, а ретрайна перевірка повторює умову, доки вона не виконається.
Джерела
Локатор — це рецепт пошуку, а не знайдений елемент
- Playwright — Locators — локатор як спосіб знайти елемент у будь-який момент і його роль в автоочікуванні та ретраях.
- Playwright — Best Practices — локатори приходять з автоочікуванням і ретрайністю: перед кліком інструмент перевіряє придатність елемента до дії.
Сім вбудованих локаторів і чому роль перша
- Playwright — Locators — перелік семи рекомендованих локаторів, пріоритет user-facing атрибутів і поділ «інтерактивні за роллю, неінтерактивні за текстом».
- Playwright — Best Practices — чому опора на структуру DOM крихка: змінився CSS-клас — зламався тест.
- MDN — Accessibility tree (glossary) — дерево доступності як окреме подання сторінки, побудоване на основі DOM.
- W3C — Accessible Rich Internet Applications (WAI-ARIA) 1.2 — роль і доступне імʼя як пара, яку зчитує допоміжна технологія.
- Testing Library — Guiding Principles — принцип «що більше тести схожі на реальне використання, то більше довіри».
getByTestId: найстійкіший, але не user-facing
- Playwright — Locators — test id як найстійкіший, але не user-facing спосіб; налаштовний атрибут; спільна домовленість QA й розробки.
- MDN — Use data attributes —
data-*як штатний механізм HTML для додаткових даних на елементі. - Cypress — Best Practices — інша ієрархія в іншого інструмента: шкала
Never/Sparingly/Depends/Alwaysіdata-*як основна стратегія.
Коли CSS і XPath ще виправдані
- Playwright — Locators —
page.locator()для CSS і XPath, формулюванняnot recommendedз названою причиною, і межа: XPath не пробиває shadow root. - MDN — CSS selectors — за чим CSS добирає елементи й чому позиційний добір крихкий.
- MDN — Attribute selectors — часткове зіставлення
^=,$=,*=і відмінність~=як добору за словом зі списку. - MDN — :has() CSS pseudo-class — добір предка або попереднього сусіда й статус Baseline із грудня 2023 року.
- W3C — XML Path Language (XPath) Version 1.0 — осі вгору по дереву, пастка
//x[1]і конвертація множини вузлів у рядок за першим вузлом. - MDN — Using shadow DOM — як межа інкапсуляції, крізь яку звичайний пошук не бачить.
Chaining і filter(): звужувати, а не ускладнювати
- Playwright — Best Practices — локатори можна зчіплювати, щоб звузити пошук до частини сторінки.
- Playwright — Locators — фільтрувальний локатор мусить бути відносним до вихідного й шукається від його збігу, а не від кореня документа.
- CodeceptJS — Locators — семантичний локатор у контексті як рекомендований патерн, його мотив і ціна.
Строгість локатора: «знайшло два» — це фейл, а не дрібниця
- Playwright — Locators — строгість локатора, виняток при кількох збігах і застереження до
first()/last()/nth(). - Playwright — Auto-waiting (actionability) — однозначність локатора як перша перевірка перед кліком.
Списки: коли елементів справді багато
- Playwright — Locators — локатор як спосіб знайти елементи в будь-який момент і явний вихід зі строгості для наборів.
- Playwright — Best Practices — ретрайна перевірка чекає, ручний гетер не чекає жодної секунди.
- Playwright — Auto-waiting (actionability) — ретрайні перевірки повторюють умову, доки вона не виконається.
Actionability: що відбувається між click() і кліком
- Playwright — Auto-waiting (actionability) — набір перевірок перед дією,
TimeoutErrorпри недочіканні й точні означення видимості, стабільності, отримання подій, увімкненості та редагованості. - Playwright — Locators — однозначність збігу як умова операції над локатором.
- Playwright — Best Practices — автоочікування як перевірка придатності, а не пауза.
force: true — не фікс, а глушник
- Playwright — Auto-waiting (actionability) —
forceвимикає необовʼязкові перевірки й перестає контролювати отримання кліку; падіння «перехоплює події» як корисний сигнал;opacity:0вважається видимим.
- Playwright — Locators — строгість, межа XPath у Shadow DOM і статус test id як найстійкішого, але не user-facing способу.
- Playwright — Auto-waiting (actionability) — що саме вимикає
forceі чомуopacity:0вважається видимим. - W3C — XML Path Language (XPath) Version 1.0 — пастка
//x[1]і конвертація множини вузлів у рядок за першим вузлом. - MDN — Using shadow DOM — shadow tree як межа, крізь яку звичайний пошук не бачить.
- Playwright — Locators — локатор як спосіб знайти елемент у будь-який момент, пріоритет user-facing атрибутів, статус test id, строгість і вимога відносності фільтрувального локатора.
- Playwright — Auto-waiting (actionability) — перелік перевірок перед кліком,
TimeoutErrorпри недочіканні й наслідкиforce.
- Playwright — Locators — пріоритет локаторів, строгість і вихід із неї, статус test id.
- Playwright — Auto-waiting (actionability) — перелік перевірок перед дією і наслідки
force. - Playwright — Best Practices — ретрайний асерт проти ручного гетера.
- Cypress — Best Practices — інша ієрархія локаторів як контраргумент до «універсального пріоритету».
Відео до теми
Чим локатор (locator) у Playwright відрізняється від знайденого елемента?
тримає умову пошуку, а не вузол. У момент, коли ти пишеш page.getByRole('button', { name: 'Оплатити' }), на сторінці ще нічого не шукається — об'єкт запам'ятовує, як шукати, а сам пошук відбудеться пізніше, коли по локатору клікнуть або перевірять його. Саме ця відкладеність і дає інструменту змогу автоматично чекати й перезапитувати DOM. Наслідків три. По-перше, локатор безпечно оголосити один раз — у змінній чи полі — і смикати скільки завгодно, бо він нічого не утримує. По-друге, сторінка може перемалюватися між оголошенням і дією: це штатна ситуація, а не гонка. По-третє, два звертання до одного локатора — два незалежні пошуки, тож поки сторінка довантажується, результати можуть розійтися. Контраст із класичним findElement, який повертає посилання на конкретний вузол, помітний одразу: там після ререндера отримуєш stale element reference, тут цей клас падінь майже зникає.
Які вбудовані локатори є в Playwright і за чим шукає кожен?
Рекомендованих сім. getByRole() знаходить елемент за атрибутами доступності — явними й неявними. getByText() шукає за текстовим вмістом. getByLabel() бере елемент керування формою через текст його підпису. getByPlaceholder() цілиться в поле введення за значенням placeholder. getByAltText() знаходить елемент — зазвичай зображення — за текстовою альтернативою. getByTitle() спирається на атрибут title. getByTestId() шукає за , типово data-testid, причому сам атрибут можна перевизначити в конфігурації. Це перелік способів, а не жорсткий рейтинг: пріоритет задає окрема рекомендація — спиратися на те, що бачить користувач, і на явні домовленості з розробкою.
Який пріоритет локаторів радить дока Playwright і чому саме такий?
Стійкі тести будують на user-facing атрибутах і явних контрактах — як приклад дока прямо називає page.getByRole(). Причина суто інженерна. Роль описує, чим елемент є для користувача й допоміжних технологій: кнопка, посилання, чекбокс. Верстка навколо цього може змінюватися як завгодно — переїхали, класи перейменували, обгортку додали, — а роль лишається тією самою, тож локатор виживає. Ланцюжок на кшталт .btn-primary.mt-3 > span виживає рівно доти, доки фронтендер не торкнеться жодного з цих класів. CSS і XPath дока позначає як not recommended саме через це: DOM змінюється часто. Додатковий бонус user-facing підходу — тест мимохідь перевіряє, що елемент узагалі доступний користувачеві.
До чого тут дерево доступності (accessibility tree) і навіщо разом із роллю передавати ім'я?
Браузер будує на основі DOM ще одне, паралельне подання сторінки — дерево доступності. Саме звідти допоміжні технології беруть пару «роль + доступне ім'я»: читач екрана для кнопки «OK» може вимовити щось на кшталт «push-button OK». getByRole() працює по цьому ж поданню, тому і поводиться як користувач із читачем екрана, а не як парсер розмітки. Ім'я в цій парі майже завжди обов'язкове на практиці: сама роль button на типовій сторінці збігається з десятком елементів, і без імені локатор не прицілиться в потрібний. Тому робоча форма — getByRole('button', { name: 'Надіслати' }), а не гола роль.
Кнопка на екрані є, але getByRole('button') її не бачить — виявилось, це <div> з обробником кліку. Це обмеження інструмента?
Ні, це знахідка. Елемент, який виглядає й поводиться як кнопка, але не має відповідної ролі, недоступний для допоміжних технологій: читач екрана не оголосить його кнопкою, з клавіатури він, найімовірніше, теж недосяжний. Тобто локатор не «не спрацював» — він показав дефект доступності, який до цього ніхто не помічав. Другий бік тієї самої монети: елемент без доступного імені або з неправильною роллю приречений бути нестабільним локатором, бо чіплятися доведеться за класи або структуру. Правильна реакція — не переписувати тест на CSS-шлях, а завести дефект і поговорити з розробкою про семантичну розмітку чи хоча б про тест-ідентифікатор.
Коли брати getByLabel, коли getByText, а коли getByRole?
Орієнтир простий і теж іде з доки, як рекомендація, а не заборона: інтерактивне — за роллю, неінтерактивне — за текстом. Кнопки, посилання, чекбокси, вкладки — getByRole() з доступним іменем. Поля форми зручніше брати через getByLabel(): підпис — це те, що читає користувач, і він же зв'язаний з інпутом семантично, тож локатор ловить одразу і поле, і його контракт із підписом. Статичний текст у div, span, p — повідомлення «Замовлень не знайдено», заголовок, помилку валідації — шукають getByText(), бо ролі там або немає, або вона неінформативна. getByPlaceholder() лишається запасним варіантом — для полів, у яких підпису немає взагалі.
Чим getByTestId кращий і чим гірший за локатор за роллю?
Виграє він стійкістю: дока називає test id найстійкішим способом — переписали текст кнопки, змінили роль, перебудували верстку, а тест лишається зеленим, бо атрибут ніхто не чіпав. Програє тим, що він не user-facing: тест-ідентифікатор нічого не каже про те, чи бачить і чує елемент реальний користувач, тож із таким локатором проблема доступності проходить повз тест непоміченою. Звідси й правило вибору: якщо для перевірки важливі роль або текст — беруть user-facing локатор; test id доречний там, де команда свідомо обрала цю методологію або де ні за роль, ні за текст зачепитися не виходить. Формулювання «testid — найкращий локатор» на співбесіді видає людину, яка чула висновок, але не читала застереження.
Хто домовляється про тест-ідентифікатори і чи не засмічують вони розмітку?
Дока каже про це прямо: явні тест-ідентифікатори визначають QA й розробники разом. Це домовленість двох сторін, а не приватна ініціатива автотестера, який уночі дописав атрибут у чужий компонент — інакше наступний рефакторинг знесе його без жодного попередження. Технічно ніякого засмічення немає: атрибути з префіксом data- — штатний механізм HTML, спеціально спроєктований, щоб тримати на елементі додаткову інформацію, яка нічого не означає для самої розмітки. За замовчуванням Playwright шукає data-testid, але якщо в проєкті вже прижився інший атрибут — його вказують у конфігурації, і getByTestId() працює з ним.
У Cypress ієрархія локаторів інша. Хто помиляється?
Ніхто — акценти різні. Cypress ставить тест-атрибути основною стратегією й дає для селекторів пряму шкалу: cy.get('button') і cy.get('.btn.btn-large') — Never, cy.get('#main') та вибірка за атрибутом на кшталт [name="submission"] — Sparingly, cy.contains('Submit') — Depends, бо лишається зчепленим із текстом, який можуть змінити, а cy.get('[data-cy="submit"]') — Always, як ізольований від будь-яких змін. Тобто тест-атрибут там переважає текст — а в Playwright і Testing Library ваги розставлені навпаки, з ухилом до того, що бачить користувач. Компроміс той самий, просто зважений по-різному: максимальна стійкість проти максимальної близькості до реального користувача. На співбесіді ця дельта — гарна нагода показати, що «пріоритет локаторів» ти розумієш як інженерний вибір, а не як заповідь.
Коли CSS і XPath усе ще виправдані?
Дока їх не забороняє: для селектора є page.locator(), і формулювання звучить як «якщо це справді необхідно». Обережність пояснена — прив'язка до структури DOM крихка, бо структура змінюється часто; особливо це стосується позиційного добору на кшталт :nth-child, який після вставленого вище рядка починає вказувати на сусіда. Але два робочі випадки лишаються. Перший — часткове зіставлення за значенням атрибута, коли клас згенеровано разом із хешем. Другий — коли треба піднятися до предка чи попереднього сусіда або зіставити текст елемента цілком: у XPath для цього є осі parent, ancestor, preceding-sibling (а скорочення .. — це рівно parent::node()), яких серед комбінаторів CSS немає. Частину таких випадків закриває реляційний псевдоклас :has(), проте за статусом Baseline повна підтримка в браузерах у нього тільки з грудня 2023-го, тож у проєктах зі старішою матрицею на нього ще озираються.
Бандлер генерує класи з хешем (Button_root__a1b2c). Як за таке зачепитися і де тут пастка?
Рятує часткове зіставлення за атрибутом: ^= бере значення, що починається з підрядка, $= — що ним закінчується, *= — що містить його будь-де. Для згенерованих класів робочий варіант — щось на кшталт [class^="Button_root"], бо стабільним лишається саме префікс, а хвіст-хеш змінюється від збірки до збірки. Пастка — оператор ~=, який виглядає як четвертий у цьому ряду, але робить зовсім інше: він розглядає значення атрибута як список слів через пробіл і шукає серед них точний збіг. Тому [class~="logo"] знайде елемент із class="header logo" і не знайде class="logotype" — підрядок його не цікавить. І все ж це паліатив: коли стабільних зачіпок немає взагалі, виходом стає не хитріший селектор, а узгоджений із розробкою тест-ідентифікатор.
Назви дві класичні пастки XPath.
Перша — індексація. //button[1] читається не як «перша кнопка на сторінці», а як «усі кнопки, що є першими серед кнопок-дітей свого батька»: предикат застосовується всередині кожного кроку, а не до підсумкового набору. Специфікація XPath 1.0 наводить рівно цей приклад — //para[1] не еквівалентно /descendant::para[1]. Щоб узяти справді перший збіг, вираз беруть у дужки: (//button)[1]. Друга пастка — текст. Візьми <button>Оплата <b>картою</b></button>: вираз contains(text(), 'Оплата картою') по ньому промахнеться. Причина в тому, що text() віддає множину вузлів, а перетворення такої множини на рядок бере з неї лише перший вузол у порядку документа — на перевірку йде сама «Оплата ». Ліки — крапка замість text(): contains(., 'Оплата картою') працює з рядковим значенням елемента, яке склеює всіх його текстових нащадків.
Playwright бачить елементи всередині Shadow DOM? Де межа?
Типово так — вбудовані локатори працюють з елементами в , і це одна з причин, чому в застосунках на web components вони настільки зручніші за ручний пошук. Але винятків два, і обидва варто називати. Перший: XPath межу не пробиває, тож у компонентному застосунку XPath як стратегія просто відпадає. Другий: закриті (closed) shadow root локатори не підтримують взагалі — до їхнього вмісту доступу немає. Практичний висновок для розбору падінь: якщо елемент видно очима, а тест його не знаходить, версія «треба точніший селектор» іде не першою — спершу перевіряють, чи не сидить елемент в іншому документі (iframe) або за .
Що таке строгість (strict mode) локатора і що робити, коли збігів два?
Будь-яка операція, що передбачає один цільовий елемент, кидає виняток, якщо локатор збігся більш ніж з одним. Джуна це дратує, але подумай про альтернативу: фреймворк, який на неоднозначності мовчки бере перший елемент, робить тест зеленим не тому, що продукт працює, а тому, що інструмент вгадав — і одного дня вгадає інакше. Строгість перетворює цю лотерею на явне падіння. Правильна перша реакція на strict mode violation — не first(), а звуження пошуку: контейнер, у якому елемент унікальний, плюс фільтр. Явний вихід зі строгості (first(), last(), nth()) існує, але дока супроводжує його застереженням, і воно не про смак: сторінка змінюється, і індекс легко почне вказувати на елемент, якого ти не мав на думці. Окремо варто дивитися на саме падіння як на сигнал продукту: два однакові доступні імені на одному екрані — привід перевірити це як можливий дефект.
Як правильно звужувати пошук і яка тут головна пастка?
Зчеплення (chaining): спершу локатор на контейнер, у якому потрібний елемент унікальний, потім пошук усередині цього контейнера. Для вибору самого контейнера є filter(), який відсіює збіги за вмістом — наприклад, картку замовлення можна знайти як listitem, усередині якого є текст із потрібним номером, а вже потім клікнути в ній кнопку. Пастка одна, зате в неї потрапляють усі: фільтрувальний локатор мусить бути відносним до вихідного — він шукається від збігу зовнішнього локатора, а не від кореня документа. Тому умова у filter(), яка насправді живе поза контейнером — глобальний банер, заголовок сусідньої секції, — не збіжиться ніколи: збігів стане нуль, і дія впаде по , хоч очима «все на сторінці є». До речі, той самий прийом в інших інструментах не трюк, а базовий патерн: CodeceptJS рекомендує за замовчуванням саме семантичний локатор у контексті (I.click('Скасувати', '.order-card')), бо контекст знімає нестабільність від дублікатів підпису деінде на сторінці, і водночас робить незначущою ціну семантичного пошуку — кілька запитів замість одного.
Локатор дає двадцять збігів. Це помилка?
Сам по собі — ні. Локатор цілком законно описує набір: усі рядки таблиці, усі картки товару, усі повідомлення в стрічці. Помилка починається там, де над таким набором намагаються виконати дію — саме її і зупиняє строгість. Шляхів два: зійти до єдиного елемента через контейнер плюс фільтр (штатний і рекомендований варіант) або свідомо вийти зі строгості через nth(), first(), last(), прийнявши , що на зміненій сторінці індекс поїде. А от питання «скільки їх?» — це вже не про дію, а про перевірку, і тут важливо не зісковзнути на гетер: isVisible() чи ручний підрахунок повертають миттєвий зріз і не чекають ані секунди, тобто змагаються з рендерингом. Кількість перевіряють ретрайним , який повторює умову, доки вона не виконається.
Що відбувається між викликом click() і справжнім кліком?
Перед дією виконується набір перевірок придатності до дії (actionability), і Playwright чекає, доки всі релевантні пройдуть. Для кліку їх п'ять: локатор указує рівно на один елемент; елемент видимий — має непорожній і не має обчисленого visibility: hidden; він стабільний — bounding box не змінився щонайменше два послідовні кадри анімації; він отримує події — саме він є ціллю вказівникової події в точці дії; він увімкнений. Для дій уведення додається ще «редагований»: увімкнений і не readonly. Деталі означень і дають більшість «незрозумілих» падінь: елемент нульового розміру чи з display: none невидимий, а от opacity: 0 за цим означенням видимий; «увімкнений» — це не лише відсутність атрибута disabled, бо задизейбленим елемент робить і батьківський fieldset, і aria-disabled="true". Якщо в межах таймауту умови не зійшлися, дія падає з TimeoutError.
Тест падає з TimeoutError на кліку, а локатор точно правильний. Як розбираєш?
Головне — не читати TimeoutError як «елемент не знайдено». Він однаково добре означає «елемент знайдено, але його щось перекриває» або «елемент знайдено, але він досі їде». Тому розбір іде по списку перевірок. Чи справді збіг один — інакше падіння було б від строгості, і це вже інший діагноз. Чи не перехоплює клік накладений зверху шар: невидимий бекдроп модалки, cookie-банер, тост, що не встиг зникнути, — типова трійка. Чи не триває анімація або переїзд контенту: доки bounding box рухається, дія чесно перечікується. Чи не задизейблений елемент через батьківський fieldset або aria-disabled. І чи той це взагалі документ — iframe і shadow root ловляться на цьому ж кроці. Trace viewer тут вирішує швидше за здогади: у ньому видно стан сторінки на момент дії. Лікується знайдене — закрити банер, дочекатися зникнення оверлея, — а не глушенням перевірки.
Коли доречний force: true?
Майже ніколи, і це питання-фільтр. force вимикає необов'язкові перевірки придатності — зокрема ту, що елемент справді отримує клік: інструмент просто б'є по координатах, навіть якщо подію в цій точці забирає накладений зверху шар. Тепер прочитай наслідок очима тестувальника: тест був червоний, бо кнопку перекривало; після force він зелений, а живий користувач у цю кнопку так само не влучить. Дефект нікуди не подівся — вимкнули сигнал про нього, тобто стабільним став звіт, а не продукт. Тому падіння через перехоплення вказівникових подій — саме той сигнал, який хочеться мати: він вказує або на реальну проблему UX, або на пропущену передумову в тесті. І окремо про сусідню плутанину: opacity: 0 вважається видимим, тож клік по практично невидимій кнопці пройде і без жодного force — тут падіння якраз не буде, і саме тому такий дефект легко проґавити.
Три кейси з робочого життя AQA: розбір strict mode violation на списку замовлень, діагностика TimeoutError на кліку без переходу до force, і ревізія крихких у . Скрізь — не «як написати», а що саме дивитися і чому обраний варіант кращий за сусідній.
Кейс 1. «Знайшло два» на списку замовлень
Тест на скасування замовлення падає ще до кліку. Лог короткий і зрозумілий:
Error: strict mode violation: getByRole('button', { name: 'Скасувати' })
resolved to 3 elements:
1) <button class="btn btn-sm">Скасувати</button> aka getByRole('listitem').filter({ hasText: 'Замовлення 1039' }).getByRole('button')
2) <button class="btn btn-sm">Скасувати</button> aka getByRole('listitem').filter({ hasText: 'Замовлення 1042' }).getByRole('button')
3) <button class="btn btn-sm">Скасувати</button> aka getByRole('listitem').filter({ hasText: 'Замовлення 1055' }).getByRole('button')
Перше, що варто зрозуміти: продукт тут ні в чому не винен. На сторінці три замовлення, у кожного своя кнопка скасування — так і має бути. Винен тест, який не сказав, яке саме замовлення скасовує. Варіанти виходу різні за ціною:
| Варіант | Що станеться | Вердикт |
|---|---|---|
.first() | візьме верхній рядок списку | працює, доки сортування й дані не змінилися |
.nth(1) | візьме другий за порядком | найкрихкіше: новий рядок вище — і клікаємо не туди |
filter() за текстом замовлення | візьме рядок конкретного замовлення | стабільно, поки номер замовлення в тесті детермінований |
getByTestId('cancel-1042') | точний контракт із розробкою | найстійкіше, але потребує домовленості |
Рекомендований докою шлях — не глушити строгість, а звузити пошук до , у якому кнопка унікальна:
import { test, expect } from '@playwright/test';
test('скасування конкретного замовлення', async ({ page }) => {
await page.goto('/orders');
// спершу картка: рядок списку, всередині якого є потрібний номер
const card = page
.getByRole('listitem')
.filter({ hasText: 'Замовлення 1042' });
// кнопка шукається вже в межах картки, а не по всій сторінці
await card.getByRole('button', { name: 'Скасувати' }).click();
await expect(card.getByText('Скасовано')).toBeVisible();
});
Що дивитися і чому:
- Фільтр працює відносно вихідного локатора.
card.getByRole('button')шукає всередині збігуcard, а не від кореня документа — саме тому кнопок знову стає одна. Зворотний бік тієї самої механіки: умова уfilter(), яка насправді живе поза карткою, не збіжиться ніколи — локатор дасть нуль збігів, і дія впаде по , хоч очима «все на сторінці є». - теж варто прив'язати до картки.
expect(card.getByText('Скасовано'))перевіряє статус саме цього замовлення. Глобальнийpage.getByText('Скасовано')пройде навіть тоді, коли скасувалося сусіднє — класичний хибнозелений тест. .first()не «швидке рішення», а відкладена проблема. Він мовчки бере верхній рядок, і день, коли на сторінці зміниться сортування за замовчуванням, стане днем незрозумілого падіння в іншому тесті.- Іноді падіння — знахідка. Якщо на екрані виявилося дві кнопки з ідентичним доступним іменем там, де за дизайном мала бути одна, це варто перевірити як дефект: користувач із читачем екрана почує два однакові «Скасувати» і не зрозуміє різниці.
Кейс 2. TimeoutError на кліку, хоча локатор точно правильний
Наступний тест червоний, і повідомлення виглядає :
TimeoutError: locator.click: Timeout 30000ms exceeded.
Call log:
- waiting for getByRole('button', { name: 'Оформити замовлення' })
- locator resolved to <button class="btn-primary">Оформити замовлення</button>
- attempting click action
- waiting for element to be visible, enabled and stable
- element is visible, enabled and stable
- scrolling into view if needed
- done scrolling
- <div class="cookie-banner">…</div> intercepts pointer events
Читаємо лог знизу вгору: елемент знайдено, він видимий, увімкнений і стабільний — тобто «не знайшло» тут узагалі ні до чого. Клік перехоплює cookie-банер. Ось як розкладаються типові причини TimeoutError на дії:
| Що видно в логу | Що це насправді | Чим лікується |
|---|---|---|
intercepts pointer events | зверху накладений шар: банер, бекдроп модалки, тост | передумова: закрити банер, дочекатися зникнення оверлея |
element is not stable | елемент їде: анімація, зсув розкладки, віртуальний список | нічого не робити — інструмент дочекається сам; якщо не встигає, дивитися на тривалість анімації |
element is not enabled | атрибут disabled, батьківський fieldset або aria-disabled="true" | дочекатися розблокування; якщо не розблоковується — це дефект або пропущений крок сценарію |
waiting for locator без resolved to | елемент справді не знайдено | перевірити iframe, Shadow DOM і лише потім селектор |
not visible при видимій очима кнопці | нульовий розмір або visibility: hidden на предку | дивитися обчислені стилі, часто це дефект верстки |
Правильна правка — прибрати причину, а не перевірку:
test('оформлення замовлення', async ({ page }) => {
await page.goto('/checkout');
// передумова: банер згоди перекриває нижню частину екрана
const banner = page.getByRole('region', { name: 'Використання cookie' });
if (await banner.isVisible()) {
await banner.getByRole('button', { name: 'Прийняти' }).click();
await expect(banner).toBeHidden();
}
await page.getByRole('button', { name: 'Оформити замовлення' }).click();
});
Що дивитися і чому:
force: trueвимкнув би саме ту перевірку, яка спрацювала. Клік пішов би по координатах, тест позеленів би — а користувач, який зробить те саме руками, влучить у банер, не в кнопку. Дефект поїде в прод разом із власноруч вимкненим сигналом про нього.- Умовний
isVisible()тут доречний, а в асерті — ні. Це керування потоком: банер може бути, а може й не бути. Але перевіряти результат гетером не можна — він повертає миттєвий зріз і не чекає; тому зникнення банера фіксує саме ретрайнийtoBeHidden(). - Найкраще — узагалі не бачити банер у тесті. Стан згоди зазвичай зводиться до cookie або запису в
localStorage, і його виставляють один раз на контекст. Тоді зникає зі сценарію, а тест перевіряє те, заради чого написаний. - Окрема пастка поруч:
opacity: 0. Прозорий елемент вважається видимим, тож клік по невидимій для ока кнопці пройде без жодногоTimeoutError. Тобто цей клас дефектів автотест сам не ловить — його ловлять перевірки.
Кейс 3. Ревізія локаторів у page object
Успадкований page object для сторінки товару. Тести падають після кожного релізу фронтенду:
// було
export class ProductPage {
readonly addToCart: Locator;
readonly title: Locator;
readonly firstReview: Locator;
readonly price: Locator;
constructor(private page: Page) {
this.addToCart = page.locator('.Button_root__a1b2c.mt-3 > span');
this.title = page.locator('//div[@class="pdp"]/div[2]/h1');
this.firstReview = page.locator('//div[contains(@class,"review")][1]');
this.price = page.locator('xpath=//span[contains(text(), "Ціна: ")]');
}
}
Розбір по рядку — і чим замінити:
| Було | Чому ламається | Стало |
|---|---|---|
.Button_root__a1b2c.mt-3 > span | хеш у класі змінюється з кожною збіркою, mt-3 — утилітарний клас верстки | getByRole('button', { name: 'Додати в кошик' }) |
//div[@class="pdp"]/div[2]/h1 | позиційний шлях: одна вставлена обгортка — і div[2] вже інший | getByRole('heading', { level: 1 }) |
//div[contains(@class,"review")][1] | це не «перший відгук», а всі перші серед сусідів свого батька | getByTestId('review').filter({ hasText: author }) |
//span[contains(text(), "Ціна: ")] | зламається, щойно всередині з'явиться вкладений тег | getByText('Ціна:') або contains(., "Ціна: ") |
// стало
export class ProductPage {
readonly addToCart: Locator;
readonly title: Locator;
readonly price: Locator;
constructor(private page: Page) {
this.addToCart = page.getByRole('button', { name: 'Додати в кошик' });
this.title = page.getByRole('heading', { level: 1 });
this.price = page.getByText('Ціна:');
}
// відгуки лишаються набором; звужуємо за автором, а не за індексом
review(author: string) {
return this.page.getByTestId('review').filter({ hasText: author });
}
}
Що дивитися і чому:
- Хеш у назві файлу — благо, хеш у класі — пастка.
Button_root__a1b2cгенерує , і його стабільність не гарантована нічим. Якщо зачепитися більше немає за що, часткове зіставлення[class^="Button_root"]дає перепочинок, але це паліатив: правильна відповідь — роль або домовленість проdata-testid. //div[contains(@class,"review")][1]— та сама класична помилка індексації. Предикат застосовується всередині кроку, тож вираз означає «усі, хто є першим серед відповідних дітей свого батька». Справді перший збіг записується як(//div[contains(@class,"review")])[1]— але в цьому кейсі індекс не потрібен узагалі: відгук шукається за автором.contains(text(), ...)варто вважати діагнозом. Для<span>Ціна: <b>1 200 грн</b></span>він перевіряє лише перший текстовий вузол. Якщо XPath за текстом усе-таки потрібен, працюєcontains(., ...)— рядкове значення елемента склеює всіх текстових нащадків.- Локатор у полі класу — це безпечно. Він не тримає вузол і не «протухає» між тестами: пошук усе одно відбудеться в момент дії. Тому оголошувати локатори в конструкторі page object можна спокійно, а от зберігати в полі результат
elementHandle— уже ні. - Метод замість поля там, де елемент один із багатьох.
review(author)повертає локатор, звужений фільтром: тест читається як речення, а строгість лишається ввімкненою і далі ловить неоднозначність.
Що таке локатор
- Можу пояснити, що (locator) — це збережена умова пошуку, а не посилання на вузол DOM: пошук відбувається в момент дії, і саме тому класичний
stale element referenceтут майже не трапляється. - Розумію три наслідки відкладеного пошуку: локатор можна перевикористовувати, перемальовування сторінки між оголошенням і дією — норма, а два звертання до одного локатора дають два незалежні пошуки.
Пріоритет локаторів
- Назву всі сім вбудованих локаторів (
getByRole,getByText,getByLabel,getByPlaceholder,getByAltText,getByTitle,getByTestId) і скажу, за чим шукає кожен. - Можу пояснити рекомендацію доки — спиратися на user-facing атрибути й явні контракти — і назвати причину: роль не залежить від верстки, а ланцюжок класів залежить.
- Знаю, що таке дерево доступності (accessibility tree), чому роль майже завжди йде в парі з доступним іменем і звідки береться поділ «інтерактивне за роллю, неінтерактивне за текстом».
- Можу пояснити, чому клікабельний
divбез ролі, якого не знаходитьgetByRole, — це знайдений дефект доступності, а не привід переписати локатор на CSS.
Test id і чужі ієрархії
- Знаю точне формулювання доки про
getByTestId: найстійкіший, але не user-facing — і чим це відрізняється від «найкращий». - Пам'ятаю, що — спільна домовленість QA й розробки, що атрибут за замовчуванням (
data-testid) перевизначається в конфігурації, і щоdata-*— штатний механізм HTML, а не засмічення розмітки. - Знаю, що в Cypress ієрархія інша (шкала
Never/Sparingly/Depends/Always, де текст поступається тест-атрибуту), і вмію пояснити цю розбіжність як різні акценти: стійкість проти близькості до користувача.
CSS і XPath: коли й з якою ціною
- Розумію, чому дока позначає CSS і XPath як
not recommended, знаю, що для них єpage.locator(), і назву два випадки, де вони ще виправдані. - Не плутаю
^=/$=/*=(підрядок) з~=(точне слово зі списку через пробіл) і поясню різницю на прикладі[class~="logo"]. - Знаю обидві класичні пастки XPath:
//button[1]— не перша кнопка на сторінці (перший збіг —(//button)[1]), аcontains(text(), ...)ламається на вкладених тегах, бо в рядок іде лише перший текстовий вузол. - Пам'ятаю межі: локатори працюють у Shadow DOM, але XPath не пробиває, а закриті корені не підтримуються;
:has()частину випадків закриває, проте за Baseline доступний в усіх браузерах лише з грудня 2023-го.
Звуження пошуку і строгість
- Можу пояснити, чому строгість локатора — фіча, а не примха, і чому
strict mode violationіноді сам по собі знахідка: два однакові доступні імені на екрані варто перевірити як дефект. - Знаю штатний шлях пройти строгість (зчеплення до плюс
filter()) і головне правило фільтра: він мусить бути відносним і шукається від збігу вихідного локатора, а не від кореня документа. - Розумію ціну
first()/last()/nth(), знаю, що набір із двадцяти збігів сам по собі нормальний, і що питання «скільки їх?» вирішується ретрайним , а не гетером.
Придатність до дії та force
- Назву всі п'ять перевірок перед кліком: рівно один збіг, видимий, стабільний, отримує події, увімкнений — і додаткову «редагований» для дій уведення.
- Знаю точні означення: непорожній і відсутність
visibility: hiddenдля видимості, той самий bounding box два послідовні кадри анімації для стабільності. - Пам'ятаю пастки означень: нульовий розмір і
display: noneроблять елемент невидимим,opacity: 0вважається видимим, а «увімкнений» ширший за атрибутdisabled— дизейблять також батьківськийfieldsetіaria-disabled="true". - Не читаю
TimeoutErrorна кліку як «елемент не знайдено» — перевіряю оверлей, анімацію, iframe і Shadow DOM — і можу пояснити, щоforce: trueвимикає необов'язкові перевірки, роблячи зеленим звіт, а не продукт.
Квіз
Перед стартом
- Питань: 16
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Локатор (locator) у Playwright — це що саме?



