Розділ 1 з 12
Що таке локатор
Локатор — це не елемент, а спосіб знайти елемент. Він нічого не шукає в момент створення, а шукає заново щоразу, коли ви виконуєте дію чи перевірку. Саме на цьому тримаються автоочікування і стійкість до перемальовування сторінки.
Документація називає локатори центральним елементом автоочікування та повторних спроб у Playwright: локатор описує, як знайти елемент на сторінці в будь-який момент. Рядок const signIn = page.getByRole('button', { name: 'Sign in' }) миттєвий і не звертається до браузера. Пошук відбувається лише під час await signIn.hover(), а потім знову під час await signIn.click(). Якщо між цими викликами фреймворк перемалював кнопку, другий виклик знайде вже нову.
Порівняйте це з ElementHandle, який повертає page.$(). Handle вказує на конкретний DOM-вузол. Коли вузол видаляють і створюють новий, handle продовжує вказувати на старий, відʼєднаний від сторінки. Тому ElementHandle у документації позначений як discouraged.
Друга ключова властивість — строгість. Якщо дія чи перевірка, що працює з одним елементом, знаходить кілька, локатор не обирає перший навмання, а кидає помилку strict mode violation. Детально про це в розділі 7.
Розділ 2 з 12
Вбудовані локатори і їхній пріоритет
Playwright має сім вбудованих методів пошуку. Документація радить ставити на перше місце атрибути, які бачить користувач, і явні контракти з розробниками, а вже потім структуру DOM.
getByRole шукає за явною або неявною роллю доступності і доступною назвою. Документація рекомендує його насамперед, бо він найближчий до того, як сторінку сприймають користувачі й допоміжні технології. getByLabel знаходить поле форми за текстом підпису. getByPlaceholder — поле без підпису, але з placeholder. getByText радять для неінтерактивних елементів на кшталт div, span, p, а для кнопок, посилань і полів краще роль. getByAltText шукає зображення за альтернативним текстом. getByTitle — елемент з атрибутом title. getByTestId шукає за атрибутом data-testid: це найстійкіший до змін тексту варіант, але він не відображає того, що бачить користувач.
Розділ 3 з 12
getByRole детально
Більшість HTML-елементів мають неявну роль: <button> — це button, <h2> — heading. Тому getByRole працює і без атрибутів role у розмітці.
Роль майже завжди уточнюють доступною назвою через опцію name. За замовчуванням назва порівнюється без урахування регістру і як підрядок. exact: true вимагає точного збігу з урахуванням регістру, але пробіли на краях однаково обрізаються. Замість рядка можна передати регулярний вираз. Інші опції відповідають ARIA-станам: checked, disabled, expanded, pressed, selected, level (рівень заголовка), а includeHidden дозволяє знаходити приховані елементи, які за замовчуванням виключаються.
Ролі не замінюють аудит доступності. Документація окремо зазначає, що role-локатори дають ранній сигнал про відповідність ARIA, але не є перевіркою доступності.
Розділ 4 з 12
Текст, label, placeholder, alt, title і test id
У текстових локаторів спільні правила порівняння, і їх варто знати напам'ять: від них залежить, скільки елементів знайде локатор.
Рядок за замовчуванням шукається як підрядок без урахування регістру. { exact: true } вимагає збігу всього рядка з урахуванням регістру. Регулярний вираз дає повний контроль, і для нього exact ігнорується. Пробіли нормалізуються завжди, навіть з exact: кілька пробілів стають одним, переноси рядків — пробілами, пробіли на краях відкидаються. У getByText є ще нюанс: поля <input type="button"> і type="submit" порівнюються за атрибутом value.
getByLabel знаходить поле за текстом пов'язаного <label>, за елементом з aria-labelledby або за атрибутом aria-label. getByTestId за замовчуванням читає data-testid, але атрибут можна змінити в конфігу.
Розділ 5 з 12
CSS, XPath, Shadow DOM і iframe
Коли вбудованих локаторів замало, є page.locator() з CSS чи XPath. Документація радить робити це лише за потреби, бо такі селектори прив'язані до верстки.
Playwright сам розпізнає CSS і XPath, тож префікси css= і xpath= необов'язкові. Довгі ланцюжки на кшталт #tsf > div:nth-child(2) > div.A8SBwf … документація прямо наводить як погану практику: вони ламаються від будь-якої зміни DOM. Якщо CSS неминучий, шукайте короткий селектор за стабільним атрибутом, а краще домовтеся з розробниками про data-testid.
Shadow DOM
Усі локатори за замовчуванням бачать елементи всередині Shadow DOM, наче тіньового кореня немає. Винятків два: XPath не проходить у shadow root, а закриті (closed-mode) shadow root не підтримуються.
iframe
Вміст iframe — окремий документ, і звичайні локатори сторінки його не бачать. Щоб «увійти» у фрейм, використовують page.frameLocator(selector) або, маючи локатор самого iframe, locator.contentFrame(). Далі працюють ті самі getByRole, getByText тощо.
Розділ 6 з 12
Фільтри, ланцюжки й оператори
Коли схожих елементів багато, локатор звужують: ланцюжком від контейнера до потрібного елемента, фільтром за текстом чи вмістом або комбінацією двох локаторів.
filter({ hasText }) лишає елементи, що містять текст десь усередині, зокрема в нащадках. Рядок порівнюється як підрядок без урахування регістру. hasNotText робить навпаки. has і hasNot фільтрують за наявністю нащадка, що відповідає іншому локатору. Важлива деталь з документації: внутрішній локатор шукається відносно зовнішнього, а не від кореня документа. Тому has: page.getByRole('list').getByText('…') усередині listitem не спрацює. filter({ visible: true }) або скорочення .visible() лишає тільки видимі елементи, але документація радить спершу пошукати надійніший спосіб відрізнити елемент.
Методи getBy… є і в Locator, тому їх можна ланцюжити: product.getByRole('button') шукає кнопку лише всередині продукту. a.and(b) знаходить елементи, які відповідають обом локаторам одночасно. a.or(b) — будь-якому з них. Якщо обидва варіанти присутні, or поверне два елементи і може спричинити strict mode violation.
Розділ 7 з 12
Строгість і робота зі списками
Локатори строгі: операція, що має на увазі один елемент, кидає помилку, якщо знайдено кілька. Операції над множиною елементів працюють з будь-якою кількістю.
Строгість захищає від тихих помилок: якщо на сторінці дві кнопки «Delete», тест не повинен навмання клікнути першу. Розв'язок — уточнити локатор роллю, назвою, фільтром чи ланцюжком від контейнера. first(), last() і nth(i) теж знімають неоднозначність, але документація застерігає: коли сторінка змінюється, позиційний локатор може почати вказувати на зовсім інший елемент.
Перевірки для списків
Для списків є дві основні web-first перевірки. toHaveCount(n) перевіряє кількість елементів. toHaveText([...]) з масивом перевіряє тексти всіх елементів по порядку. Обидві повторюють перевірку, доки список не прийде до очікуваного стану.
Перебір елементів
Документація відносить перебір до рідкісних випадків. locator.all() не чекає на елементи, а одразу повертає те, що є на сторінці. Якщо список змінюється, результат буде непередбачуваним. Тому спочатку дочекайтеся повного списку, наприклад через toHaveCount, і лише потім перебирайте.
Розділ 8 з 12
Auto-waiting: перевірки перед дією
Перед кожною дією Playwright чекає, доки елемент стане придатним для неї (actionability checks). Лише коли всі потрібні перевірки пройдено, дія виконується. Якщо цього не сталося в межах тайм-ауту, дія падає з TimeoutError.
Для click() документація перелічує повний набір: локатор знаходить рівно один елемент, елемент Visible, Stable, Receives Events і Enabled. Кожен термін має точне визначення. Visible — непорожній bounding box і без visibility:hidden. Отже, елементи нульового розміру чи з display:none невидимі, а елемент з opacity:0 видимий. Stable — bounding box не змінювався щонайменше два послідовні кадри анімації. Receives Events — у точці дії саме цей елемент є ціллю вказівника, а не, скажімо, оверлей поверх нього. Enabled — елемент не disabled: не має атрибута disabled, не лежить у вимкненому <fieldset> і не є нащадком елемента з aria-disabled=true. Editable — елемент enabled і не readonly.
Різні дії потребують різних перевірок. click, check, dblclick, tap перевіряють перші чотири, hover і dragTo — без Enabled, fill і clear — Visible, Enabled і Editable. press, focus, setInputFiles і dispatchEvent не роблять жодної з п'яти перевірок, але, як і всі дії, чекають, поки локатор знайде елемент.
force: true — не спосіб полагодити тест
Проблема. Клік падає через оверлей чи анімацію, і в код додають { force: true }.
Чому це погано. force повністю обходить actionability-перевірки. Тест клікає туди, куди користувач клікнути не зміг би, і перестає ловити реальний баг: оверлей, що не зникає, чи кнопку, яка так і не стала активною.
Рішення. Спершу з'ясуйте, яка перевірка блокує: це видно в логу дії та в трейсі. Потім дочекайтеся потрібного стану сторінки, наприклад закрийте банер або перевірте toBeHidden() для оверлею.
Коли force доречний. Рідко: коли поведінка навмисно відрізняється від звичайної, наприклад прихований нативний input під кастомним контролом, і ви свідомо перевіряєте саме цей input.
Опція trial: true працює навпаки: виконує лише перевірки і не робить саму дію. Це зручно, щоб дочекатися готовності елемента без кліку.
Розділ 9 з 12
Очікування в assertions і явні очікування
Дії чекають на actionability, web-first assertions повторюють перевірку до тайм-ауту. Між цими двома механізмами майже не лишається місця для ручних очікувань.
Auto-retrying assertions — toBeVisible, toBeHidden, toBeEnabled, toBeChecked, toHaveText, toHaveValue, toHaveCount, toHaveURL та інші — повторюють перевірку, доки умова не виконається або не мине expect.timeout. Методи-запити isVisible(), isEnabled(), isChecked(), isHidden() повертають стан негайно і не чекають. Best practices наводять це як типову помилку: expect(await locator.isVisible()).toBe(true) не почекає жодної секунди.
locator.waitFor
Явне очікування стану є: waitFor({ state }) зі станами 'attached', 'detached', 'visible' (за замовчуванням) і 'hidden'. Воно потрібне рідко, наприклад у хелпері, який має дочекатися зникнення спінера перед подальшою логікою. Для перевірок у тесті використовуйте assertions.
Чого не варто робити
Проблема. У тестах з'являються page.waitForTimeout(3000), page.waitForSelector() чи waitForLoadState('networkidle') «для надійності».
Чому. Фіксована пауза або занадто коротка, і тест нестабільний, або занадто довга, і тест повільний. У документації waitForTimeout призначений лише для налагодження, а waitForSelector і networkidle позначені як discouraged.
Рішення. Сформулюйте, якого видимого стану ви чекаєте, і перевірте його web-first assertion: await expect(page.getByRole('table')).toBeVisible().
Коли пауза припустима. Лише тимчасово під час налагодження, і в комміт вона не потрапляє.
Розділ 10 з 12
Інструменти для підбору локаторів
Локатори не обов'язково писати наосліп. Playwright має три інструменти, які пропонують локатор і показують, що він знаходить.
Генератор тестів (npx playwright codegen <url>) відкриває браузер і Playwright Inspector. За документацією, він обирає найкращий локатор, віддаючи перевагу ролі, тексту й test id. Якщо знаходиться кілька елементів, генератор уточнює локатор, щоб той був унікальним. Щоб лише підібрати локатор, зупиніть запис кнопкою Record, і стане доступною кнопка Pick Locator: наведіть на елемент і скопіюйте локатор або відредагуйте його в Inspector, спостерігаючи підсвічування на сторінці.
В UI mode є така сама кнопка Pick locator для DOM-знімків трейсу і пісочниця, де можна змінювати локатор і бачити, що він знаходить. Розширення VS Code вміє записувати тести, підбирати локатори й підсвічувати в браузері елементи, які знаходить локатор під курсором у коді.
Згенерований локатор — чернетка, а не остаточне рішення. Перевірте, чи не прив'язаний він до випадкового тексту чи позиції, і чи читається він як опис для людини. Для складних локаторів у Page Object корисний locator.describe('Subscribe button'): цей опис з'являється у трейсі та звітах замість технічного селектора.
Розділ 11 з 12
Типові помилки
Більшість нестабільних тестів спричинені не повільною сторінкою, а локатором, який знаходить не те або не стільки, або ручним очікуванням замість вбудованого.
Найчастіше трапляються такі помилки. Довгі CSS-ланцюжки, прив'язані до верстки. getByText для кнопок замість ролі. Неоднозначний локатор, «виправлений» через first(). has з локатором від кореня сторінки. isVisible() всередині expect. Фіксовані паузи. force: true як заплатка. Перебір all() до того, як список завантажився.
Розділ 12 з 12
Шпаргалка
Усе основне на одному екрані. Після шпаргалки — підсумковий квіз для самоперевірки.
Три правила на пам'ять. Локатор описує, як знайти елемент, і шукає його заново під час кожної дії. Найкращий локатор описує елемент так, як його бачить користувач, і знаходить рівно один елемент. Чекати вручну майже ніколи не потрібно: дії чекають на actionability, а web-first assertions — на очікуваний стан.
Самоперевірка
Підсумковий квіз
15 питань за всіма розділами. Після кожної відповіді побачите пояснення, а в кінці — список тем, які варто повторити.