Практичні сценарії AQA: флак і синхронізація
Зміст
Найчастіша реальна задача AQA — не написати тест, а зрозуміти, чому він падає раз на десять прогонів. Відповідь майже завжди та сама: тест і застосунок живуть у різному часі. Тест виконує команди швидко й послідовно, а браузер тим часом добудовує розмітку, виконує JavaScript і ходить у мережу. Розрив між «сторінка ще думає» і «тест уже клікає» — джерело більшості нестабільних падінь.
Глава підсумкова: вона майже не вводить нової механіки, а звʼязує пройдене розділом. Це канон теми очікувань і синхронізації на сайті — інші глави посилаються сюди. При першому проході розділу її можна пропустити й повернутися тоді, коли почнете писати автотести. , і повна таксономія (flakiness) — тема розділу про автоматизацію; тут браузерного плюс процедура, якою його локалізують.
Синхронізація: діяти рівно тоді, коли застосунок готовий
Очікування ((auto-)wait) потрібне не тому, що «браузер повільний», а тому, що застосунок має власний темп. Стоїть синхронізація на двох речах.
Перша — перевірки придатності (actionability), які інструмент робить сам: перед дією він чекає, поки всі релевантні перевірки пройдуть; не дочекався в межах — падає з TimeoutError. Для кліку перевіряється, що вказує рівно на один елемент і що елемент видимий, стабільний, отримує події й увімкнений; для введення додається «редагований». Означення точні, і саме в них — половина «загадок»:
- Видимий — непорожній без
visibility: hidden; нульовий розмір іdisplay: noneневидимі, аopacity: 0— видимий. - Стабільний — той самий bounding box два послідовні кадри анімації.
- Отримує події — є ціллю вказівникової події в точці дії; так ловиться оверлей, що перехоплює клік.
- Увімкнений — не задизейблений
disabled, батьківськимfieldsetчиaria-disabled="true".
Опція force вимикає необовʼязкові перевірки — зокрема перестає контролювати, чи елемент отримує клік: це спосіб зробити тест зеленим, приховавши дефект.
Друга річ — явні очікування прикладних сигналів, яких інструмент знати не може: чи навішений обробник, чи завершився запит, чи оновилися дані. (auto-waiting) закриває «елемент зʼявився і став клікабельним», не більше. Явне очікування — коли інженер сам пише умову й таймаут (WebDriverWait у Selenium); неявне реагує лише на «елемент не знайдено», і змішувати їх не варто. (hard wait) програє двічі: замало — тест флакне на CI, забагато — прогін розтягується, а web-first й так повторюють перевірку самі. Орієнтир рівня — те, що бачить користувач; анімації глушать окремо.
Наша практика (не канон). Дві речі з цього абзацу джерела в такій формі не стверджують. Перша — межі автоочікування: дока задає їх переліком перевірок, а не застереженням «воно не знає про прикладні стани»; формулювання наше. Друга — глушіння анімацій CSS- на час прогону: джерельною тут є лише опція animations: 'disabled' для візуальних порівнянь.
Деталь, що коштує годин: очікування відповіді створюють до дії, яка тригерить запит, інакше швидка відповідь прослизне непоміченою.
// проміс створюємо ДО кліку — інакше швидка відповідь прослизне
const orderCreated = page.waitForResponse(
(r) => r.url().includes('/api/orders') && r.status() === 201
);
await page.getByRole('button', { name: 'Оплатити' }).click();
await orderCreated;
Подієвий цикл і гонка станів
JavaScript у браузері виконується в одному потоці: у кожен момент (JavaScript engine) виконує рівно один фрагмент коду. Однопотоковий саме JS-рушій, а не браузер: мережу й таймери обслуговують окремі механізми поза цим потоком.
Узгоджує все (event loop) — механізм, яким браузер координує події, дії користувача, скрипти, рендеринг і мережу. Спрощено: виконати весь синхронний код → коли стек порожній, узяти наступну задачу з черги → повторити. асинхронної дії стає задачею в черзі, і кожна задача обробляється повністю (run-to-completion). Черга мікрозадач стоїть окремо й чергою задач не є: мікрозадачі — це реакції промісів, queueMicrotask і MutationObserver, а задачі (task) — setTimeout, події DOM і XHR. Після кожної задачі цикл спорожнює чергу мікрозадач повністю.
Звідси два наслідки. setTimeout(fn, 0) означає «якнайшвидше, коли звільниться потік», а не «зараз»: специфікація прямо каже, що таймери не гарантують виконання за розкладом. І не витісняється примусово — вона затримує і обробку подій, і кадр рендерингу, тож «клік не спрацював» іноді означає, що потік був зайнятий.
Гонка станів (race condition) — коли результат залежить від порядку завершення асинхронних операцій, а цей порядок не гарантований. Оскільки задача обробляється повністю, гонка виникає не всередині задачі, а між задачами. Класика — живий пошук.
Лікують це скасуванням застарілого запиту через AbortController.
Наша практика (не канон). Тут кілька формулювань наші, і джерела їх не несуть. Саме означення гонки джерело не дає — реф покриває лише відсутність гарантій щодо порядку задач. Приклад живого пошуку з двома запитами, стисла формула «перемагає останній, що відповів, хоча має перемагати останній, що надіслали» і конструкція «наївний тест містить одразу дві гонки» (елемента ще може не бути в DOM, а його вміст оновиться після запиту, що триває) — теж наші.
Та сама механіка має й безпековий бік: у каталозі слабкостей гонка стоїть власним класом -362, а її найупізнаваніший вид — нащадок CWE-367 , де стан ресурсу змінюється між перевіркою й використанням. Припис OWASP звідси варто запамʼятати: якщо два запити можуть змагатися, напиши тест, який їх зіштовхне.
«Елемент є в DOM, а клік не проходить»
Наявність вузла в DOM — найраніша й найслабша гарантія готовності. Між «є в дереві документа» і «по ньому можна клікнути» лежить увесь конвеєр рендерингу (rendering pipeline): DOM і CSSOM → дерево рендерингу → layout → paint → composite.
У дереві рендерингу лишаються тільки вузли, які реально малюватимуться: елемент із display: none там відсутній — він є в DOM, але геометрії не має, — а visibility: hidden присутній, займає місце й просто не видно. Layout рахує геометрію, і результат залежить від розміру екрана; тому свідомо зафіксований viewport — базова умова відтворюваності. Paint заповнює пікселі (растеризація належить саме сюди), composite складає шари.
Звідси три типові діагнози: елемент ще без геометрії; елемент їде й не проходить перевірку стабільності; головний потік зайнятий важким перерахунком — намальовано, але на кліки не реагує. Четвертий, найпідступніший, — обробник ще не навісився.
Дерево, події й межі пошуку
DOM — не текст відповіді сервера, а структура в памʼяті браузера: вихідний HTML видно у View Page Source, а DOM показує панель Elements після виконання всього JavaScript. У SPA різниця драматична — у вихідному HTML може стояти лише порожній <div id="root">, тож інструмент без виконання JS (curl) вмісту не побачить. Дерево живе безперервно, тому локатор варто розуміти як рецепт пошуку, що резолвиться в момент дії, а не як збережене посилання на вузол (DOM, селектори та події).
Події проходять дерево трьома фазами — захоплення, ціль, спливання, — і за замовчуванням обробники спрацьовують на спливанні. Звідси делегування: один обробник на спільному предку обслуговує всі дочірні елементи, зокрема ті, що зʼявляться згодом. Наслідок прямий: обробник може не висіти на елементі, по якому ви клікаєте, тому перевіряти треба результат кліку. А preventDefault() в обробнику скасовує стандартну дію — тож клік по «Надіслати», що не веде до навігації, буває не багом.
Другий клас «зникнень» — межі пошуку. Shadow DOM дає елементу власне приховане під-дерево, якого document.querySelectorAll() не бачить. буває open або closed: у першому element.shadowRoot віддає корінь, у другому — null, причому closed є сигналом наміру, а не барʼєром безпеки. Коли подія перетинає , спрацьовує ретаргетинг: зовні ціллю видно host-елемент. Playwright автоматично пробиває відкриті для CSS- і текстових локаторів, XPath — ні.
iframe — не «ще один шар» сторінки, а інший документ зі своїм деревом: контекст треба перемкнути (frameLocator у Playwright, switchTo().frame() у Selenium). (same-origin policy) забороняє сторінці читати чужий документ, але драйвери працюють на рівні браузера, поза цим обмеженням. Звідси діагностичний порядок: «знаходжу вручну в DevTools, а тест — ні» першим ділом перевіряють на інший контекст.
Локатор: стабільність починається з нього
Селектор — шаблон, за яким добирають елементи; для тесту важать не сила правила в каскаді, а унікальність і стійкість збігу. Дві пастки дають не падіння, а гірше — тихо неправильний локатор. Перша: часткові збіги атрибутів (^=, $=, *=) рятують від згенерованих класів на кшталт Button_root__a1b2c, але ~= до трійки не належить — він шукає ціле слово у списку через пробіл. Друга — XPath: contains(text(), 'Оплата') для <button>Оплата <b>картою</b></button> бачить лише перший текстовий вузол, а //x[1] це не «перший x у документі»; ліки — contains(., ...) і (//x)[1]. У XPath ідуть заради осей угору по дереву (parent, ancestor), бо CSS їх не має, а реляційний :has() доступний в усіх браузерах лише з грудня 2023.
Ієрархія вибору: роль плюс доступне імʼя → видимий текст або лейбл → data-testid → CSS за стабільним атрибутом → XPath. Локатори за роллю не залежать від верстки й побічно ловлять дефекти доступності: якщо «кнопка» насправді клікабельний div без ролі, пошук за роллю її не знайде. Слабке місце локаторів за текстом — локаль, а data-testid — явний контракт із кодом, який легко ненавмисно видалити, бо «він нічого не робить».
Розмітка є, а обробників ще немає: CSR, SSR і гідратація
Хто будує HTML — сервер чи браузер, — вирішує, коли сторінка стає придатною до дії. CSR (client-side rendering) будує розмітку в браузері силами JavaScript, тож у перші миті в DOM немає нічого корисного; SSR (server-side rendering) віддає готовий HTML. А гідратація (hydration) — запуск клієнтських скриптів, які додають стан та інтерактивність до відрендереної сервером розмітки, — створює найпідступніший стан: сторінка виглядає завантаженою, але не реагує на введення, доки скрипти не виконаються й обробники не навісяться.
Звідси незручна правда: перевірка видимості підтверджує наявність у розмітці, а не готовність до взаємодії. Чекати треба на ознаку інтерактивності — зникнення disabled чи узгоджений маркер; стандартного DOM-атрибута «гідратацію завершено» не існує. Універсального «сторінка готова» теж немає: DOMContentLoaded настає, коли розібрано HTML, load чекає на підресурси, але не на AJAX, а очікування «тихої мережі» документація Playwright позначає як небажане.
Архітектурна пара, з якої це росте, — SPA і MPA. Джерельним тут є один полюс: SPA (single-page application) завантажує один документ і далі перемальовує його вміст скриптом, а History API (pushState/replaceState) міняє адресу без запиту на сервер (Асинхронне завантаження: SPA/MPA, CSR/SSR).
Наша практика (не канон). Практика домовлятися з командою про явний маркер готовності (data-hydrated, aria-busy="false") канонічного джерела не має — це річ конкретного застосунку й наша конвенція. Другий полюс архітектурної пари й висновки з порівняння — теж наші. Термін MPA (multi-page application) з означенням «кожен екран є окремим документом» канонічного джерела не має; порівняння частоти події load (у MPA при кожному переході, у SPA один раз на старті) — наш вивід; механіка «клієнтський роутер перехоплює клік по посиланню й сам змінює адресу та вміст» у джерелах не описана. Звідси й наше робоче правило: після клієнтської навігації перевіряй дві речі одночасно — адресу й появу контенту, бо сама лише адреса доводить тільки роботу роутера. Наше й застереження про проміжну сторінку в ланцюжку редиректів: очікування ловить URL, який існує лише мить, тож надійніше синхронізуватися на кінцевому стані.
Дані приїжджають окремо: запит, мережа й затримка
«Сторінка відкрилась» не означає «дані на місці». AJAX — обмін даними із сервером у фоні без перезавантаження; технічно це або старий XMLHttpRequest, або Fetch API. У fetch є пастка, на якій ловляться і розробники, і тестувальники: відповідь 404 чи 500 дає успішний проміс, бо реджект стається лише на частині помилок — як-от мережевій або поганій схемі. Тому статус перевіряють вручну: response.ok істинний лише для 2xx. Тіло при цьому є потоком і читається один раз. У XMLHttpRequest схоже: readyState === 4 означає «завершилось» — успішно або з помилкою.
Під кожним запитом лежить повний життєвий цикл: DNS → TCP → TLS → надсилання → очікування сервера → відповідь. Перший запит до нового хоста дорожчий за наступні, бо зʼєднання перевикористовується (keep-alive), а стан pending означає не одне: сервер думає, запит стоїть у черзі через ліміт одночасних зʼєднань до походження, або зʼєднання довге навмисно (Життєвий цикл запиту, DevTools).
(latency) — час, за який дані долають шлях від джерела до призначення, зазвичай як round-trip; (bandwidth) — скільки інформації проходить крізь зʼєднання за одиницю часу. Величини незалежні, тому десятки дрібних API-викликів і завантаження великого файла ламаються з різних причин. Сюди ж — (head-of-line blocking), де важливо не сплутати рівні: у HTTP/1.x воно на прикладному рівні, тому клієнти відкривають кілька зʼєднань; HTTP/2 прибирає його там, але його специфікація прямо зазначає, що блокування на рівні TCP протокол не усуває; HTTP/3 над QUIC закриває і це (HTTP/2 і HTTP/3).
І про таймаути: коли тест падає з таймауту, спершу зрозумій, чий саме таймаут спрацював — мережевий, HTTP-клієнта, (клієнт бачить 504) чи тест-фреймворка. Піднімати таймаут замість діагнозу — ховати проблему.
Як зробити тест детермінованим
Найпряміший спосіб прибрати недетермінізм — не покладатися на реальний сервер. (network interception) вклинює тест між браузером і мережею: він реєструє правило «для запитів за цим шаблоном URL викликай цей обробник», а обробник або пропускає запит (дорогою можна змінити URL, метод, заголовки чи тіло), або підмінює відповідь, не викликаючи сервер, або обриває запит мережевою помилкою.
// маршрут реєструємо ДО дії, що тригерить запит
await page.route('**/api/rates', (route) =>
route.fulfill({ status: 200, body: JSON.stringify({ usd: 41.5 }) })
);
Тонкощі, на яких спотикаються. Правило спрацьовує лише для запитів, зроблених після реєстрації, а те, що не підійшло під шаблон, іде на реальний сервер. Заборонені заголовки (Cookie, Host, Content-Length) підмінити не вийде: перевизначення ігнорується, і тест «підмінив» Cookie лише в уяві. Коди обриву не синоніми: connectionrefused, namenotresolved та internetdisconnected моделюють три різні сценарії. Зворотний бік — дрейф контракту: замокавши все, ви тестуєте свої припущення про бекенд (Перехоплення й мокання мережі).
Наша практика (не канон). Сам припис «спершу маршрут, потім дія» джерело прямо не формулює — воно лише показує цей порядок у прикладі; вимога наша. Наше й правило прибирати маршрут наприкінці тесту як частину ізоляції: залишений маршрут перехоплюватиме запити наступних тестів у тому самому й дасть плаваючі падіння.
Другий інструмент — (throttling): панель мережі ріже пропускну здатність. Пресети змінювалися між версіями браузера, тому надійніше задати власний профіль; поруч живуть Offline і Disable cache.
Наша практика (не канон). Роль тротлінгу для AQA і сам приклад — наші, довідник панелі їх не описує: для нас це не «перевірка продуктивності», а спосіб зробити видимими гонки, які на швидкому каналі не встигають проявитися (класика — кнопка лишається активною, поки запит летить, і дає подвійне надсилання). Наше й робоче формулювання: якщо тест падає на повільному профілі, він не «флакне на швидкому» — він просто ще не впав.
Третє — знати, що можна повторювати. (idempotency) означає, що кілька однакових запитів дають на сервері той самий ефект, що й один; такими є всі , а також PUT і DELETE. POST неідемпотентний: два однакові POST /orders створять два замовлення — звідси дублі від подвійного кліку. Тонкість, на якій легко зробити хибний висновок: ідемпотентність стосується стану сервера, а не відповіді, тож 404 на повторний DELETE — очікувана поведінка. Це й відповідає на питання при мережевому збої: ретрай безпечний чи зіпсує дані.
Чужий стан: кеш, сховища, сесія й токен
Останній великий клас флаку — стан, який лишився не від цього тесту. «Стара версія» приходить із чотирьох місць: HTTP-кеш браузера, кеш service worker, CDN або , кеш усередині застосунку (Кешування). Найдешевша ізоляція — свіжий профіль на прогін: у Playwright цю роль грає окремий . Але вимикати кеш у всіх тестах не можна: якщо перевіряється сама поведінка , кеш має бути ввімкнений.
Сховища протікають так само. localStorage розділений лише за походженням і переживає перезапуск браузера; sessionStorage розділений ще й за вкладкою, тож новий контекст його не успадковує — часта причина, чому «залогінена» в одному місці сесія не працює в іншому. захоплює кукі, локальне сховище та IndexedDB, а sessionStorage не зберігається взагалі — його відновлюють вручну. Симптом протікання впізнаваний: тести зелені поодинці й червоні в наборі. Важлива межа: чистка на клієнті не прибирає серверного стану.
Класична сесія тримає стан на сервері, а браузеру віддає лише ключ, ідентифікатор сесії в кукі; браузер сам вирішує за збереженими метаданими, чи додати кукі до наступного запиту. Тому «наступний тест бачить користувача розлогіненим» означає рівно одне: не поїхала валідна кукі сесії — стан не підхопився, кукі сесійна й не пережила рестарт, сесія згасла або невірні Domain/Path. Файл зі збереженим станом містить живі секрети, тож його не (Кукі, сесії та сховище браузера).
Альтернатива серверній сесії — токен, найчастіше JWT (JSON Web Token). У звичайному підписаному JWT payload лише закодований base64url, а не зашифрований (шифрування дає окремий варіант — JWE), тож тест може прочитати claim exp і перевірити свіжість токена до запиту, а не ловити 401 постфактум. Для довгих прогонів це принципово: токен зі старту цілком може дожити своє в середині (Автентифікація та авторизація). Ширша рамка: кеш, кукі й сховища переживають перезавантаження, тож без чистки результат залежить від порядку запуску — а це вже не флак, а прихована залежність між тестами.
Зелено локально, червоно в CI
Це найбільший біль практики. Локальна машина й агент CI — різні світи, і різниця не в «магії», а в трьох задокументованих речах: свіжа машина на кожен прогін, інші ресурси, інше середовище рендерингу.
Свіжа машина. Кожен прогін іде на щойно розгорнутій віртуальній машині, стан між прогонами не переноситься, а один виконує одну за раз. Звідси перший крок налагодження, який дока інструмента ставить перед усіма іншими: переконатися, що агент узагалі здатний запускати браузери — офіційним на Linux-агентах або встановленням залежностей через CLI. Для помилок «Failed to launch browser» є задокументований важіль DEBUG=pw:browser.
Ресурси, а не «повільний CI». Кількість воркерів за замовчуванням ставиться за виявленою кількістю ядер, і перевищення цього числа спричиняє непотрібні таймаути й падіння — тобто це не випадковість, яку лікує перезапуск джоби. Рекомендація дослівна: workers: 1 у CI, щоб пріоритетом були стабільність і відтворюваність; причина названа тут же — послідовний прогін дає кожному тесту всі ресурси системи. Ширшу паралельність беруть між джобами або потужною self-hosted системою.
// playwright.config.ts — найдешевший CI-специфічний фікс
export default defineConfig({
workers: process.env.CI ? 1 : undefined,
});
Інше середовище рендерингу. За замовчуванням браузери запускаються в headless-режимі, а на Linux-агентах headed узагалі вимагає встановленого Xvfb (в офіційному образі й GitHub Action він уже є). Результат рендерингу залежить від операційної системи, її версії, налаштувань, заліза, джерела живлення й самого headless-режиму — звідси припис ганяти візуальні порівняння в тому самому середовищі, де знято бейзлайн.
Паралельний прогін як лакмус ізоляції
Паралельність вмикають заради швидкості, а отримують ще й діагноз: вона оголює дефекти ізоляції, яких послідовний прогін не показував.
Механіка проста. Паралельність — це кілька воркер-процесів, що працюють одночасно, а одиниця паралельності за замовчуванням — файл, а не тест: усередині файлу тести йдуть по черзі в одному воркері. Воркер — процес операційної системи зі своїм браузером, тож спільного стану й глобальних змінних між воркерами немає, а після падіння тесту воркер гарантовано вбивається, щоб наступні стартували в чистому оточенні. Окремого прапорця «вимкнути паралельність» немає — її вимикають обмеженням до одного воркера. Послідовний режим для групи тестів дока прямо називає не рекомендованим: падіння одного пропускає всі наступні, а ретрай іде на всю групу.
Одна деталь ламає перенесення інтуїції між інструментами: воркер не завжди процес. У CodeceptJS це може бути потік із незалежним екземпляром інструмента — і тоді питання «чи ізольовані глобальні змінні» має іншу відповідь. Там-таки названо побічний наслідок паралельності, який дратує при дебагу: покрокового виводу в немає, бо потоки не змішуються охайно.
в усіх випадках одна: ізоляція знімає питання порядку виконання — саме вона робить паралельність і шардінг можливими. Тому «падає в паралелі» майже завжди читається як «спільна фікстура»: у таксономії xUnit клас можливий тільки за глобально спільної , яку різні тести змінюють. Сусідній клас — Resource Leakage: тести або система зʼїдають кінцеві ресурси, симптом упізнаваний («дедалі повільніше, потім раптово падає, після перезапуску тимчасово гоїться»), а рецепт джерела контрінтуїтивний — зменшити пул ресурсів до одиниці, щоб винний тест проявився швидше.
Хто винен: застосунок, тест чи середовище
Коли тест червоний, головне питання не «як зробити зелено», а «де саме дефект». Канонічне означення тут вужче за побутове: — це тест, що дає різні результати залежно від того, коли і хто його запускає; у вимірювальному формулюванні Google — той самий код дає і зелений, і червоний результат.
Обидва «прості» виходи джерело називає шкідливими: прибрати тест означає свідомо втратити перевірку, а лишити його червоним — затулити інші падіння, бо перехід «зелене → червоне» помітний, а «одне червоне → два червоних» уже ні.
Процедура діагностики теж канонічна, і вона не про здогади, а про систематичний збір даних у часі за чотирма осями: середовище, підмножина тестів, повтор поспіль, паралельні ранери. Кожна вісь дає власний маркер:
| Спостереження | Клас за таксономією | Куди дивитися |
|---|---|---|
| Наодинці зелений, у сюїті падає | Interacting Tests | Спільна фікстура, порядок, дані |
| Перший запуск інакший за наступні | Unrepeatable Test | Фікстура, що переживає прогін |
| Падає лише в паралелі | Test Run War | Глобально спільна фікстура |
| Падає випадково навіть на одному ранері | Nondeterministic Test | Різні значення на вході алгоритму |
| Дедалі повільніше, потім раптово падає | Resource Leakage | Кінцеві ресурси; звести пул до одиниці |
Далі — три речі, які цю картину доповнюють. Перша: офіційна дока інструмента прямо називає гонку станів однією з головних причин флакі-тестів — іноді браузер устигає першим, іноді код тесту. Друга: до списку джерел нестабільності інший інструмент додає , доступність сервера й бази, залежності від ресурсів і мережу — тобто «винне середовище» є законною гілкою, а не відмовкою. Третя, найнезручніша: застосунок справді буває недетермінованим за побудовою. Класика — сховища: «the responses may not reflect the results of a recently completed write operation. If you repeat your read request after a short time, the response should eventually return the more recent item». Тут тест не флакне, а чесно ловить документовану поведінку, і детермінізм інколи перемикається опцією запиту, а не виборюється ретраями.
Числа, якими цю тему часто підпирають, вживають лише з атрибуцією й датою: за вимірюванням Google 2016 року, 1,5 % прогонів дають флакі-результат, близько 16 % тестів мають якийсь рівень флаку, а 84 % переходів «зелений → червоний» — це флак, а не . Там-таки названо механізм шкоди: звикання до хибних сигналів веде до ігнорування справжніх падінь.
Наша практика (не канон). Робочий порядок кроків — наш, джерела процедуру в такому вигляді не описують. Читаємо помилку буквально (перехоплення вказівникових подій, таймаут очікування селектора й expected 200, got 500 — це вже три різні гілки), далі дивимось артефакти прогону, далі відтворюємо в умовах CI, далі ганяємо тест підряд багато разів і насамкінець порівнюємо «наодинці» проти «в наборі». І висновок формулюємо чесно: доки причина не відтворена, коректне формулювання — «ще не встановлено», а не поспішний «root cause». Якщо винен застосунок, тест має лишатися червоним і фіксувати регресію, а не «зеленитися» через force, підняття таймауту чи паузу.
Типові помилки
- Виглядає як поганий локатор, а насправді інший контекст. Елемент видно в DevTools, тест його не знаходить — перша гіпотеза не «переписати селектор», а
iframeчи shadow tree. - Виглядає як «елемента немає», а насправді він не готовий. Вузол уже в DOM, але без геометрії, ще їде або перекритий оверлеєм.
- Виглядає як баг застосунку, а насправді гідратація. Кнопка намальована, реакції немає: обробники ще не навісилися.
- Виглядає як флак, а насправді залежність від порядку запуску. Зелені поодинці й червоні в наборі — стан протік через сховище, кукі чи незнятий маршрут.
- Виглядає як лікування, а насправді маскування.
force, збільшений таймаут і фіксована пауза прибирають червоний колір, а не причину. - Виглядає як випадковість CI, а насправді конфігурація. Воркерів більше, ніж ядер на агенті, — це задокументована причина таймаутів і падінь, тож перезапуск джоби на тому самому агенті нічого не змінить.
Підсумок
- Флак браузерного походження — це розбіжність темпів: тест діє за годинником, застосунок — за подієвим циклом. Синхронізація означає очікування конкретного стану, а не паузу, а щоб знайти винного, дані збирають систематично — за чотирма осями: середовище, підмножина тестів, повтор поспіль, паралельні ранери.
- Автоочікування закриває придатність елемента до дії: видимість, стабільність, отримання подій, увімкненість. Прикладну готовність воно не знає.
- Між «є в DOM» і «дія проходить» лежить конвеєр рендерингу; між «розмітка є» і «сторінка реагує» — гідратація.
- Детермінізм купують контролем над входами: перехоплення прибирає залежність від сервера, свіжий контекст — від чужого стану, тротлінг робить гонку видимою до CI.
- Перед будь-яким повтором перевір : ретрай
GET/PUT/DELETEбезпечний, ретрайPOSTстворює дублі.
Можливі питання
- «Тест падає раз на десять прогонів. Ваші дії?» Дивляться на порядок міркування: локалізувати, на чому падає → зрозуміти, якого стану тест не дочекався → правити. Відповідь «додам таймаут» закриває тему не на вашу користь.
- «Чим явне очікування відрізняється від неявного й від автоочікування?» Перевіряють, хто формулює умову й чому неявне з явним не змішують.
- «Елемент є в DOM, але клік не проходить. Чому?» Очікують кілька причин: немає геометрії, елемент рухається, перекритий оверлеєм, задизейблений, потік зайнятий.
- «Що таке гонка станів і як вона виглядає у вебі?» Дивляться, чи є за терміном механізм: черга задач, порядок завершення, «перемагає останній, що відповів».
- «Навіщо мокати мережу в UI-тесті і коли цього робити не варто?» Перевіряють баланс детермінізму проти дрейфу контракту.
- «Тести зелені поодинці, але червоні в наборі. Де шукати?» Очікують слово «ізоляція» і конкретику: сховища, кукі, дані спільного акаунта, незняті перехоплення.
- «Локально зелено, на CI червоно. Що перевірите першим?» Дивляться, чи є в голові модель агента: свіжа машина на кожен прогін, воркери за кількістю ядер, headless за замовчуванням і залежне від середовища відтворення візуальних порівнянь.
- «Чому не можна просто поставити
sleep(3)?» Сильна відповідь пояснює не «це погана практика», а чому пауза не перевіряє стан і програє в обидва боки.
Джерела
Синхронізація: діяти рівно тоді, коли застосунок готовий
- Playwright — Auto-waiting (actionability) — набір перевірок придатності, означення видимості, стабільності, отримання подій та увімкненості; що саме вимикає
force. - Playwright — Locators — локатор як рецепт пошуку, що резолвиться в момент дії.
- Playwright — class Page — очікування мережевої відповіді й вимога створювати проміс до дії.
- Selenium — Waiting Strategies — явне, неявне й жорстке очікування та чому їх не змішують.
- Playwright — Best Practices — орієнтир, на що саме синхронізуватися в тесті.
- Testing Library — Guiding Principles — правило «перевіряй те, що бачить користувач».
- Playwright — Visual comparisons (test snapshots) — вимкнення анімацій для стабільного порівняння.
- MDN — JavaScript execution model — однопотокове виконання, черги задач і мікрозадач, run-to-completion, наслідки блокування потоку.
- WHATWG HTML Standard — Event loops — цикл подій як координатор подій, скриптів, рендерингу й мережі; контрольна точка мікрозадач.
- WHATWG HTML Standard — Timers (§8.6) — відсутність гарантії виконання таймера за розкладом.
- Chrome for Developers — Inside look at modern web browser (part 3) — головний потік і робота браузера поза потоком JavaScript.
- MDN — Using the Fetch API — скасування застарілого запиту через
AbortController. - Playwright — Auto-waiting (actionability) — очікування за умовою як альтернатива фіксованій паузі.
- Selenium — Waiting Strategies — чому
sleepмаскує гонку замість того, щоб її усунути. - CWE-367 — Time-of-check Time-of-use (TOCTOU) Race Condition — гонка як окремий клас слабкості: стан змінюється між перевіркою й використанням.
- OWASP Cheat Sheet — Business Logic Security — припис ставитися до конкурентності як до реальної й перевіряти її тестом.
«Елемент є в DOM, а клік не проходить»
- MDN — Critical rendering path — інкрементальна побудова DOM і CSSOM, дерево рендерингу, layout і paint як етапи критичного шляху.
- web.dev — Rendering performance (pixel pipeline) — пʼять етапів конвеєра, растеризація всередині paint, скорочені проходи без layout.
- Chrome for Developers — Inside look at modern web browser (part 3) —
display: noneпротиvisibility: hiddenу дереві рендерингу. - Chrome DevTools — Simulate mobile devices with device mode — залежність результату layout від розміру .
- Playwright — Auto-waiting (actionability) — наявність вузла в DOM як найслабша гарантія готовності та вимога стабільності перед дією.
- MDN — Introduction to the DOM — DOM як дерево обʼєктів у памʼяті, яке скрипти змінюють після завантаження.
- MDN Glossary — SPA (Single-page application) — вміст SPA зʼявляється після виконання скрипта, а не в розмітці відповіді.
- curl — manual page (--write-out variables) — HTTP-клієнт без виконання JavaScript бачить лише вихідну відповідь.
- WHATWG DOM Standard — фази події, спливання й делегування,
preventDefault, ретаргетинг на shadow boundary. - MDN — Introduction to events — скасована стандартна дія як причина «клік є, навігації немає».
- Playwright — Locators — локатор як рецепт пошуку; пробивання відкритих shadow tree і межа для XPath.
- MDN — Using shadow DOM — shadow tree, режими
openіclosed, невидимість вузлів для звичайного пошуку. - Playwright — Frames — фрейм як окремий контекст пошуку.
- Selenium — Working with IFrames and frames — перемикання контексту пошуку на фрейм.
- MDN — Same-origin policy — обмеження на доступ до вмісту документа іншого походження.
Локатор: стабільність починається з нього
- MDN — CSS selectors — типи селекторів і комбінатори як основа добору.
- MDN — Attribute selectors — часткові збіги
^=,$=,*=і чому~=до них не належить. - MDN — :has() CSS pseudo-class — добір предка й межа доступності за Baseline.
- W3C — XML Path Language (XPath) Version 1.0 — осі вгору по дереву, пастка
//x[1]і рядкове значення елемента протиtext(). - MDN — Use data attributes —
data-*як власний атрибут розмітки для машинного . - Playwright — Locators — рекомендовані локатори за роллю й доступним імʼям,
data-testidяк запасний варіант. - Testing Library — Guiding Principles — шукати елемент так, як його знаходить користувач і читач екрана.
Розмітка є, а обробників ще немає: CSR, SSR і гідратація
- web.dev — Rendering on the web — означення CSR і SSR, гідратація та розрив між «виглядає інтерактивним» і «реагує на введення».
- MDN — Document: DOMContentLoaded event — межі сигналу «HTML розібрано».
- MDN — Window: load event —
loadяк пізніша й грубіша межа завантаження. - Playwright — class Page — стани завантаження сторінки й позначка небажаності очікування на «тиху мережу».
- MDN Glossary — SPA (Single-page application) — один документ, який далі перемальовує JavaScript без перезавантаження.
- MDN — History API —
pushState/replaceStateбез запиту на сервер і поведінкаpopstate.
Дані приїжджають окремо: запит, мережа й затримка
- MDN — Using the Fetch API —
fetchне реджектить на404, перевіркаresponse.ok, одноразовість потоку тіла. - MDN — XMLHttpRequest: readyState property —
DONEозначає «завершилось», а не «успішно». - RFC 9293 — Transmission Control Protocol (TCP) — надійний упорядкований потік і повторна передача втрачених сегментів.
- RFC 8446 — TLS 1.3 — як частина вартості першого зʼєднання.
- MDN — Connection management in HTTP/1.x — зʼєднання й чому перший запит дорожчий.
- Chrome DevTools — Network features reference — фази запиту, стан очікування й обмеження на одночасні зʼєднання.
- MDN — Understanding latency — означення затримки, вартість першого запиту, різниця з часом очікування сервера.
- MDN Glossary — Bandwidth — пропускна здатність як окрема від затримки величина.
- RFC 9113 — HTTP/2 — HOL-блокування в HTTP/1.x і пряма заява, що на рівні TCP HTTP/2 його не усуває.
- RFC 9114 — HTTP/3 — чому втрата пакета в TCP спиняє всі й що змінює окремий потік.
- RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport — порядок гарантується лише всередині потоку.
- RFC 9110 — HTTP Semantics — семантика шлюзових помилок, які клієнт бачить як таймаут.
- Playwright — Timeouts — таймаути тест-фреймворка як окремий рівень.
Як зробити тест детермінованим
- Playwright — Network — модель перехоплення, три рішення обробника й вибірковість правил.
- Playwright API — class: Route — ігнорування заборонених заголовків і закритий перелік кодів обриву.
- Playwright — class Page — маршрут діє лише на запити після його реєстрації.
- Playwright — Mock APIs — підміна відповіді як спосіб отримати детерміновані дані.
- Ian Robinson — Consumer-Driven Contracts: A Service Evolution Pattern — дрейф контракту між споживачем і постачальником як ціна мокання.
- Chrome DevTools — Network features reference — меню Throttling, пресети, режим Offline і Disable cache.
- Selenium — Waiting Strategies — прив'язка очікувань до подій замість сподівання на швидкий канал.
- RFC 9110 — HTTP Semantics — означення ідемпотентного методу й те, що воно про стан сервера, а не про відповідь.
- MDN — HTTP request methods — які методи ідемпотентні, а які ні.
Чужий стан: кеш, сховища, сесія й токен
- RFC 9111 — HTTP Caching — HTTP-кеш як окремий рівень зберігання відповідей.
- MDN — HTTP caching — чому кеш не можна вимикати в тестах самої поведінки кешування.
- MDN — Service Worker API — програмований кеш, що живе повз HTTP-кеш.
- MDN Glossary — CDN — проміжний кеш між користувачем і origin-сервером.
- Playwright — Test Isolation (browser contexts) — окремий контекст браузера як найдешевша ізоляція стану.
- MDN — Web Storage API — область видимості й час життя
localStorageтаsessionStorage. - Playwright — Authentication — збережений стан автентифікації: що він захоплює і чому містить живі секрети.
- RFC 6265 — HTTP State Management Mechanism — кукі як механізм стану й автоматичне надсилання браузером.
- MDN — Using HTTP cookies — сесійна проти постійної кукі, атрибути доставки й час життя.
- RFC 7519 — JSON Web Token (JWT) — base64url як кодування, а не шифрування; claim
expяк момент, з якого токен не приймають. - Chrome for Developers — Inside look at modern web browser (part 3) — мережевий шар і сховище як підсистеми, якими тест керує ззовні.
- GitHub Docs — Understanding GitHub Actions — свіжа віртуальна машина на кожен прогін і один ранер на одну джобу.
- Playwright — Continuous Integration — здатність агента запускати браузери як перший крок,
workers: 1у CI, воркери за кількістю ядер і наслідок перевищення, headless за замовчуванням, Xvfb на Linux,DEBUG=pw:browser. - Playwright — Visual comparisons (test snapshots) — залежність рендерингу від ОС, версії, налаштувань, заліза й режиму запуску.
Паралельний прогін як лакмус ізоляції
- Playwright — Parallelism — воркер як процес ОС, файл як одиниця паралельності, знищення воркера після падіння, вимкнення паралельності одним воркером,
serialяк не рекомендований режим. - CodeceptJS — Parallel Execution — воркер як потік із незалежним екземпляром інструмента й відсутність покрокового виводу.
- Playwright — Test Isolation (browser contexts) — ізоляція як передумова, що знімає питання порядку виконання.
- xUnit Test Patterns — Erratic Test — Test Run War лише за глобально спільної фікстури; Resource Leakage і порада звести пул ресурсів до одиниці.
Хто винен: застосунок, тест чи середовище
- xUnit Test Patterns — Erratic Test — означення нестабільного тесту, шкідливість обох «простих» виходів, систематичний збір даних за чотирма осями, класи Interacting Tests, Unrepeatable Test, Test Run War, Nondeterministic Test і Resource Leakage.
- Google Testing Blog — Flaky Tests at Google and How We Mitigate Them — вимірювальне означення, частки 1,5 %, 16 % і 84 % як вимір 2016 року, механізм шкоди від звикання до хибних сигналів.
- Selenium — Waiting Strategies — гонка станів як одна з головних причин нестабільних тестів.
- Cypress — Test Retries — виклики API, доступність сервера й бази, ресурси й мережа як названі джерела нестабільності.
- AWS — DynamoDB read consistency — кінцева узгодженість: свіжий запис може не потрапити у відповідь, а сильна узгодженість є опцією запиту.
Відео до теми
Тест падає приблизно раз на десять прогонів, а руками сценарій проходить завжди. Що відбувається?
Це типовий (flakiness) браузерного : тест і застосунок рухаються в різному темпі. Скрипт виконує команди майже миттєво й послідовно, а сторінка в цей час ще добудовує розмітку, виконує скрипти й чекає на відповіді бекенда. На швидкій машині розробника все встигає — тест зелений; варто агенту CI підгальмувати, і клік прилітає раніше, ніж кнопка стала робочою. Тобто причина не в тому, що «браузер повільний», а в тому, що між тестом і застосунком немає домовленості про момент, коли можна діяти. Лікується це не паузами, а очікуванням конкретного стану: конкретного елемента, конкретної відповіді, конкретного маркера в інтерфейсі. На співбесіді тут дивляться саме на хід міркування — локалізувати крок, зрозуміти, якого стану тест не дочекався, і лише потім правити.
Чим автоочікування відрізняється від явного й неявного очікування?
Це три різні речі. (auto-waiting) — вбудована поведінка інструмента: перед кожною дією він сам ганяє набір перевірок придатності й повторює їх до вичерпання . Явне очікування — умова, яку інженер формулює власноруч (класика — WebDriverWait у Selenium): «чекай, поки в таблиці зʼявиться рядок із цим номером замовлення». Неявне очікування — глобальне налаштування, що реагує лише на один випадок: елемента ще немає в дереві. Різниця принципова, бо автоочікування закриває тільки готовність елемента до дії — про прикладну логіку воно не знає нічого: чи доїхала відповідь, чи перерахувався кошик, чи навісився обробник. А змішувати явне з неявним не радять самі документації: два механізми накладаються, фактичний час очікування стає непередбачуваним, і потім важко зрозуміти навіть те, чому тест чекав саме стільки.
Які саме перевірки інструмент виконує перед тим, як клікнути?
Перед дією одночасно перевіряється кілька умов, і поки хоча б одна не виконана, дія відкладається; вичерпався таймаут — тест падає з TimeoutError. Для кліку це: веде рівно до одного елемента, елемент видимий, він стабільний (не рухається), він справді отримує вказівникові події в тій точці, куди піде клік, і він увімкнений. Для введення тексту додається ще одна умова — поле має бути редаговане. Знати цей перелік корисно суто практично: у журналі падіння інструмент друкує, якого локатора він чекав і які саме перевірки мали пройти перед дією. Далі перелік працює як готовий список гіпотез: оверлей зверху, незавершена анімація, форма, що ще валідується. Кандидат, який тримає цей перелік у голові, читає лог, а не гадає.
Елемент має opacity: 0. Чи вважатиме його тест видимим?
Так — і це одна з найчастіших несподіванок. Критерій видимості геометричний: у елемента є непорожній і немає visibility: hidden. Прозорість у ці умови не входить, тому повністю прозорий елемент формально видимий, і тест спокійно в нього клікне, хоча користувач не бачить нічого. Для контрасту: display: none і нульові розміри дають невидимість, бо геометрії немає взагалі. Наслідок подвійний. По-перше, модалка, що «зникає» плавним згасанням прозорості, ще ловить кліки — звідси примарні падіння на наступному кроці сценарію. По-друге, коли треба перевірити саме те, що бачить користувач, перевірки видимості замало: потрібен на стан, який справді змінюється — клас, атрибут або зникнення вузла з дерева.
Чому не можна просто поставити фіксовану паузу на три секунди?
Бо пауза нічого не перевіряє — вона лише витрачає час. Програє вона одразу в обидва боки: трьох секунд замало на завантаженому CI, і тест усе одно почервоніє; трьох секунд забагато на локальній машині, і кожен такий рядок множиться на сотні тестів у прогоні. Гірше те, що пауза ховає діагноз: тест зеленіє, а справжня причина — гонка чи повільний запит — лишається невідомою й вилазить пізніше в іншому місці. Альтернатива одна: чекати на стан, який означає готовність. Асерти, що самі повторюють перевірку до таймауту, це вже роблять за вас, тож дописувати перед ними паузу — подвійна робота без результату.
Що робить опція force і коли її ввімкнення чесне?
force вимикає необовʼязкові перевірки придатності — зокрема ту, що елемент реально отримує клік у точці дії. Інструмент перестає питати «а він не перекритий?» і бʼє по координатах. Найпоширеніший сюжет виглядає так: тест червонів, бо клік перехоплював прозорий оверлей; додали force, тест позеленів, дефект лишився в продукті. Це і є маскування: користувач у тому самому місці клікнути не зможе, а тест каже, що все гаразд. Чесних застосувань мало, і вони усвідомлені — наприклад, коли елемент навмисно перекритий декоративним шаром, поведінка описана у вимозі, і команда свідомо погоджується не перевіряти клікабельність. Робоче правило: force у ревʼю має йти з коментарем, чому саме тут перевірка зайва.
Елемент уже є в DOM, але клік не проходить. Назвіть причини.
Наявність вузла в дереві документа — найслабша з можливих гарантій готовності; між нею й «дія проходить» лежить увесь конвеєр рендерингу. Звідси перелік. У елемента ще немає геометрії: він у DOM, але в дерево рендерингу не потрапив або layout ще не порахував розміри. Елемент рухається — анімація чи тулбар, що виїжджає, зсувають його, і перевірка стабільності не проходить. Зверху лежить оверлей: спінер, банер про кукі, невидима підкладка модалки — вона й перехоплює вказівникову подію. Елемент задизейблений — власним disabled, батьківським fieldset або атрибутом aria-disabled. Головний потік зайнятий важким перерахунком, тому сторінка намальована, але на події не реагує. І окремим, найпідступнішим пунктом — обробник ще не навісився, бо клієнтські скрипти не доїхали. Сильна відповідь — це не одна причина, а саме такий список із порядком перевірки.
У DevTools я знаходжу елемент руками, а тест його не бачить. З чого почнете розбір?
Перша гіпотеза — не «поганий селектор», а «не той контекст пошуку». Кандидатів два. iframe — це окремий документ зі своїм деревом, і поки контекст не перемкнули (frameLocator у Playwright, switchTo().frame() у Selenium), пошук іде не там, де ви дивитесь. Другий — (shadow DOM): елемент живе у власному прихованому під-дереві компонента, куди звичайний document.querySelectorAll() не дістає. Тут є нюанс, який любить сеньйор-інтервʼюер: відкриті shadow tree Playwright пробиває автоматично для CSS- і текстових локаторів, а XPath крізь не працює — тож «той самий елемент» знаходиться CSS-ом і не знаходиться XPath-ом. Ще одна деталь: у закритому режимі властивість shadowRoot віддає null, але це декларація наміру автора компонента, а не механізм захисту. Перевіряється все за хвилину: подивитися в , чи є елемент у document, і перерахувати фрейми сторінки.
Що таке подієвий цикл і чому setTimeout(fn, 0) не означає «зараз»?
(event loop) — це диспетчер усередині браузера, який розводить у часі події, скрипти, рендеринг і роботу з мережею. Ключове: однопотоковий, тобто в кожен момент виконується рівно один фрагмент коду, і кожна задача доводиться до кінця, перш ніж почнеться наступна (run-to-completion). Мережу й таймери обслуговують механізми поза цим потоком — однопотоковий саме рушій, а не браузер цілком. Тому setTimeout(fn, 0) кладе у чергу задач і означає «якнайшвидше, щойно звільниться потік»: специфікація не обіцяє, що таймер спрацює точно за розкладом. Окремо стоїть черга мікрозадач — реакції , queueMicrotask, MutationObserver; чергою задач вона не є, і цикл спорожнює її повністю після кожної задачі. Для тестування звідси два висновки: довгий синхронний код гальмує і обробку кліків, і черговий кадр (звідси «клік ніби пройшов, а нічого не сталося»), а порядок виконання асинхронних шматків залежить від того, у яку чергу вони потрапили.
Що таке гонка станів і як вона виглядає в браузері?
Гонка станів (race condition) — ситуація, коли результат залежить від того, яка з асинхронних операцій завершиться першою, а гарантій щодо цього порядку немає. Оскільки кожна задача доводиться до кінця, гонки трапляються не в межах однієї задачі, а на стику між ними. Канонічний приклад — живий пошук: користувач набирає «ab», летить запит; дописує «c», летить другий; відповідь на «abc» повертається швидше, а відповідь на «ab» приходить після неї й перезаписує список. На екрані результати застарілого запиту, хоча в полі вже «abc»: перемагає той, хто відповів останнім, а мав би перемогти той, кого надіслали останнім. У продукті це закривають так: щойно летить новий запит, попередній скасовують через AbortController. У наївному тесті ховаються ще дві гонки — вузла може ще не бути в дереві, а його вміст перемалюється лише після відповіді, — і фіксована пауза не знімає жодної з них. Тема має й безпековий бік: гонки винесені в окремий клас слабкостей, а найвідоміший їх різновид, , — це коли між моментом перевірки й моментом використання стан ресурсу встигає змінитися. Звідси й порада: якщо два запити можуть змагатися, напишіть тест, який їх навмисно зіштовхне.
Чому проміс очікування мережевої відповіді створюють до дії, а не після?
Бо очікування ловить лише те, що сталося після його створення. Якщо спершу клікнути, а потім почати чекати на відповідь, швидкий бекенд встигне відповісти в проміжку — і тест зависне до таймауту, чекаючи на подію, яка вже проїхала. Тому послідовність жорстка: створити проміс із предикатом (URL і статус), зробити дію, дочекатися промісу. Помилка підступна тим, що на повільному стенді код працює роками, а після оптимізації чи переїзду на швидше середовище раптом починає . Той самий принцип діє й для : правило спрацьовує лише на запити, зроблені після його реєстрації, тож реєструвати його треба до навігації чи кліку, який тригерить запит.
Сторінка намальована повністю, а кнопка не реагує на кліки. Що це таке?
Найімовірніше — гідратація (hydration). Якщо розмітку побудував сервер (SSR), браузер показує готовий HTML майже одразу, але поки клієнтські скрипти не виконаються й не навісять обробники, це фактично картинка. Виходить розрив: тест бачить видиму кнопку, впевнено клікає — і нічого, бо реагувати ще нема кому. Перевірка видимості тут бреше: вона доводить лише те, що елемент присутній у розмітці, а не те, що він уже вміє реагувати. Стандартного DOM-атрибута «гідратацію завершено» не існує, тому чекати треба на прикладну ознаку інтерактивності: зникнення disabled з кнопки, появу стану, який ставить саме клієнтський код, або узгоджений із розробниками маркер. У застосунку з рендерингом на клієнті (CSR) симптом інший — у перші миті в дереві взагалі немає корисного вмісту, — але висновок той самий: орієнтир готовності задає застосунок, а не інструмент.
Чи існує універсальний сигнал «сторінка готова»?
Ні, і це варто вміти розкласти на рівні. DOMContentLoaded настає, коли розібрано HTML: картинки ще можуть вантажитися, а дані з API часом навіть не запитувалися. load чекає на підресурси, тобто спрацьовує пізніше й грубіше, але про фонові AJAX-запити не знає нічого. Очікування «тихої мережі» виглядає спокусливо, проте документація Playwright прямо позначає його як небажане: у застосунку з опитуванням, вебсокетами чи аналітикою мережа не затихає взагалі. У SPA додається ще одне: документ вантажиться один раз, тому після клієнтської навігації load більше не спрацює — роутер міняє адресу через History API без походу на сервер. Саме тому після переходу всередині SPA перевіряють дві речі: що змінився URL і що зʼявився очікуваний контент.
Яку ієрархію локаторів ви обираєте і які пастки знаєте в CSS та XPath?
Порядок такий: спершу роль плюс доступне імʼя, далі видимий текст або лейбл, потім data-testid, потім CSS за стабільним атрибутом і лише в крайньому разі XPath. Логіка — шукати елемент так, як його знаходить користувач: такі локатори не прив'язані до верстки й заодно виявляють проблеми доступності (коли «кнопка» насправді є клікабельним div без ролі, локатор за роллю до неї просто не дістанеться — і це знахідка, а не незручність). У кожного рівня своя ціна: текстові локатори ламаються при зміні локалі, а data-testid — це контракт із кодом, який легко видалити під час рефакторингу «бо він нічого не робить». Пастки, на яких ловляться найчастіше: серед атрибутних селекторів часткові збіги дають ^=, $= і *=, а ~= до них не належить: він звіряє ціле слово зі списком значень, розділених пробілами. У XPath contains(text(), ...) дивиться лише на перший текстовий вузол, тому кнопка з вкладеним тегом усередині не знайдеться (рятує contains(., ...)), а //div[1] — це не «перший div у документі», а «перший серед сусідів», для чого потрібні дужки: (//div)[1]. Найгірше в обох випадках те, що тест не падає, а тихо працює з іншим елементом.
Чому fetch не кидає помилку на 404 і що з цього випливає для тестів?
Відповідь 404 чи 500 — це нормально завершений обмін, тому проміс резолвиться успішно, просто з відповідним статусом. Реджект стається на іншому класі проблем — на збоях самого обміну; документація називає прикладами мережеву помилку й некоректну схему, а вичерпного переліку не дає. Звідси класичний баг у продукті: код вважає, що потрапив у гілку успіху, і намагається розібрати помилку як дані — а на екрані порожній список замість повідомлення. Перевіряти статус доводиться вручну: властивість ok істинна лише для діапазону 2xx. У тесті це означає два різні асерти — окремо на код відповіді, окремо на те, що інтерфейс коректно показав помилку. Побічна деталь, на якій ловляться: тіло відповіді — потік, який читається один раз, тож повторна спроба його прочитати впаде. У старому XMLHttpRequest пастка аналогічна: readyState зі значенням 4 означає «обмін завершився», а не «завершився успішно».
Навіщо мокати мережу в UI-тесті й коли цього робити не варто?
Перехоплення мережі (network interception) ставить тест посередині — між браузером і сервером: реєструється правило для шаблону URL, а обробник вирішує долю запиту. Варіантів три: пропустити далі (по дорозі дозволено підмінити адресу, метод, заголовки або тіло), відповісти самому без походу на сервер або обірвати запит мережевою помилкою. Виграш прямий: детерміновані дані замість плаваючих, миттєва відповідь замість очікування і, головне, можливість відтворити те, чого на живому бекенді не викличеш, — 500, порожній список, обрив зʼєднання. Зворотний бік — дрейф контракту: замокавши все, ви перевіряєте власні уявлення про API, і коли бекенд змінить формат, тести лишаться зеленими. Тому варто те, що є перешкодою, а не предметом перевірки, і обовʼязково тримати шар тестів, які ходять у справжній сервіс. З практичних тонкощів: правило діє лише на запити після реєстрації; усе, що не підпало під шаблон, спокійно піде на реальний сервер; а заборонені заголовки на кшталт Cookie, Host чи Content-Length підмінити не вийде — перевизначення просто проігнорують, і тест «підмінить» їх лише в уяві автора.
Навіщо AQA тротлінг, якщо продуктивність міряє не він?
(throttling) у панелі мережі ріже і додає — і для AQA це не вимірювання швидкості, а спосіб зробити гонки видимими. На швидкому каналі вікно між дією і відповіддю таке вузьке, що дефект просто не встигає проявитися: кнопка лишається активною, поки запит летить, але клікнути вдруге ніхто не встигає. На повільному профілі це вікно розтягується — і подвійне надсилання, зникання спінера раніше за дані чи перемальовування списку старою відповіддю зʼявляються стабільно. Звідси незручний висновок про такі падіння: повільний профіль нічого не ламає, він лише збільшує ймовірність того, що дефект нарешті проявиться, — на швидкому каналі той самий дефект просто ще не встиг спрацювати. Технічна деталь: пресети між версіями браузера змінювалися, тому надійніше задати власний профіль, а поруч у тій самій панелі живуть режим Offline і вимкнення кешу.
Тести зелені поодинці, але червоні в повному наборі. Де шукати причину?
Це майже завжди не флак, а прихована залежність між тестами: попередній лишив по собі стан, наступний його успадкував. Порядок перевірки такий. Клієнтські сховища: localStorage розділяється лише за походженням і переживає перезапуск браузера, а sessionStorage привʼязаний ще й до вкладки, тому новий контекст його не бачить — звідси «залогінилися в одному місці, а в іншому користувач анонімний». Кукі й кеш — те саме, вони теж переживають перезавантаження. Далі — незняте перехоплення мережі, яке лишилося з попереднього тесту й тепер підміняє відповіді там, де цього ніхто не чекає. І спільний тестовий акаунт чи спільний запис у базі, який два тести правлять одночасно. Найдешевша ізоляція — окремий на тест: він стартує з чистими кукі й сховищами. Але межу треба памʼятати: чистка на клієнті не прибирає стану на сервері, тож якщо тест зіпсував дані акаунта, свіжий контекст не врятує.
Що таке ідемпотентність і як вона впливає на рішення про ретрай?
Метод ідемпотентний, якщо кілька однакових запитів лишають сервер у тому самому стані, що й один запит. До таких належать усі , а ще PUT і DELETE. POST — ні: два ідентичні POST /orders дадуть два окремі замовлення, і саме звідси беруться дублі від подвійного кліку або від автоматичного повтору при обриві мережі. Тонкість, на якій легко зробити хибний висновок: описує стан сервера, а не відповідь, тому повторний DELETE, що повертає 404, поводиться правильно — ресурс уже видалено, стан не змінився. Для AQA це прикладне знання у двох місцях. Перше: коли мережа моргнула посеред сценарію, саме ідемпотентність відповідає на питання, чи безпечно повторити запит. Друге: сценарій із подвійним надсиланням форми треба перевіряти свідомо — продукт має або блокувати кнопку на час запиту, або дедуплікувати запити на бекенді.
Тест упав з таймауту. Який ваш порядок дій?
Перше питання — чий саме таймаут спрацював, бо їх у стеку кілька й лікуються вони по-різному. Мережевий або таймаут HTTP-клієнта означає, що відповідь не прийшла зовсім. видно за кодом 504: проміжний вузол не дочекався застосунку, тобто винен повільний бекенд, а не тест. Таймаут тест-фреймворка — окремий рівень: тест не дочекався умови, яку сам собі й поставив. Далі читаємо повідомлення: інструмент пише, на якому локаторі він чекав і які перевірки придатності мали пройти перед дією, — це вже половина діагнозу. Потім дивимося трасу й запис мережі: чи запит узагалі пішов, чи прийшла відповідь, який був статус. І тільки після цього приймаємо рішення. Чого точно не варто робити першим кроком — піднімати таймаут: це не виправлення, а спосіб відкласти те саме падіння й заразом розтягнути прогін.
Три кейси про те, як перетворюється на діагноз: переписування нестабільного тесту живого пошуку з паузи на очікування стану, для розбору «тест не бачить елемент / клік не проходить», і навмисне сповільнення каналу, щоб гонка з подвійним надсиланням проявилася до того, як її знайде CI.
Кейс 1. Живий пошук: із паузи в очікування стану
Тест на пошук падає раз на десять прогонів і завжди по-різному: то список порожній, то в ньому результати попереднього запиту. Ось версія, з якої все починалося.
// БУЛО: пауза замість умови — зелено локально, червоно в CI
await page.getByPlaceholder('Пошук').fill('abc');
await page.waitForTimeout(2000);
await expect(page.getByTestId('search-result').first()).toContainText('abc');
Пауза тут не перевіряє нічого: вона не знає ні про запит, ні про перемальовування списку. Переписуємо на очікування конкретних подій і конкретного стану.
// СТАЛО: проміс — ДО введення, далі асерти на стан інтерфейсу
const results = page.waitForResponse(
(r) => r.url().includes('/api/search') && r.url().includes('q=abc') && r.ok()
);
await page.getByPlaceholder('Пошук').fill('abc');
await results; // відповідь саме на цей запит уже прийшла
const items = page.getByTestId('search-result');
await expect(page.getByTestId('search-spinner')).toBeHidden();
await expect(items.first()).toContainText('abc');
await expect(items).toHaveCount(3);
Що дивитися і чому:
- створено до
fill, і це не стилістика. Очікування ловить лише те, що сталося після його створення. ПоставитеwaitForResponseпісля дії — на швидкому стенді відповідь проскочить у проміжку, і тест зависне до , чекаючи на подію, яка вже проїхала. - «Відповідь прийшла» і «список перемальовано» — різні моменти. Обробка відповіді стає окремою задачею в черзі, тому після
await resultsінтерфейс ще може бути старим. Саме тому далі йдуть , які самі повторюють перевірку: зниклий спінер і вміст першого рядка. - Предикат прибитий до конкретного запиту. Живий пошук шле запит на кожну зміну поля, тож щойно тест набирає текст посимвольно або дописує до вже введеного, у мережі одночасно летить кілька запитів. Без перевірки рядка запиту проміс зарезолвиться на першій-ліпшій відповіді, і тест перевірятиме проміжний стан. Це та сама гонка, тільки перенесена в код тесту.
- Якщо результати для «ab» перезаписують результати для «abc» — це дефект продукту, а не тесту. Перемагає той, хто відповів останнім, хоча має перемагати той, кого надіслали останнім; лікується скасуванням застарілого запиту через
AbortController. Тест на це пишеться окремо — з навмисно повільною відповіддю на перший запит.
Кейс 2. Таблиця рішень: тест не бачить елемент або не може по ньому клікнути
Помилка про таймаут називає і перевірки, яких чекала дія, — далі цей перелік працює як список гіпотез. Нижче — відповідність «симптом → перша гіпотеза → перевірка, що займає хвилину».
| Симптом у логу чи в поведінці | Перша гіпотеза | Як перевірити швидко |
|---|---|---|
| Локатор нічого не знайшов, у DevTools елемент є | Інший контекст: iframe або shadow tree | У консолі document.querySelectorAll('...'); окремо перерахувати фрейми сторінки |
| Знайдено кілька елементів замість одного | Локатор описує клас віджетів, а не конкретний | Перевірити count() локатора й додати прив'язку до контейнера |
| Скарга на невидимість | Немає геометрії: display: none, нульовий розмір, вузол ще не відрендерено | Подивитися обчислені стилі й розміри bounding box |
| Скарга на нестабільність | Незавершена анімація або елемент, що виїжджає | Вимкнути анімації в тестовому середовищі й повторити |
| Клік перехоплює інший елемент | Оверлей: спінер, банер про кукі, підкладка модалки, що згасає через opacity | document.elementFromPoint(x, y) у точці кліку — покаже, хто зверху |
| Скарга на те, що елемент не увімкнений | disabled, батьківський fieldset або aria-disabled | Перевірити атрибути самого елемента й предків |
| Клік проходить, але нічого не відбувається | Гідратація не завершилась або обробник викликав preventDefault | Подивитися в панелі мережі, чи пішов хоч якийсь запит |
| Тест зелений, але перевіряє не той елемент | ~= замість *=, contains(text(), ...) на вкладеній розмітці, //div[1] без дужок | Підсвітити всі збіги локатора й порівняти з очікуваним |
| Зелений поодинці, червоний у наборі | Протік стан: кукі, сховища, незняте перехоплення, спільні дані | Прогнати той самий тест в ізольованому контексті й переставити порядок у наборі |
Що дивитися і чому:
- Порядок гіпотез важливіший за їх кількість. Контекст пошуку перевіряють до селектора: переписаний XPath не допоможе, якщо елемент живе в іншому документі. Ця помилка коштує найбільше часу, бо «правити локатор» здається очевидною дією.
opacity: 0не робить елемент невидимим для перевірок. Модалка, що плавно згасає, ще ловить кліки, хоча користувач її вже не бачить. Симптом упізнаваний: падає не той крок, який щойно закрив вікно, а наступний.- Скарга на перехоплення кліку — це знахідка, а не перешкода. Якщо банер накриває кнопку, користувач теж у неї не влучить. Реакція «додам
force» прибирає червоний колір разом із доказом дефекту. - Тихі помилки локатора найдорожчі. Тест, який стабільно перевіряє не той елемент, роками звітує «все гаразд» і не ловить — на відміну від чесного падіння, яке хтось таки розбере.
Кейс 3. Зробити гонку видимою: повільна відповідь і подвійне надсилання
Найпоширеніша гонка в замовленнях: користувач клікає «Оформити» двічі, поки запит летить, і отримує два замовлення. На швидкому каналі вікно між кліком і відповіддю таке вузьке, що дефект не встигає проявитися. Розтягуємо його штучно й перевіряємо, що інтерфейс закривається на час запиту.
import { test, expect } from '@playwright/test';
test('кнопка оформлення блокується на час запиту', async ({ page }) => {
const orderRequests: string[] = [];
// маршрут реєструємо ДО навігації; штучна затримка робить вікно гонки помітним
await page.route('**/api/orders', async (route) => {
orderRequests.push(route.request().url());
await new Promise((resolve) => setTimeout(resolve, 2000));
await route.continue();
});
await page.goto('/checkout');
const submit = page.getByRole('button', { name: 'Оформити' });
await submit.click();
// саме це й захищає від дубля: поки відповіді немає, кнопка недоступна
await expect(submit).toBeDisabled();
await expect(page.getByTestId('order-number')).toBeVisible();
expect(orderRequests).toHaveLength(1);
});
Той самий перевіряється дешевше на рівні API — двома однаковими запитами, надісланими одночасно.
test('два одночасні POST не створюють два замовлення', async ({ request }) => {
const payload = { cartId: 'cart-42', total: 1200 };
const [first, second] = await Promise.all([
request.post('/api/orders', { data: payload }),
request.post('/api/orders', { data: payload }),
]);
const codes = [first.status(), second.status()].sort();
// очікування береться з контракту API, а не з «як має бути за підручником»
expect(codes).toEqual([201, 409]);
});
Що дивитися і чому:
- і штучна розвʼязують одну задачу. Панель мережі ріже і додає затримку для всієї сторінки, перехоплення — точково для одного . Друге відтворюється в CI буквально, перше зручніше для ручного розбору. Пресети тротлінгу між версіями браузера змінювалися, тому для повторюваного результату надійніше задати власний профіль.
- Якщо тест падає на повільному профілі, він не «флакне на швидкому». Він просто ще не впав: дефект той самий, змінилася лише ймовірність збігу.
POSTнеідемпотентний за визначенням, тож два замовлення — це питання до контракту. Захист може стояти на клієнті (блокування кнопки), на сервері (дедуплікація) або в обох місцях. Тест має асертити те, що записано в контракті, — саме тому в API-перевірці стоїть пара кодів, а не одне очікування.- Маршрут зареєстровано до
goto, і це принципово. Правило спрацьовує лише для запитів, зроблених після реєстрації; усе, що не підпало під шаблон, спокійно піде на реальний сервер. Заодно памʼятайте про межу: заборонені заголовки —Cookie,Host,Content-Length— перевизначенню не піддаються, спроба їх підмінити просто нічого не змінить. - Знятий маршрут — частина . Перехоплення, що пережило свій тест, підмінятиме відповіді сусідам, і ви отримаєте класичний симптом «поодинці зелено, у наборі червоно» — це вже не , а залежність тестів один від одного.
Синхронізація: чекати стан, а не час
- Знаю набір перевірок придатності перед дією (єдиність , видимість, стабільність, отримання вказівникових подій, увімкненість; для введення — ще й редагованість) і памʼятаю їх точні означення:
opacity: 0лишається видимим,display: noneі нульовий розмір — ні, а стабільність означає незмінну геометрію два кадри поспіль. - Розрізняю , явне (
WebDriverWait) і неявне очікування, знаю, чому два останні не змішують і чому фіксована пауза разом ізforceлише ховають проблему: перша програє в обидва боки, другий вимикає перевірку, яка й ловила дефект. - Знаю правило «спершу проміс очікування або правило перехоплення, потім дія» і чому інакше швидка відповідь прослизає непоміченою.
Подієвий цикл і гонки
- Можу пояснити, що однопотоковий саме , а не браузер, і що задача виконується до кінця — звідси «клік не спрацював», коли потік зайнятий; не плутаю чергу задач (
setTimeout, події DOM, XHR) із чергою мікрозадач (,queueMicrotask,MutationObserver) і памʼятаю, щоsetTimeout(fn, 0)— це не «зараз». - Можу розповісти гонку станів на живому пошуку («перемагає той, хто відповів останнім») і назвати
AbortControllerяк лікування з боку продукту.
Від вузла в DOM до кліку
- Знаю етапи конвеєра рендерингу і те, що
display: noneу дерево рендерингу не потрапляє, аvisibility: hiddenпотрапляє й займає місце; звідси й вимога свідомо фіксувати viewport, бо геометрію рахує layout за розміром . - Можу назвати пʼять причин «елемент є, а клік не проходить» — немає геометрії, рухається, перекритий, задизейблений, зайнятий потік — і окремо шосту, найпідступнішу: обробник ще не навісився.
Межі пошуку й локатори
- Перевіряю контекст раніше за селектор:
iframe— це інший документ, — окреме під-дерево; знаю, що відкриті shadow tree пробиваються CSS- і текстовими локаторами, а XPath — ні. - Можу пояснити ієрархію локаторів (роль і доступне імʼя → текст чи лейбл →
data-testid→ стабільний CSS-атрибут → XPath як крайній засіб) і ціну кожного рівня. - Знаю тихі пастки, від яких тест не падає, а бере не той елемент:
~=про ціле слово,contains(text(), ...)про перший текстовий вузол,//div[1]не означає «перший у документі».
Готовність сторінки
- Можу пояснити гідратацію і чому «видимий» ще не означає «реагує», знаю, що стандартного маркера її завершення в DOM немає, а
DOMContentLoadedіloadмають власні межі — тому після навігації в SPA перевіряють і адресу, і появу контенту.
Детермінізм і чужий стан
- Можу пояснити три рішення обробника перехоплення (пропустити, підмінити, обірвати), правило «діє лише на запити після реєстрації» і те, що неспіймане шаблоном піде на реальний сервер, а заборонені заголовки (
Cookie,Host,Content-Length) підміні не піддаються. - Знаю ціну — дрейф контракту, через який частина тестів має ходити в справжній сервіс.
- Знаю, навіщо AQA : не міряти швидкість, а зробити гонку видимою до того, як вона стане плаваючим падінням у CI.
- Можу назвати чотири джерела «старої версії» (HTTP-кеш браузера, сховище service worker, проміжний CDN чи , власний кеш застосунку) і пояснити, чому кеш не глушать у всіх тестах підряд.
- Знаю різницю області видимості
localStorageіsessionStorage, розумію, що «зелені поодинці, червоні в наборі» — це про ізоляцію стану, а не про , і що чистка на клієнті серверного стану не прибирає. - Розумію, що описує стан сервера, а не відповідь, і вмію вирішити, чи безпечно повторити запит після обриву.
CI, паралель і пошук винного
- Тримаю в голові модель агента CI: свіжа машина на кожен прогін, за кількістю виявлених ядер (перевищення дає й падіння, а не «невезіння»), headless за замовчуванням і залежність візуального порівняння від середовища, де знято бейзлайн.
- Знаю механіку паралелі: воркер — окремий процес зі своїм браузером, одиниця паралельності за замовчуванням — файл, а вимкнути її можна лише обмеженням до одного воркера. «Падає в паралелі» читаю як спільну , а не як випадковість.
- Локалізую винного систематично, а не здогадом: збираю дані за чотирма осями (середовище, підмножина тестів, повтор поспіль, паралельні ) і читаю маркери — наодинці зелений і в червоний, перший запуск інакший, падає лише в паралелі, падає випадково на одному ранері, дедалі повільніше до раптового падіння.
Квіз
Перед стартом
- Питань: 17
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Чому локатор називають рецептом пошуку, а не посиланням на вузол?
