07 · Інструменти автоматизації
Playwright: перевірки (assertions) і автоочікування (auto-waiting)
Зміст
Попередня глава «Playwright: локатори і дії з елементами» закінчилася на тому, що перед кліком інструмент щось сам перевіряє. Ця глава — про те, що саме він перевіряє, чого не перевірить ніколи і як із цих двох фактів народжується або зникає (flakiness). Бо «тест упав по таймауту» — не діагноз, а симптом: у кілька, вони незалежні, і поки ви не знаєте, який саме спрацював, будь-яка правка конфіга — гадання.
Друга складність теми — тут легко зробити помилку, яку не видно в коді. Ретрайний і ручна перевірка виглядають майже однаково, різняться на один await — і одна з них чекає, а друга ні. Так само з таймаутами: збільшити «той, що на очах» простіше, ніж зʼясувати, який саме вичерпався. Тому тема й працює як індикатор: людина розуміє механізм або підкручує числа, доки не позеленіє.
Автоочікування: набір перевірок перед дією
Playwright не «спить перед кліком». Перед кожною дією він виконує набір перевірок придатності (actionability), автоматично чекає, поки всі релевантні пройдуть, і лише тоді виконує запитану дію. Якщо в межах таймауту перевірки не пройшли, дія падає з TimeoutError.
Для locator.click() перевіряється пʼять умов:
| Перевірка | Що означає |
|---|---|
| локатор вказує рівно на один елемент | два збіги — це помилка (strict mode), а не «беремо перший» |
| Visible | має непорожній bounding box і не має обчисленого visibility:hidden |
| Stable | зберігає той самий bounding box щонайменше два послідовні кадри анімації |
| Receives Events | елемент є цільовим для вказівникової події в точці дії |
| Enabled | не задизейблений |
Деталі, які й дають більшість непорозумінь:
- Елементи нульового розміру й
display:noneневидимі, аopacity:0вважається видимим. Прозорий елемент пройде перевірку видимості й отримає клік. - Задизейбленим елемент робить атрибут
disabled, батьківськийfieldset[disabled]абоaria-disabled=true. - Набір перевірок залежить від дії: для дій уведення в ньому є Editable — увімкнений і не
readonly. - «Отримує події» — це буквально перевірка цілі: при кліку в точку (10;10) інструмент дивиться, чи не перехопить клік інший елемент, зазвичай оверлей. Саме ця перевірка ловить тост, спінер або модалку, що зʼїдає клік.
при цьому — не сам елемент, а рецепт, як його знайти: пошук відбувається в момент дії, а не в момент оголошення. Тому взагалі можливе — інструмент має право перешукати ще раз.
Опція force вимикає необовʼязкові перевірки — зокрема перестає контролювати, чи справді елемент отримує клік. Технічно це дозволено, практично — це спосіб зробити тест зеленим, приховавши дефект: якщо клік перехоплює оверлей, force не прибирає оверлей, а лише перестає про нього повідомляти. Правильна реакція — закрити оверлей у (детальніше — у главі про локатори і дії).
Чого автоочікування не чекатиме
Автоочікування закриває рівно одну задачу: «елемент існує, видимий і придатний до дії». Воно не знає нічого про прикладні стани — чи навішений обробник після гідратації (hydration), чи завершився фоновий запит, чи вже оновилися дані в таблиці. Кнопка може бути видимою, стабільною й увімкненою, а клік по ній — не робити нічого, бо JavaScript ще не підписався на подію. Формально дія пройшла, фактично нічого не сталося, і падіння прилетить пізніше й в іншому місці — класичний профіль .
Спроба закрити цю прогалину «загальним» сигналом мережевої тишини — теж пастка: стан networkidle офіційна дока позначає як DISCOURAGED і прямо радить замість нього спиратися на вебасерти. Причина проста: відсутність запитів не означає, що інтерфейс домалював результат, а сторінка з постійним фоновим (polling) чи аналітикою може взагалі не дати потрібного вікна тиші.
Що працює натомість:
- Асерт на видимий наслідок. Перевіряйте те, що бачить користувач: що ближче тест до способу використання продукту, то більше довіри він дає. Мережева відповідь — обʼєкт синхронізації другого вибору, коли зміна в інтерфейсі надто тонка, щоб надійно за неї зачепитися.
- Очікування конкретної відповіді — до дії, що її тригерить. очікування створюють перед кліком: інакше між дією і викликом очікування є вікно, у яке швидка відповідь прослизне непоміченою. Це знову гонка, тільки написана вручну (механіка — у главі про мережу).
- Розуміння, на що ви синхронізуєтеся. Стан
domcontentloadedнастає, коли розібрано HTML, а підресурси й дані ще можуть вантажитися;load— коли завантажено й підресурси. Ні те, ні те не означає «застосунок готовий». - Прикладний маркер готовності. Домовтеся з командою про явний атрибут —
data-hydrated,aria-busy="false"— і чекайте на нього. Це річ конкретного застосунку, а не стандарт, тому й домовленість потрібна.
Окремо стоять анімації: вони ламають і клік (елемент їде, перевірка стабільності перечікує), і візуальне порівняння (у кадр потрапляє проміжний стан). Тому знімки роблять із заглушеними анімаціями — за це відповідає опція animations: 'disabled'; детальніше — у главі про візуальне тестування.
Web-first асерти: перевірка, що вміє чекати
Автоматизувати треба не лише виконання, а й перевірку: порівняння фактичного результату з очікуваним ISTQB CTAL-TAE радить робити саме асертами інструмента, а до статусу висуває дві вимоги — статус має визначатися коректно, а при failed потрібна додаткова інформація про причину (наприклад, ). Це та сама логіка, що стоїть за й артефактами падіння в главі про дебаг і репортинг.
Web-first асерт — це асерт, який чекає, доки умова виконається. Якщо повідомлення зʼявляється за пів секунди, toBeVisible() почекає й повторить перевірку. Ключова властивість: такі асерти асинхронні, тож без await вони просто не працюють.
// Чекає й повторює перевірку до таймауту асерту
await expect(page.getByRole('alert')).toHaveText('Замовлення створено');
// Не чекає жодної секунди: дивиться стан просто зараз і одразу повертає
expect(await page.getByRole('alert').isVisible()).toBe(true);
Другий рядок — один із найпоширеніших способів отримати -тест, не роблячи нічого «неправильного» на вигляд: await стоїть усередині expect, а не перед ним, тому очікування зникло.
Асерти діляться на дві групи:
| Група | Поведінка | Ціна |
|---|---|---|
Ретрайні (auto-retrying: toHaveText, toBeVisible, toHaveScreenshot…) | повторюють перевірку, доки вона не пройде або не вичерпається таймаут асерту | обовʼязковий await |
Неретрайні (toBe, toEqual, toContain…) | перевіряють будь-яку умову один раз | на асинхронній сторінці саме вони роблять тест флакі |
Офіційна порада однозначна: віддавати перевагу ретрайним асертам, коли це можливо, а для складніших умов брати expect.poll або expect.toPass. Неретрайні матчери не «погані» — вони просто нічого не знають про час, і на сторінці, яка показує дані асинхронно, це і є джерело недетермінізму.
Візуальний асерт живе за тими самими правилами: toHaveScreenshot теж ретрайний і чекає, доки два послідовні знімки сторінки збіжаться, і лише тоді порівнює останній з еталоном.
Ієрархія таймаутів
У ранері кілька налаштовуваних таймаутів на різні задачі. Це не одна ручка з різними назвами — рівні незалежні, і саме тому питання «який таймаут спрацював» має сенс.
| Рівень | За замовчуванням | Що обмежує |
|---|---|---|
| Таймаут тесту | 30 с | тіло тесту + налаштування фікстур + хуки beforeEach |
| Таймаут асерту | 5 с | кожен ретрайний асерт окремо |
beforeAll / afterAll | дорівнює тестовому | окремий таймаут; часу з тестом ці хуки не ділять |
| Таймаут дії | відсутній | обмежує лише таймаут тесту |
| Таймаут навігації | відсутній | те саме |
| Таймаут фікстури | відсутній | те саме |
| Глобальний таймаут | відсутній | увесь прогін цілком |
Головне правило дока формулює прямо: таймаут асерту не має стосунку до таймауту тесту. Наслідок читається в обидва боки. Збільшили таймаут тесту — ретрайний асерт від цього більше спроб не отримав. Збільшили таймаут асерту — тест, який уже вичерпав свої 30 секунд, це не врятує.
Візуальний асерт вписується в ту саму схему: у toHaveScreenshot власний таймаут на , і за замовчуванням він дорівнює таймауту асерту. Такий асерт можна ще й скасувати сигналом — для тесту скасування виглядає як таймаут.
Розділ низькорівневих таймаутів у доці супроводжується застереженням, яке варто цитувати на співбесідах: якщо ви прийшли сюди через флакі-тест, рішення майже напевно не тут. Це канонічна відповідь на «давайте поставимо таймаут більший». Для окремого справді повільного тесту передбачені важелі рівня тесту — власний таймаут через test.setTimeout() або test.slow(), а не правка глобального конфіга (конфігурація й проєкти — у главі про фікстури й конфіг).
Ще одна межа, про яку часто плутаються: у бібліотеки й у ранера різні моделі. Бібліотека дає 30 секунд на більшість операцій. Ранер, навпаки: більшість операцій власного таймауту не мають, зате кожен тест має таймаут, який його валить. Тому досвід «у нас же було 30 секунд на клік» із бібліотечного скрипта в тести не переноситься (архітектурна різниця — у главі про архітектуру).
Мʼякі асерти: expect.soft
За замовчуванням впалий асерт зупиняє виконання тесту. Мʼякий асерт (expect.soft) виконання не зупиняє, але позначає тест як провалений.
await expect.soft(page.getByTestId('total')).toHaveText('1 200 ₴');
await expect.soft(page.getByTestId('delivery')).toHaveText('Нова пошта');
await expect.soft(page.getByTestId('eta')).toHaveText('2 дні');
Навіщо: за один прогін ви бачите всі три розбіжності, а не лише першу. Для сторінок із десятком полів це різниця між одним циклом розбору й трьома.
Дві межі, які треба знати. По-перше, посеред тесту можна перевірити, чи були мʼякі падіння, і вийти раніше — це потрібно саме тому, що тест продовжує виконуватися: наступні кроки підуть на стані, у якому перевірена умова не виконалася, і сенсу в їхніх результатах може вже не бути. По-друге, мʼякі асерти працюють лише з ранером Playwright — у бібліотечному сценарії їх немає.
expect.poll і toPass: коли ретрайного матчера не досить
Ретрайні матчери покривають стани сторінки. А якщо чекати треба на щось поза DOM — готовність сервісу, зміну в БД, появу листа? Для цього є дві обгортки.
expect.poll перетворює будь-який синхронний expect на асинхронний полінг. Канонічний приклад у самій доці — полінг до статусу HTTP 200:
await expect.poll(async () => {
const response = await page.request.get('/api/health');
return response.status();
}, {
timeout: 10_000, // за замовчуванням 5 с; 0 вимикає стелю
intervals: [1_000, 2_000, 10_000],
}).toBe(200);
Дефолтні інтервали — [100, 250, 500, 1000] мс, після чого повторюється останній. Дефолтний таймаут — 5 секунд, а timeout: 0 вимикає його повністю. Остання опція виглядає як надійність, а насправді перетворює фейл на зависання: тест, який не може впасти, перестає бути перевіркою.
expect.toPass повторює блок коду, доки він не пройде цілком, — зручно, коли всередині кілька дій і асертів. Тут є окрема пастка, названа в доці прямо: toPass не успадковує налаштований таймаут асерту, за замовчуванням у нього таймаут 0, тож задавати його треба явно.
Скільки ставити? Тут корисна порада з протилежного боку: стеля має бути навмисно високою, а не «реалістичною» — маленький інтервал полінгу й так обмежує втрачений час, тож висока стеля майже не коштує нічого й прибирає хибні падіння на повільному CI. Але скінченною вона бути зобовʼязана. Ще одна деталь на подумати: функція всередині полінгу викликається багато разів, тож вона мусить бути безпечною для повторення — полінг на створенні замовлення створить N замовлень.
Мʼякий асерт із полінгом поєднується: тоді тест продовжиться навіть при невдачі всередині poll — і фейл треба ловити окремо.
Чому waitForTimeout — антипатерн
Метод page.waitForTimeout() існує й робить рівно те, що обіцяє. Але офіційна дока позначає його як Discouraged і формулює правило без помʼякшень: ніколи не чекати таймаутом у продакшн-тестах, бо тести, що чекають час, флакі за побудовою. Дозволене застосування назване одне — дебаг.
Аргумент проти (hard wait) — не стилістичний, а арифметичний, і дока Selenium розкладає його на дві половини. Код не може знати, скільки саме треба чекати: замало — тест падає на повільному прогоні; забагато, та ще й у кожному потрібному місці — тривалість сесії стає непідйомною. Пауза синхронізується з годинником, а не із застосунком, тому вона не буває «правильною» — лише випадково достатньою. Там же названо й корінь проблеми: гонка між браузером і кодом тесту — одна з головних причин флакі-тестів.
Заміна голої паузи — не довша пауза, а умова. Дока перелічує саме це: дії локатора й вебасерти, які чекають автоматично, плюс сигнали — мережеві події, поява селектора. Тобто весь інструментарій, розібраний вище: автоочікування перед дією, ретрайний асерт на наслідок, expect.poll для складнішої умови.
// Антипатерн: тест синхронізується з годинником
await page.getByRole('button', { name: 'Зберегти' }).click();
await page.waitForTimeout(3_000);
await expect(page.getByRole('alert')).toBeVisible();
// Те саме, але синхронізовано із застосунком
await page.getByRole('button', { name: 'Зберегти' }).click();
await expect(page.getByRole('alert')).toHaveText('Збережено');
Один випадок варто назвати чесно, і разом із його статусом. Коли перевіряють відсутність зміни — «за N секунд нічого не сталося», — умови «нічого не сталося» в асерті не існує, тож рука тягнеться до паузи. Наша практика (не канон): джерела такого винятку не формулюють — ні дока Selenium, ні дока Playwright його не називають, — тому подавати його правилом не можна. Чесніший хід: знайти спостережуваний сигнал, після якого відсутність зміни вже щось означає (прихід конкретної відповіді, завершення запиту), і перевіряти стан після нього; час лишається останнім засобом, а не виправданим винятком.
Заборона на голі паузи не є особливістю одного інструмента. У CodeceptJS очікування так само вбудоване в дії, а невдалий крок повторюється автоматично — рівень ретраю там нижчий за тест (деталі — у главі про CodeceptJS). Політика ретраїв і засоби боротьби з флаком на рівні конфігурації — у главі «Боротьба з флаком засобами інструмента», а браузерна механіка синхронізації — у главі «Практичні сценарії AQA: флак і синхронізація».
Типові помилки
- «Упало по таймауту — збільшимо таймаут тесту» → якщо вичерпався таймаут асерту, тестовий не додасть жодної спроби: рівні не повʼязані. Дока сама попереджає, що для флаку рішення майже напевно не в низькорівневих таймаутах.
- «Кнопка видима, а клік не працює — баг застосунку» → зазвичай спрацювала перевірка «отримує події»: клік перехоплює оверлей, тост або спінер. Це корисне падіння, і лікується воно закриттям оверлея в передумові, а не
force. - «Елемент прозорий, отже, невидимий» →
opacity:0вважається видимим. Невидимими роблять нульовий розмір,display:noneіvisibility:hidden. expect(await locator.isVisible()).toBe(true)→ виглядає як асерт із очікуванням, а насправді не чекає жодної секунди:awaitстоїть усерединіexpect, а не перед ним.- «Асерт без
await— просто негарно» → ретрайні асерти асинхронні: безawaitвони не працюють, і тест зеленіє, нічого не перевіривши. - «
toPassвізьме мійexpect.timeoutіз конфіга» → не візьме: уtoPassза замовчуванням таймаут 0, його треба задати явно. - «Мʼякі асерти не впали — тест зелений» → мʼякий асерт не зупиняє виконання, але позначає тест провальним; а наступні кроки йдуть на стані, у якому умова не виконалася.
- «
timeout: 0у полінгу — це надійніше» → це фейл, який перетворюється на зависання. Стеля має бути високою, але скінченною. - «Дочекаюсь
networkidle— і сторінка готова» → стан позначено як DISCOURAGED, і дока радить вебасерти: тиша в мережі не означає, що інтерфейс домалював результат. - «Перед скріншотним асертом треба пауза, бо він ловить кадр посеред анімації» → не треба:
toHaveScreenshotретрайний і сам чекає, доки два послідовні знімки збіжаться, а анімації в ньому вимкнені за замовчуванням — скінченні перемотуються до кінця, нескінченні скасовуються до початкового стану.
Підсумок
- Автоочікування — про придатність елемента, не про готовність застосунку. Перед дією перевіряється однозначність локатора, видимість, стабільність, отримання подій і увімкненість; гідратація, фонові запити й оновлення даних до цього переліку не входять.
- Ретрайний асерт чекає, ручна перевірка — ні.
await expect(locator).toHaveText()повторює перевірку до таймауту асерту;expect(await locator.isVisible())дивиться стан просто зараз. Ретрайні асерти асинхронні — безawaitвони не працюють. - Таймаути незалежні. Тест — 30 секунд (тіло + +
beforeEach), асерт — 5 секунд,beforeAll/afterAll— окремо, дії/навігація/фікстура/глобальний — за замовчуванням відсутні. Таймаут асерту не має стосунку до таймауту тесту. - Складніша умова — обгортка, а не пауза.
expect.pollдля одного значення,toPassдля блоку; стеля висока, але скінченна, іtoPassсвій таймаут не успадковує. waitForTimeout— лише для дебагу. Тести, що чекають час, флакі за побудовою; заміна — умова: автоочікування, вебасерт, сигнал. Спостереження відсутності зміни джерельним винятком не є — це наша практика, і сигнал шукають навіть там.
Можливі питання
- «Що таке auto-waiting і чого він не чекає?» — інтервʼюер дивиться, чи ви розводите два рівні: придатність елемента до дії (це інструмент) і готовність застосунку (це ваш явний сигнал). Сильна відповідь називає кілька перевірок поіменно й одразу — межу.
- «Які таймаути ти знаєш?» — перевіряють саме розділення рівнів. «30 секунд» без назви рівня — половина відповіді; очікують «тест — 30, асерт — 5, і вони не повʼязані».
- «Чим
await expect(locator).toBeVisible()відрізняється відexpect(await locator.isVisible())?» — питання на розуміння ретраю. Відповідь у одному реченні: перший чекає й повторює, другий дивиться один раз і не чекає. - «Чому не можна просто поставити паузу? А коли можна?» — чекають дві речі: аргумент «тести, що чекають час, флакі за побудовою» (і що це слова доки, а не ваш смак) плюс чесний виняток — спостереження відсутності зміни та дебаг.
- «Коли доречний
expect.soft?» — хочуть побачити, що ви розумієте ціну: тест іде далі, тож мʼякий асерт годиться для набору незалежних перевірок одного екрана, а не там, де наступні кроки залежать від перевіреної умови. - «Чим
expect.pollвідрізняється відtoPass?» — одне значення проти блоку коду; бонусні бали за згадку, щоtoPassне успадковує таймаут асерту. - «Тест падає на кліку, локатор правильний — що робиш?» — оцінюють порядок розбору, а не здогад: який саме таймаут спрацював → чи асерт був ретрайний → чи не перехоплює клік оверлей → і лише потім розмова про числа. Відповідь «ставлю
force/ збільшую таймаут» тут читається як червоний .
Джерела
Автоочікування: набір перевірок перед дією
- Playwright — Auto-waiting (actionability) — перелік перевірок придатності перед дією, дослівні означення
Visible/Stable/Receives Events/Enabled/Editable,TimeoutErrorі наслідок опціїforce. - Playwright — Locators — локатор як рецепт пошуку, який резолвиться в момент дії.
Чого автоочікування не чекатиме
- Playwright — Auto-waiting (actionability) — межі автоочікування: набір перевірок стосується придатності елемента, а не готовності застосунку.
- Playwright — class Page —
networkidleпозначено як DISCOURAGED із порадою спиратися на вебасерти; проміс очікування відповіді створюється до дії. - MDN — Document: DOMContentLoaded event — різниця між
domcontentloadedіload. - Testing Library — Guiding Principles — що ближче тест до способу використання продукту, то більше довіри він дає.
- Playwright — Visual comparisons (test snapshots) — опція
animations: 'disabled'для знімків.
Web-first асерти: перевірка, що вміє чекати
- ISTQB® CTAL-TAE Syllabus v2.0 — §6.1.1: порівняння фактичного з очікуваним найкраще робити асертами інструмента; статус має бути коректним, а при
failedпотрібна додаткова інформація про причину. - Playwright — Best Practices — web-first асерт чекає й повторює;
isVisible()усерединіexpect«не чекає жодної секунди». - Playwright — Test assertions (expect.poll, toPass) — поділ на ретрайні й неретрайні матчери, обовʼязковий
await, порада надавати перевагу ретрайним. - Playwright — API: PageAssertions —
toHaveScreenshotчекає збігу двох послідовних знімків.
- Playwright — Timeouts — кілька незалежних таймаутів, 30 с на тест і що в них входить, окремий таймаут
beforeAll/afterAll, відсутні за замовчуванням низькорівневі таймаути, застереження про флак,test.setTimeout()іtest.slow(). - Playwright — Test assertions (expect.poll, toPass) — таймаут ретрайного асерту 5 с за замовчуванням.
- Playwright — Library — модель бібліотеки: 30 с на більшість операцій проти таймауту на тест у ранері.
- Playwright — API: PageAssertions — візуальний асерт має власний таймаут; скасування сигналом виглядає як таймаут.
- Playwright — Test assertions (expect.poll, toPass) — звичайний асерт зупиняє тест, мʼякий не зупиняє, але робить його провальним; перевірка стану мʼяких падінь посеред тесту; обмеження «лише з ранером».
expect.poll і toPass: коли ретрайного матчера не досить
- Playwright — Test assertions (expect.poll, toPass) —
expect.pollперетворює синхронний асерт на полінг, приклад ізHTTP 200, дефолтний таймаут 5 с іtimeout: 0, інтервали[100, 250, 500, 1000],toPassіз власним таймаутом 0, поєднання зexpect.soft. - Martin Fowler — Eradicating Non-Determinism in Tests — стеля очікування має бути навмисно високою, бо малий інтервал полінгу обмежує втрачений час; кращий за полінг, але таймаут потрібен і йому.
Чому waitForTimeout — антипатерн
- Playwright — page.waitForTimeout (Discouraged) — метод позначено як Discouraged: «Never wait for timeout in production», тести, що чекають час, флакі за побудовою; дозволено лише для дебагу; заміна — дії локатора, вебасерти й сигнали.
- Selenium — Waiting Strategies — подвійна вада
sleep(замало / непідйомно довго), гонка як одна з головних причин флакі-тестів, два механізми синхронізації як «better». - Martin Fowler — Eradicating Non-Determinism in Tests — пряма заборона: «Never use bare sleeps to wait for asynchonous responses: use a callback or polling».
- CodeceptJS — Basics — очікування вбудоване в дії, невдалий крок повторюється автоматично.
- Playwright — Timeouts — незалежність рівнів («Assertion timeout is unrelated to the test timeout») і застереження, що для флаку рішення шукати не тут.
- Playwright — Auto-waiting (actionability) — перевірка «отримує події» проти оверлея,
opacity:0як видимий, наслідокforce. - Playwright — Best Practices — ручна перевірка
isVisible()не чекає жодної секунди. - Playwright — Test assertions (expect.poll, toPass) — обовʼязковий
awaitдля ретрайних асертів, таймаут 0 уtoPass, семантикаexpect.soft,timeout: 0у полінгу. - Playwright — class Page —
networkidleяк DISCOURAGED. - Playwright — API: PageAssertions —
toHaveScreenshotчекає збігу двох послідовних знімків; анімації вимкнені за замовчуванням (скінченні перемотуються, нескінченні скасовуються до початкового стану).
- Playwright — Auto-waiting (actionability) — склад перевірок придатності та їхні межі.
- Playwright — Test assertions (expect.poll, toPass) — ретрайні проти неретрайних,
expect.soft,expect.poll,toPassі його таймаут. - Playwright — Timeouts — склад і незалежність рівнів таймаутів.
- Playwright — page.waitForTimeout (Discouraged) — статус методу й формулювання «inherently flaky».
- Selenium — Waiting Strategies — подвійна ціна фіксованої паузи й механізми синхронізації, які дока називає кращими.
- Playwright — Timeouts — рівні таймаутів і застереження, що збільшення таймауту флаку не лікує.
- Playwright — Auto-waiting (actionability) — перевірки придатності та наслідок
force. - Playwright — Best Practices — різниця між web-first асертом і ручною перевіркою.
- Playwright — Test assertions (expect.poll, toPass) —
expect.soft,expect.poll,toPassі його таймаут. - Playwright — page.waitForTimeout (Discouraged) — «Tests that wait for time are inherently flaky» і дозволене застосування для дебагу.
Відео до теми
Що таке автоочікування (auto-waiting) у Playwright?
Це вбудований набір умов, які інструмент перевіряє перед кожною дією й чекає, поки вони не виконаються. Жодного sleep під капотом немає: замість «поспати три секунди» інструмент у циклі перепитує стан елемента, і щойно всі релевантні умови виконані — робить клік чи введення. Якщо за відведений час умови так і не зійшлися, дія завершується помилкою TimeoutError. Працює це завдяки тому, що — не збережене посилання на вузол DOM, а опис, за яким елемент шукають щоразу заново в момент дії. Практичний наслідок: більшість «зачекай, поки кнопка домалюється» у коді тесту зайві — вони дублюють те, що інструмент уже робить безкоштовно. А от «зачекай, поки застосунок довантажить дані» не покриває — про це нижче.
Які саме перевірки виконуються перед кліком?
Для locator.click() їх пʼять. Перша — однозначність: локатор має дати рівно один збіг, два збіги вважаються помилкою (strict mode), а не приводом узяти перший. Далі йде видимість: елемент має мати непорожній і не бути схованим через visibility:hidden. Третя — стабільність: розміри й позиція не змінюються протягом двох послідовних кадрів анімації, тобто елемент уже не «їде». Четверта — отримання подій: у точці, куди піде клік, цільовим має бути саме цей елемент, а не хтось, хто його накрив. Пʼята — увімкненість. Для введення тексту додається ще одна умова, Editable: поле мусить бути увімкненим і без readonly. Знати цей перелік поіменно варто хоча б тому, що кожна перевірка дає свій характерний профіль падіння.
Що робить елемент невидимим для Playwright, а що ні?
Невидимим елемент роблять три речі: нульовий розмір, display:none і visibility:hidden. Найпопулярніша пастка — opacity: 0: прозорий елемент з інструмента цілком видимий, бо bounding box у нього нормальний, а видимість властивість opacity не змінює. Тому напівпрозорий чи повністю прозорий оверлей спокійно пройде перевірку і навіть отримає клік. З цього ж боку живе дзеркальна проблема: користувач елемента не бачить, а тест з ним успішно «працює», і функціональна дірка лишається непокритою. Якщо треба перевірити саме «користувач цього не бачить», прозорість краще окремо — через обчислений стиль, а не сподіватися на toBeHidden().
Чим web-first асерт відрізняється від звичайної перевірки?
Web-first асерт уміє чекати: він повторює перевірку, поки умова не виконається або поки не витече асерту. Якщо тост зʼявляється через 400 мс, toHaveText() цього навіть не помітить — просто перепитає ще раз і пройде. Звичайний матчер працює за іншою моделлю: він порівнює те, що йому передали, один раз і одразу віддає результат. На сторінці, де дані приходять асинхронно, це і є джерело недетермінізму — тест вимірює стан у випадковий момент. Ключова технічна деталь: ретрайні асерти асинхронні, тому без await вони фізично не можуть чекати — просто нікому не потрібен, і тест зеленіє, нічого не перевіривши. Офіційна рекомендація однозначна: там, де є ретрайний матчер, брати ретрайний матчер.
Чим await expect(locator).toBeVisible() відрізняється від expect(await locator.isVisible())?
Різниця в тому, де стоїть await, і вона змінює семантику повністю. У першому варіанті очікується сам асерт — він у циклі перепитує стан елемента до свого таймауту. У другому await застряг усередині: спершу виконується isVisible(), який миттєво повертає булеве значення «як зараз», і далі неретрайний toBe() порівнює вже застигле значення. Тобто друга форма не чекає ані секунди, хоча в коді виглядає майже так само і навіть містить слово await. Це один із найчастіших способів отримати , не написавши нічого візуально неправильного: локально сторінка малюється швидко й тест зелений, у CI під навантаженням — червоний через раз. Тому на код-ревʼю патерн expect(await ...) — сигнал придивитися.
Які асерти ретрайні, а які ні?
Ретрайні — це матчери, які працюють з локатором, сторінкою або відповіддю: toBeVisible(), toHaveText(), toHaveValue(), toHaveScreenshot() і решта того ж родоводу. Вони знають, як перепитати стан, тому повторюють перевірку до вичерпання таймауту асерту й вимагають await. Неретрайні — загальні матчери на будь-яке значення: toBe(), toEqual(), toContain(). Вони не дефектні: про час їм просто невідомо нічого, і там, де значення вже зафіксоване, це рівно те, що потрібно — код відповіді API, довжина масиву, розібраний JSON. Проблема виникає, коли неретрайним матчером перевіряють живий інтерфейс. Для умов, під які готового ретрайного матчера немає, є обгортки expect.poll і expect.toPass — вони додають до звичайного асерту.
Які таймаути є в ранері й чому їх кілька?
Рівнів кілька, і вони незалежні — це не одна ручка під різними назвами. Тест має 30 секунд за замовчуванням, і в цей бюджет входить не лише тіло, а й налаштування та хуки beforeEach. Кожен ретрайний асерт має власні 5 секунд, і рахуються вони для кожного асерту окремо. Хуки beforeAll і afterAll живуть за окремим таймаутом, рівним тестовому, але часу з тестом не позичають. Таймаути дії, навігації та фікстури за замовчуванням не задані взагалі — їх обмежує лише таймаут тесту. Є ще глобальний, на весь прогін, і він теж вимкнений. На співбесіді відповідь «30 секунд» без назви рівня зараховують за половину: чекають саме розділення «тест — 30, асерт — 5, і вони не повʼязані».
Тест падає, бо асерт не вкладається у свої 5 секунд. Чи допоможе збільшити таймаут тесту?
Ні, і це показова перевірка на розуміння моделі. Бюджет асерту виділяється окремо від бюджету тесту, тому додаткові хвилини на тест не дадуть асерту жодної додаткової спроби — він так само здасться на пʼятій секунді. Правило читається і в зворотний бік: якщо тест уже спалив свої 30 секунд, щедрий таймаут асерту його не врятує. Тому перший крок при падінні — визначити, який саме рівень вичерпався, і лише потім думати про числа. Сама документація попереджає: якщо ви прийшли до розділу низькорівневих таймаутів через -тест, розвʼязок майже напевно не там. І якщо конкретний тест справді довгий за природою, для нього є точкові важелі — test.setTimeout() або test.slow() — замість правки глобального конфіга, який зробить повільними всі падіння в .
Чому page.waitForTimeout() вважають антипатерном? І коли пауза все-таки виправдана?
Бо тест із паузою синхронізується з годинником, а не із застосунком, і документація прямо позначає метод як не рекомендований у продакшн-тестах: очікування часу робить тест за самою побудовою. Арифметика проста: правильного числа не існує. Поставиш мало — тест упаде на повільному прогоні, коли CI під навантаженням; поставиш багато — і, помножене на кожне таке місце в сюїті, це дає тривалість прогону, з якою ніхто не захоче жити. Пауза не буває коректною, вона буває випадково достатньою. Заміна голої паузи — не довша пауза, а умова: автоочікування перед дією, ретрайний асерт на видимий наслідок, очікування конкретного мережевого сигналу або expect.poll. Дозволених застосувань два, і їх варто назвати чесно: дебаг та спостереження того, що зміна не відбулася. У другому випадку асерту просто нема на що чекати — «подія не настала» не є станом, який можна опитати, — тому годинник тут залишається єдиним доступним .
Що робить опція force і чому вона рідко буває правильним рішенням?
force вимикає необовʼязкові перевірки придатності — зокрема перестає контролювати, чи справді елемент отримає клік у заданій точці. Технічно це легальний інструмент, практично — найшвидший спосіб зробити тест зеленим, приховавши дефект. Якщо клік перехоплював оверлей, force цей оверлей не прибирає — він лише припиняє про нього повідомляти, і подія піде елементу, якого користувач у цей момент дотягнутися не може. Тобто тест перестає відтворювати реальний сценарій, а перевірка «кнопка доступна для натискання» безшумно зникає з . Правильна реакція на перехоплений клік — закрити оверлей у : дочекатися зникнення спінера, закрити тост, відпрацювати модалку. force лишається для вузьких випадків, коли перевірка чесно не застосовна, і в таких місцях доречний коментар, чому саме.
Чого автоочікування не чекатиме ніколи?
Воно закриває рівно одну задачу: елемент існує й придатний до дії. Про стан застосунку воно не знає нічого — чи завершилася гідратація (hydration) і навісився обробник, чи повернувся фоновий запит, чи вже перемалювалася таблиця після фільтра. Тому кнопка здатна пройти всі пʼять перевірок і все одно нічого не зробити у відповідь на натискання: обробник події на неї ще не навісився. Дія тоді формально успішна, ефекту — нуль, а червоніє вже котрийсь із наступних кроків, тож звʼязок падіння з причиною губиться; саме такий флак найважче діагностувати. Закривають цю прогалину не паузою, а асертом на видимий наслідок дії, очікуванням конкретної відповіді або прикладним маркером готовності, який команда навішує явно: власний data-атрибут чи aria-busy="false". Останнє — домовленість всередині продукту, а не стандарт, тому її потрібно саме домовити, а не сподіватися, що вона є.
Чому networkidle — поганий сигнал готовності і що замість нього?
Тому що тиша в мережі не дорівнює готовому інтерфейсу: запити скінчилися, а рендер результату ще попереду. Дзеркальна половина проблеми — сторінки, де мережа не замовкає взагалі: фоновий статусу або аналітика, що шле події безперервно. Там очікування такого стану або зʼїдає весь таймаут, або спрацьовує з непередбачуваною . Саме тому цей стан у документації позначений як DISCOURAGED із прямою порадою спиратися на вебасерти. Заміна проста: перевіряти те, що побачив би користувач — зʼявився рядок, зник спінер, змінилося число в підсумку. Мережева відповідь як точка синхронізації — вибір другого порядку: до неї вдаються, коли візуальний наслідок дії заледве помітний і надійного асерту з нього не зробити. І варто не плутати сусідні стани: domcontentloaded означає лише те, що HTML розібрано, load — що приїхали й підресурси; жоден із них не означає «застосунок готовий до роботи».
Коли доречний expect.soft і в чому його ціна?
Звичайний асерт при падінні зупиняє тест, мʼякий — не зупиняє, але позначає тест як провалений. Виграш очевидний на екранах із десятком полів: за один прогін ви бачите всі розбіжності одразу — і суму, і спосіб доставки, і термін, — а не лише першу, після чого чекаєте наступний прогін, щоб дізнатися решту. Ціна теж очевидна: тест іде далі на стані, у якому перевірена умова не виконалася, тож результати наступних кроків можуть уже нічого не означати. Тому мʼякі асерти доречні для набору незалежних перевірок одного екрана й недоречні там, де наступний крок залежить від перевіреного. Якщо треба обидва світи, посеред тесту можна подивитися, чи вже були мʼякі падіння, і вийти раніше. І одне обмеження: механізм належить — у бібліотечному сценарії його немає.
Чим expect.poll відрізняється від expect.toPass?
Обидві обгортки додають ретраї туди, де готового ретрайного матчера немає, але працюють на різному масштабі. expect.poll бере функцію, яка повертає одне значення, і перетворює звичайний синхронний асерт на полінг (polling) — канонічний приклад із документації це очікування статусу HTTP 200 від здоровʼя. expect.toPass повторює цілий блок коду, доки він не пройде повністю, тому годиться, коли всередині кілька дій і кілька асертів разом. Головна пастка — у другого: toPass не бере число, яке ви налаштували для асертів, власний дефолт у нього нульовий, тому опцію доводиться передавати руками — інакше очікування поведеться не так, як ви розраховували. Обидві обгортки мають ще одну спільну вимогу: тіло виконується стільки разів, скільки було ітерацій, тож повторний виклик мусить бути безпечним — полінг, усередині якого створюється замовлення, наплодить їх по одному на спробу.
Який таймаут ставити полінгу і чому timeout: 0 — погана ідея?
У полінгу є два налаштування: інтервали між спробами і стеля. Дефолтні інтервали — 100, 250, 500 і 1000 мс, після чого останній повторюється; дефолтна стеля — 5 секунд. Корисна порада тут звучить контрінтуїтивно: стелю варто робити навмисно високою, а не «реалістичною». Логіка в тому, що короткий інтервал полінгу і так обмежує втрачений час — у щасливому випадку ви виходите з першої-другої спроби, тому щедрий запас майже нічого не коштує, зате прибирає хибні падіння на повільному CI. Але скінченною вона бути зобовʼязана: timeout: 0 міняє майбутній фейл на нескінченне очікування, і перевірка, що не здатна впасти, більше нічого не перевіряє — у це виглядає як мовчання, поки хтось не вбʼє вручну.
Чи потрібна пауза перед скріншотним асертом, щоб не зловити кадр посеред анімації?
Ні, і це якраз місце, де паузу ставлять «на всяк випадок» найчастіше. toHaveScreenshot() — теж ретрайний асерт: перед порівнянням з еталоном він робить знімки доти, доки два послідовні не збіжаться, і лише тоді порівнює останній. Анімації він до того ж глушить за замовчуванням: скінченні перемотуються до кінцевого стану, нескінченні скасовуються до початкового. Таймаут у нього власний і за замовчуванням дорівнює таймауту асерту, тобто в загальну ієрархію він вписується як усі інші. Окремий нюанс для розбору падінь: такий асерт можна скасувати сигналом, і для тесту скасування виглядатиме як звичайний таймаут — тому за самим повідомленням про таймаут не завжди видно справжню причину.
Чим модель таймаутів бібліотеки відрізняється від моделі ранера?
Це різні продукти з різними дефолтами, і плутанина тут дає стійке хибне очікування. Бібліотека дає 30 секунд на більшість операцій — тобто «на клік у мене є пів хвилини» там справді вірно. Ранер побудований навпаки: більшість операцій власного таймауту не мають узагалі, зате кожен тест обмежений 30 секундами, і саме цей ліміт його валить. Наслідок практичний: скрипт, який спокійно жив у бібліотечному режимі, після переїзду в тести починає падати не там, де очікуєш, — бо тепер тікає один спільний бюджет на весь тест разом із фікстурами й beforeEach. Тому досвід «у нас же було 30 секунд на дію» з бібліотечних скриптів у тести не переноситься, і при міграції варто перечитати саме конфігурацію таймаутів.
Тест падає на кліку, локатор точно правильний. Як розбираєш?
Тут оцінюють порядок дій, а не вгадану причину. Крок перший — визначити, який рівень вичерпався: таймаут дії всередині тесту, таймаут ретрайного асерту чи весь бюджет тесту разом із фікстурами. Крок другий — перевірити, чи перевірка справді ретрайна: expect(await locator.isVisible()) і забутий await дають ту саму картину «не дочекався», хоча очікування там ніколи й не було. Крок третій — подивитися, яка саме умова придатності не пройшла: найчастіший винуватець при живому локаторі — перехоплення кліку, коли поверх кнопки висить спінер, тост чи невидимий шар модалки; і моменту падіння показують це буквально. Крок четвертий — відокремити придатність від готовності: якщо елемент придатний, а дія «не працює», підозра зміщується на гідратацію чи незавершений фоновий запит, і тоді потрібен явний сигнал, а не більший таймаут. І лише після цього має сенс розмова про числа. Відповідь «поставлю force» або «підніму таймаут» на цьому питанні читається як червоний , бо обидва рішення прибирають симптом і зберігають причину.
Три кейси з робочого життя AQA: як за виглядом падіння визначити, який рівень вичерпався; як переписати -перевірку, не додаючи ані секунди паузи; і як чекати на те, чого в DOM немає взагалі. Скрізь — що дивитися і чому саме так.
Кейс 1. «Упало по таймауту» — зʼясовуємо, який саме
Приходить тікет: «тест оформлення замовлення флакає, у CI червоний приблизно раз на пʼять прогонів, локально зелений завжди». Перше, чого робити не треба, — це чіпати числа в конфізі. Спершу читаємо, який рівень здався, бо від цього залежить, де взагалі шукати.
Три типові повідомлення й три різні діагнози:
1) Test timeout of 30000ms exceeded.
2) expect(locator).toHaveText(expected) failed
Locator: locator('#node')
Expected pattern: /Text 2/
Received string: "Text content"
Timeout: 5000ms
Call log:
- Expect "toHaveText" with timeout 5000ms
- waiting for locator('#node')
3) locator.click: Test timeout of 30000ms exceeded.
waiting for element to be visible, enabled and stable
Далі — , за якою я розбираю падіння:
| Що видно в звіті | Який рівень вичерпався | Куди дивитися далі |
|---|---|---|
| «Test timeout ... exceeded» без згадки конкретного асерту | бюджет тесту: тіло + фікстури + beforeEach | що зʼїло час — довга фікстура, логін через UI, ланцюжок кроків; чи не варто тут test.slow() |
| «expect(locator).toHaveText(expected) failed» і рядок «Timeout:» під ним | таймаут асерту, свої 5 с на кожен окремо | чи умова взагалі колись стане правдою; можливо, чекаємо не на той наслідок |
| «locator.click: Test timeout ... exceeded» плюс перелік умов | дія не дочекалася придатності; власного таймауту вона за замовчуванням не має, тому її перервав бюджет тесту | яка саме умова не зійшлася — далі трейс |
| тест зелений, але наступний крок падає «нізвідки» | нічого не вичерпалося: очікування не було | шукаємо expect(await ...) і забутий await |
Що дивитися і чому:
- Формат помилки залежить від версії — і саме його найчастіше цитують застарілим. До 1.54 включно друкував один рядок «Timed out 5000ms waiting for expect(locator).toHaveText(expected)»; з 1.55 вивід розкладено на окремі рядки — заголовок з «failed», , очікуване, отримане, «Timeout:» і лише потім «Call log:». Якщо стаття чи відповідь колеги описує однорядкову форму — вона про стару версію, і шукати цей рядок у своєму терміналі марно.
- Рядок «waiting for element to be ...» — це список умов, а не декорація. Playwright перелічує, чого саме він чекав. Якщо там є
stable, а на екрані модалка з анімацією появи — питання закрите. Якщо всі умови на вигляд виконані, залишається перевірка, якої в цьому рядку не видно: перехоплення кліку. - Перехоплений клік ловиться за секунду. У Trace Viewer на кадрі моменту падіння видно, хто накрив кнопку: спінер, тост, напівпрозорий шар модалки, що вже «зачинилася», але ще не зникла з DOM. Це корисне падіння — інструмент відтворив ситуацію, у якій живий користувач теж не дотягнеться до кнопки.
- Зелений тест — не доказ, що перевірка була. Найгірший із чотирьох рядків таблиці — останній: асерт без
awaitможе не впасти ніколи, тому в статистиці його не видно. Він проявляється як падіння наступного кроку на «неможливому» стані. - Числа — останній крок, не перший. Якщо вичерпався таймаут асерту, збільшення таймауту тесту не дасть асерту жодної додаткової спроби: рівні незалежні. Правка глобального конфіга тут дає лише одне — тепер кожне справжнє падіння в чекатиме довше, перш ніж почервоніти.
Кейс 2. Рефакторинг флакі-перевірки: три версії одного чека
Перевіряємо, що після натискання «Оформити» зʼявляється підтвердження й номер замовлення. Задача одна, реалізації різні.
Версія А — те, що зазвичай і лежить у репозиторії:
await page.getByRole('button', { name: 'Оформити' }).click();
await page.waitForTimeout(3_000); // синхронізація з годинником
expect(await page.getByTestId('order-confirmation').isVisible()).toBe(true);
Тут дві незалежні проблеми, і кожна сама по собі достатня для флаку. Пауза на три секунди вгадує швидкість бекенду: у CI під паралелізмом відповідь може прийти за чотири, і тест упаде на рівному місці. А expect(await ...isVisible()) не чекає ані секунди — це знімок стану на момент виклику, і саме тому попередню паузу й довелося ставити. Прибрати паузу, не переписавши асерт, — зробити тест ще гіршим.
Версія Б — паузу прибрали, асерт лишили як був:
await page.getByRole('button', { name: 'Оформити' }).click();
expect(await page.getByTestId('order-confirmation').isVisible()).toBe(false); // ой
Класика: тест став швидким і почав стабільно перевіряти, що підтвердження ще немає. Формально він проходить, фактично нульове — асерт зафіксував проміжний стан і назвав його очікуваним. Зустрічається це частіше, ніж хочеться: хтось побачив червоне, поміняв true на false, тест позеленів, тікет закрили.
Версія В — синхронізація із застосунком:
await page.getByRole('button', { name: 'Оформити' }).click();
// ретрайний асерт: повторює перевірку до таймауту асерту
await expect(page.getByTestId('order-confirmation')).toBeVisible();
await expect(page.getByTestId('order-number')).toHaveText(/^ORD-\d{6}$/);
Жодної паузи, жодного вгадування: перевірка повторюється, поки умова не стане правдою або поки не витече бюджет асерту. Якщо бекенд відповів за 200 мс — тест піде далі за 200 мс, а не за три секунди; якщо за чотири — теж дочекається.
Якщо візуальний наслідок дії заледве помітний і надійного асерту з нього не зробити, точкою синхронізації беруть мережеву відповідь — але порядок рядків тут критичний:
// проміс СТВОРЮЄМО до дії, що тригерить запит
const orderCreated = page.waitForResponse(
(r) => r.url().includes('/api/orders') && r.status() === 201,
);
await page.getByRole('button', { name: 'Оформити' }).click();
await orderCreated;
await expect(page.getByTestId('order-confirmation')).toBeVisible();
Що дивитися і чому:
- Порядок рядків тут — не стилістика. Якщо створити очікування після кліку, між дією і початком слухання є вікно, у яке швидка відповідь прослизне непоміченою. Це та сама гонка, тільки написана вручну, і на локальній машині вона майже ніколи не відтворюється.
- Мережевий асерт — вибір другого порядку. Він перевіряє, що бекенд відповів, а не що користувач побачив результат. Тому після нього все одно потрібен асерт на інтерфейс — інакше можна отримати зелений тест на сторінці, яка показує помилку рендеру.
- Регулярка в
toHaveTextтеж ретрайна. Матчер перепитує текст, поки він не почне відповідати шаблону, тому проміжнийORD-—чи порожній рядок падінням не стане. - Пауза лишається легальною рівно в одному місці цього флоу — якщо треба довести, що чогось не сталося: наприклад, після кліку по «Скасувати» підтвердження не зʼявляється й замовлення не створюється. Опитати «події не було» неможливо — чекати нема на що, — тому єдине, чим тут можна міряти, це витриманий час.
Кейс 3. Чекати на те, чого в DOM немає
Ретрайні матчери покривають стани сторінки. А тепер задача з іншого боку: перед сюїтою треба переконатися, що стейдж піднявся, а всередині тесту — дочекатися, поки асинхронний згенерує PDF-накладну, доступну лише через API.
Готовність сервісу — expect.poll, бо чекаємо одне значення:
await expect.poll(async () => {
const response = await request.get('/api/health');
return response.status();
}, {
timeout: 60_000, // стеля навмисно щедра, але скінченна
intervals: [500, 1_000, 2_000], // короткий крок на початку — швидкий вихід у щасливому випадку
message: 'стейдж не піднявся: /api/health не віддав 200',
}).toBe(200);
Готовність документа — toPass, бо всередині кілька кроків і кілька асертів:
await expect(async () => {
const list = await request.get(`/api/orders/${orderId}/documents`);
expect(list.ok()).toBeTruthy();
const invoice = (await list.json()).find((d) => d.type === 'invoice');
expect(invoice, 'накладна ще не згенерована').toBeDefined();
expect(invoice.status).toBe('ready');
}).toPass({
timeout: 30_000, // ОБОВʼЯЗКОВО: свій таймаут toPass не успадковує, дефолт 0
intervals: [1_000, 2_000, 5_000],
});
Екран підтвердження — мʼякі асерти, бо перевірки незалежні:
await expect.soft(page.getByTestId('total')).toHaveText('1 200 ₴');
await expect.soft(page.getByTestId('delivery')).toHaveText('Нова пошта');
await expect.soft(page.getByTestId('eta')).toHaveText('2 дні');
// далі йдуть кроки, які залежать від коректності екрана — тому виходимо, якщо щось не збіглося
expect(test.info().errors).toHaveLength(0);
await page.getByRole('button', { name: 'Підтвердити' }).click();
Що дивитися і чому:
- Функція в мусить бути безпечною для повторення. Обидва приклади лише читають, і це не випадковість: спроб буде багато, тож полінг, який усередині створює замовлення, створить їх стільки, скільки було ітерацій. Мутації виносяться до полінгу.
toPassбез явного таймауту — засідка. Він не бере число зexpect.timeoutу конфізі: свій дефолт нуль, тому очікування поведе себе не так, як ви розраховували. Якщо в коді бачитеtoPass()без опцій — це перше, що варто перевірити.- Щедра стеля майже нічого не коштує, нульова коштує всього. Короткий перший інтервал означає, що в нормі ви виходите з першої-другої спроби, тому 60 секунд запасу не сповільнюють нічого й прибирають хибні падіння на перевантаженому CI. А
timeout: 0перетворює майбутній фейл на зависання: у це виглядає як мовчання, поки хтось не вбʼє руками. - Мʼякий асерт вимагає дисципліни після себе. Тест продовжується, тож наступні кроки підуть на стані, у якому умова не виконалася, і їхні результати можуть уже нічого не означати. Тому там, де далі йде залежна дія, наявність мʼяких падінь перевіряють явно й виходять раніше.
- Полінг усередині
expect.softне зупиняє тест. Комбінація легальна, але фейл доводиться ловити окремо — інакше сюїта побіжить далі на стані, який ви щойно визнали неготовим.
Автоочікування і придатність елемента
- Розумію, що перед дією інструмент не «спить», а в циклі перепитує набір умов придатності (actionability) і виконує дію одразу, як вони зійшлися; не зійшлися за —
TimeoutError. Можливе це тому, що — рецепт пошуку, який резолвиться в момент дії. - Можу назвати пʼять перевірок перед кліком поіменно: рівно один збіг локатора, Visible, Stable, Receives Events, Enabled — і додаткову Editable для введення тексту.
- Знаю, що
opacity: 0вважається видимим, а невидимість дають нульовий розмір,display:noneіvisibility:hidden; так само памʼятаю три джерела «задизейбленості» — атрибутdisabled, батьківськийfieldset[disabled],aria-disabled=true. - Розумію, що
forceне прибирає оверлей, а лише перестає перевіряти перехоплення кліку — і чому це , а не фікс.
Межі автоочікування
- Розводжу два рівні: придатність елемента до дії (це робить інструмент) і готовність застосунку (це мій явний сигнал).
- Можу описати сценарій «кнопка видима, стабільна й увімкнена, а клік нічого не робить» — обробник ще не навісився після гідратації (hydration).
- Знаю, чому
networkidleпозначено як DISCOURAGED, і чим його заміняють: на видимий наслідок, очікування конкретної відповіді, створене до дії-тригера, прикладний маркер готовності на кшталтaria-busy="false". - Не плутаю
domcontentloaded(HTML розібрано) іload(приїхали й підресурси) та розумію, що ні те, ні те не означає «застосунок готовий».
Web-first асерти
- Можу пояснити різницю між ретрайним асертом і ручною перевіркою одним реченням: перший повторює перевірку до свого таймауту, друга дивиться стан просто зараз.
- Бачу помилку в
expect(await locator.isVisible()).toBe(true)і знаю причину: ретрайні асерти асинхронні, тому забутий чи не туди поставленийawaitперетворює перевірку на нікому не потрібний — тест проходить, не перевіривши нічого. - Відрізняю ретрайні матчери (
toBeVisible,toHaveText,toHaveScreenshot) від неретрайних (toBe,toEqual,toContain) і розумію, де кожні доречні.
Таймаути
- Знаю дефолти й склад рівнів: тест — 30 с (тіло + +
beforeEach), асерт — 5 с на кожен окремо,beforeAll/afterAll— окремий бюджет, дії/навігація/фікстура/глобальний — не задані. - Можу проговорити правило в обидва боки: таймаут асерту не повʼязаний із таймаутом тесту, тож ні збільшення одного, ні другого не компенсує сусідній рівень; для справді повільного тесту є
test.setTimeout()іtest.slow(), а не правка глобального конфіга. - Памʼятаю різницю моделей: бібліотека дає 30 с на більшість операцій, власних таймаутів на операції не має, зате валить тест по його бюджету.
Мʼякі асерти й обгортки полінгу
- Розумію семантику
expect.soft: тест не зупиняється, але стає провальним, тож решта кроків виконується на стані, який перевірку вже не пройшов — а посеред тесту можна подивитися на мʼякі падіння й вийти раніше. Памʼятаю й обмеження: механізм є лише в ранері, не в бібліотечному сценарії. - Відрізняю
expect.poll(одне значення, синхронного асерту) відexpect.toPass(повторення цілого блоку) і памʼятаю, щоtoPassтаймаут асерту не успадковує — за замовчуванням у нього 0. - Знаю дефолти полінгу — інтервали 100/250/500/1000 мс і стеля 5 с — чому
timeout: 0перетворює фейл на зависання і чому тіло полінгу мусить бути безпечним для повторення.
Паузи й діагностика флаку
- Можу аргументувати заборону голих пауз не смаком, а арифметикою: правильного числа не існує — замало валить тест на повільному прогоні, забагато робить прогін непідйомним. І знаю два чесні винятки для
waitForTimeout: дебаг та спостереження того, що зміна не відбулася, бо асерту на відсутність події не існує. - Тримаю в голові порядок розбору падіння на кліку: який рівень таймауту вичерпався → чи перевірка була ретрайна → чи не перехоплює клік оверлей → і лише потім числа.
- Розумію, чому «упало по таймауту — підніму таймаут» і
forceчитаються на співбесіді як червоні прапорці: обидва прибирають симптом і залишають причину.
Квіз
Перед стартом
- Питань: 16
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Автоочікування (auto-waiting) — що це технічно?
