vyvchy
    Теми розділу

    03 · Веб і мережі для AQA

    Асинхронне завантаження: SPA/MPA, CSR/SSR

    Зміст

    Найдорожча година в житті AQA витрачається так: тест падає з «element not found», ти відкриваєш — і елемент там є. Або інакше: тест клікає по кнопці, кнопку видно, клік проходить, а нічого не відбувається. Спокуса одна — правити (locator) і додавати паузи. Причина майже завжди інша: сторінка збирається шматками, і тест зазирнув у неї між двома шматками.

    Ця глава — канон теми на сайті: решта глав посилаються сюди, а тут зібрано повний виклад того, звідки береться розрив між «браузер показав» і «застосунок готовий». І ще одна межа: тут причина, а не рецепт. Практичні прийоми боротьби з (flakiness) живуть у главі про практичні сценарії AQA, а внутрішня механіка браузера — у главі про архітектуру браузера й рендеринг.

    Запит без перезавантаження: AJAX і fetch

    AJAX (asynchronous JavaScript and XML) — обмін даними із сервером у фоні, без повного перезавантаження сторінки: оновлюється лише потрібна частина DOM (Document Object Model), і візуально сторінка не «моргає». Технічно це або старий XMLHttpRequest (XHR), або сучасний Fetch API — браузерний інтерфейс для надсилання HTTP-запитів із JavaScript, побудований на (Promise), а не , і інтегрований із service workers та CORS.

    Головний наслідок для тесту — один рядок: «сторінка відкрилась» ще не означає «дані на місці»; у момент, коли автотест бачить сторінку, потрібного контенту в DOM може не бути зовсім.

    Три деталі fetch ловлять у пастку і розробників, і тестувальників. Перша й головна: відповідь 404 чи 500 дає успішний проміс — якщо сервер відповів помилкою, fetch() «fulfills with a Response», тож статус перевіряють вручну, а response.ok істинний лише для діапазону 2xx (про самі коди — у главі HTTP статус-коди). Друга: перелік причин, коли проміс усе-таки відхиляється, у джерелі навмисно відкритий — «some errors, such as a network error or a bad scheme». Закривати його не можна: памʼятати треба межу «HTTP-відповідь є → успіх; відповіді не було → reject», а не завчений список. Третя: тіла запиту й відповіді — потоки (ReadableStream), тож одне й те саме тіло не можна прочитати двічі.

    У XHR замість промісу — пʼять станів readyState: 0 UNSENT, 1 OPENED, 2 HEADERS_RECEIVED, 3 LOADING, 4 DONE. Важать не назви, а три деталі: HEADERS_RECEIVED настає лише після того, як пройдено всі редиректи; у LOADING тіло ще тече, і responseText тримає часткову відповідь (для текстового responseType); а DONE означає «операція завершилась» — успішно або з помилкою, тож readyState === 4 сам по собі про успіх не свідчить. Скасування є в обох: xhr.abort() і AbortController із signal.

    Обидва механізми йдуть через мережевий стек браузера, тому і панель Network, і перехоплення в тесті ловлять їх однаково — незалежно від того, чим написаний фронтенд. Звідси перша заміна сліпій паузі: чекати на конкретну відповідь за URL і статусом або підмінити її (див. главу Перехоплення й мокання мережі).

    Наша практика (не канон). Термін AJAX популяризував Джессі Джеймс Ґарретт у лютому 2005 року, а технологія зʼявилася раніше — на базі XMLHttpRequest, який спершу реалізували в Microsoft для Outlook Web Access. Сюди ж деталь, про яку часто забувають: вбудованого у fetch немає — його роблять тим самим AbortController. Канонічного джерела ні під історію, ні під це твердження в реєстрі немає.

    SPA і MPA: що переміщується, а що ні

    SPA (single-page application) — це застосунок, який завантажує один вебдокумент, а далі оновлює вміст його body через JavaScript, коли треба показати інший контент. Переходи «між сторінками» документа не перезавантажують: змінюється лише URL і вміст.

    Технічна основа переходу — History API: history.pushState() і history.replaceState() міняють адресу в рядку браузера без запиту на сервер. Тут є деталь, на якій ламаються тести навігації: подія popstate реагує лише на переміщення по історії (кнопки «назад»/«вперед», history.back()), а самі виклики pushState/replaceState її не генерують.

    СерверКлієнтський роутерТестСерверКлієнтський роутерТестперевірка URL проходить тутКлік по посиланню «Профіль»pushState('/profile') — URL уже новийзапит за даними екранадані профілюконтент зʼявляється тільки теперСерверКлієнтський роутерТестСерверКлієнтський роутерТестперевірка URL проходить тутКлік по посиланню «Профіль»pushState('/profile') — URL уже новийзапит за даними екранадані профілюконтент зʼявляється тільки тепер

    Практична різниця для тесту не в архітектурі, а в наявності моменту «сторінка завантажилась». Подія load спрацьовує, коли завантажено сам документ і всі його підресурси — у багатосторінковому застосунку (MPA, multi-page application) це відбувається на кожному переході, і дочекатися навігації просто. У SPA документ завантажується раз, тож при переході старі сигнали не спрацьовують, і чекати доводиться на появу контенту.

    Друга різниця бʼє по ізоляції: документ живе весь сценарій, отже, стан у памʼяті нікуди не дівається між кроками — звідси жорсткіші вимоги: чиста сесія, скинуте сховище (Web Storage), свіжий між тестами. Третя — ціна: обсяг JavaScript росте разом із застосунком і псує чутливість сторінки до взаємодій.

    Наша практика (не канон). Далі — те, як це формулюють команди, з якими ми працювали; специфікації під ці рядки немає. MPA означує «кожен екран — окремий HTML-документ», і стан там скидається при переході, тоді як у SPA він накопичується. Клієнтський роутер перехоплює клік по посиланню, скасовує стандартну навігацію і сам міняє адресу та вміст; історичний варіант того самого — hash-роутинг (зміна частини URL після #). Звідси головна пастка: URL змінюється раніше, ніж приїжджають дані — адреса вже /profile, а на місці контенту ще скелетон (skeleton), тому перевіряти треба дві речі одночасно: URL і появу змістовного елемента. Сюди ж рівні (lazy loading) — loading="lazy" для зображень та iframe, динамічний import() для коду (code splitting), рендер компонентів через IntersectionObserver — і віртуалізація списків: нескінченна прокрутка, яка віртуалізує рядки, тримає в DOM лише видиме вікно, тож перевірка «усі 200 елементів присутні» неможлива в принципі. Рівні варто розрізняти: при loading="lazy" сам тег у DOM є, відкладено лише завантаження ресурсу, а от ліниво відрендерений компонент чи рядок віртуалізованого списку фізично відсутній у DOM, поки до нього не догорнули, — тож «element not found» тут не про поганий локатор, елемент треба спершу привести у видиму область.

    CSR, SSR і гідратація: розмітка є, а обробники ще ні

    Стратегія рендерингу відповідає на одне питання: хто будує HTML — сервер чи браузер.

    CSR (client-side rendering) — рендеринг застосунку в браузері: розмітку будує JavaScript, змінюючи DOM. При чистому CSR у перші миті після відкриття сторінки в DOM немає нічого корисного, і «сирий» HTTP-запит без виконання JavaScript побачить порожнечу.

    SSR (server-side rendering) — рендеринг на сервері: клієнт отримує HTML, а не JavaScript, який його побудує. Сервер генерує повний HTML у відповідь на навігацію, тож клієнту не потрібні додаткові звернення за даними, і перший показ зазвичай швидший — до клієнта їде менше JavaScript. Окремий варіант — статичний рендеринг: HTML будується під час збірки.

    Гідратація (hydration) — запуск клієнтських скриптів, які додають стан і інтерактивність до вже відрендереного сервером HTML (вона припускає, що DOM при цьому не змінюється). Саме тут живе найпідступніший для тестів стан, і джерело каже про нього прямо: сторінка виглядає інтерактивною, але не реагує на введення, доки не виконаються клієнтські скрипти й не навісяться обробники подій. Ціна теж названа: SSR із регідратацією покращує перший показ, але може суттєво погіршити відгук на взаємодії.

    Звідси відповідь на питання «чи є універсальний сигнал, що сторінка готова». Немає. DOMContentLoaded настає, коли розібрано HTML, — у CSR у цей момент даних ще немає. load чекає на підресурси, але AJAX-запити за даними йдуть після нього, а при клієнтській навігації він не повторюється. document.readyState === "complete" несе ті самі обмеження, що й load. Готовність — поняття конкретного застосунку, а не властивість браузера.

    Наша практика (не канон). Стандартного DOM-атрибута «гідратацію завершено» не існує: надійний сигнал залежить від фреймворка і його версії, тож маркер готовності доводиться зʼясовувати або домовляти для свого стека. І саме тому падіння на гідратації плаваюче: на швидкій машині гідратація встигає до кліку, на завантаженому CI — ні. З того самого досвіду й перелік причин, чому оманливе «мережа затихла»: (polling), вебсокет і аналітика не дають їй затихнути ніколи — джерело таких причин не називає, воно лише позначає це очікування як небажане.

    Конвеєр рендерингу: коли елемент справді готовий

    «Є на сторінці» — не одна подія, а кілька різних. Конвеєр рендерингу (rendering pipeline) перетворює HTML, CSS і JavaScript на пікселі фіксованою послідовністю етапів: DOM і CSSOM → дерево рендерингу → layout (геометрія) → paint (пікселі) → composite (складання шарів). Повний перелік етапів конвеєра починається ще раніше — з JavaScript, який ініціює візуальну зміну, і обчислення стилів. Проганяє всі етапи не кожна зміна: зміна «тільки-paint»-властивості пропускає layout, а зміна, якій не потрібні ні layout, ні paint, іде одразу в композитинг.

    Найкорисніша для AQA деталь — асиметрія видимості. Елемент із display: none у дереві рендерингу відсутній: він є в DOM, але геометрії не має. Елемент із visibility: hidden у дереві присутній — займає місце, просто не видно. Тому наявність вузла в DOM — найраніша й найслабша гарантія: перевірка на кшталт «елемент прикріплений до дерева» спрацює набагато раніше, ніж елемент реально стане клікабельним.

    Ні

    Так

    Ні

    Так

    Ні

    Так

    Локатор не знаходить
    або клік не спрацьовує

    Вузол є в DOM?

    Дані ще не приїхали
    або елемент лінивий

    Має геометрію?

    display none
    або нульовий розмір

    Стабільний і не перекритий?

    Анімація, оверлей, тост

    Обробники ще не навішені
    гідратація не завершилась

    Ні

    Так

    Ні

    Так

    Ні

    Так

    Локатор не знаходить
    або клік не спрацьовує

    Вузол є в DOM?

    Дані ще не приїхали
    або елемент лінивий

    Має геометрію?

    display none
    або нульовий розмір

    Стабільний і не перекритий?

    Анімація, оверлей, тост

    Обробники ще не навішені
    гідратація не завершилась

    Ще одна причина «елемент є, а тест мигає» — головний потік. Будь-яка зміна дерева рендерингу запускає layout повторно, а важкий синхронний перерахунок займає потік: елементи вже намальовані, але на кліки не реагують. Анімований елемент — окремий випадок тієї самої природи: інструмент вимагає стабільності перед дією. І оскільки результат layout залежить від розміру екрана, той самий локатор на «десктопній» і «мобільній» може вести до різних елементів.

    DOM як результат, а не джерело

    DOM — програмний інтерфейс до документа: сторінка подана деревом вузлів і обʼєктів, щоб код міг змінювати її структуру, стилі та вміст. Браузер будує це дерево, розібравши HTML, і далі показує сторінку вже з нього; поведінку дерева й подій у ньому нормує DOM Living Standard.

    Ключове слово — «далі». Як обʼєктне представлення сторінки DOM змінюється скриптами вже після завантаження: JavaScript додає, видаляє й переписує вузли у відповідь на дії користувача, відповіді сервера й таймери. Навіть колекції дітей вузла живі — їхній вміст змінюється щоразу, коли дітей додають чи прибирають.

    Звідси головна плутанина теми. Вихідний HTML — це текст відповіді сервера; DOM — те, що лишилося після парсингу й виконання всього JavaScript. Інструменти, які читають лише перше (простий HTTP-клієнт, curl, парсер без виконання JS), вмісту SPA не побачать: для UI-перевірок потрібен справжній браузер, а якщо потрібні лише дані — часто дешевше піти напряму в API.

    Для локаторів це означає дві речі. Перша: локатор шукає елемент не у вихідному HTML, а в поточному стані дерева, тож він має бути рецептом пошуку, який розвʼязується в момент дії, а не збереженим наперед посиланням на вузол — саме через збережені посилання виникає класична проблема «елемент застарів» після ререндеру. Друга: перш ніж правити селектор на «not found», зʼясуй, чи елемент узагалі є в DOM на момент пошуку і чи не живе він в іншому контексті — в iframe або Shadow DOM. Правильна відповідь на динамічність — не sleep(3), а очікування конкретної умови.

    У Selenium ця помилка має власне імʼя — StaleElementReferenceException, і знати його треба, щоб упізнати її в лозі. Дока формулює причину одним реченням: «Elements do not get relocated automatically» — драйвер видає елементу ідентифікатор посилання й очікує знайти його в конкретному місці DOM. Тому виняток означає не «елемента немає на сторінці», а «моє посилання на нього більше не дійсне», і лікування залежить від причини. Змінився DOM — елемент треба перезнайти. Змінився контекст (перемкнулися у фрейм або в інше вікно) — посилання «will still be valid, but will be temporarily inaccessible», і тут «it won't help to relocate the element»: треба повернутися в потрібний контекст. Після навігації контекст знищено, тож потрібні обидві дії. І окремо про звичний рефлекс: обгортку з кешем і повтором по винятку дока подає не як рішення, а як — вона «can cause problems if the locator you're using references a different element (and not the one you want) after the page has changed»; перезнаходити щоразу надійніше, але «not the most efficient approach, especially when running on a remote grid».

    Подієвий цикл: чому перевірка ловить проміжний стан

    (event loop) — механізм, яким браузер узгоджує події, дії користувача, скрипти, рендеринг і мережу. Канон теми — глава Виконання JavaScript та event loop; тут потрібні лише ті чотири наслідки, з яких народжується проміжний стан.

    Перший: кожна задача обробляється повністю, перш ніж почнеться будь-яка інша, а колбек асинхронної дії не виконується миттєво — він стає задачею в черзі. Тому асинхронний колбек ніколи не перерве синхронний код, що вже виконується.

    Другий: черга мікрозадач — окрема сутність, а не одна з черг задач, і після кожної задачі вона спорожнюється повністю. Реакції промісів — мікрозадачі; setTimeout, події DOM і події XHR — задачі. Завершення мережевого запиту браузер оформлює як задачу, яка резолвить проміс, але .then на ньому виконається вже мікрозадачею — на відміну від старого XHR.onload, який лишається звичайною задачею.

    Третій: рендеринг відбувається не після кожної задачі, а в «нагоду для рендерингу». середовище не витісняє, тож вона затримує і обробку подій, і кадр рендерингу — сторінка на екрані є, а на кліки не реагує.

    Четвертий: таймери нічого не гарантують — специфікація каже це прямо: «This API does not guarantee that timers will run exactly on schedule». Тому setTimeout(fn, 0) означає «якнайшвидше після того, як звільниться потік», а задана — мінімум, а не обіцянка. Практичний висновок: «елемент є в DOM, але тест падає» часто означає, що вміст оновлює мікрозадача чи мережевий колбек, який ще не відпрацював.

    Гонка станів у завантаженні даних

    Гонка (race condition) — це залежність результату від того, у якому порядку завершилися асинхронні операції, за відсутності гарантій щодо цього порядку. Оскільки кожна задача виконується повністю, гонка виникає не всередині однієї задачі, а між задачами: два запити за даними стартували, а хто відповість першим — питання мережі, а не коду.

    Виправлення на боці застосунку відоме: відстежувати актуальність запиту — скасовувати попередній через AbortController або ігнорувати відповіді, чий запит уже застарів.

    Виправлення на боці тесту важливіше, бо тест, який не синхронізується зі станом, сам створює гонку — дока Selenium прямо називає таку гонку між браузером і кодом тесту однією з головних причин . її не усуває, а маскує: пауза не перевіряє стан, вона лише сподівається, що за цей час усе встигне, — а джерело показує, що програє вона двічі (замало — тест падає, забагато — прогін стає непідйомним) і що механізми синхронізації кращі. Правильна альтернатива — очікування за умовою: чекати рівно доти, доки настане потрібний стан, а таймаут тримати як стелю терпіння, а не як фіксований простій.

    Наша практика (не канон). Класичний приклад гонки — живий пошук: користувач набирає ab, потім abc, летять два запити, і якщо відповідь на ab прийде пізніше, вона перезапише правильні результати для abc. Суть дефекту в одному рядку: перемагає «останній, що відповів», хоча має перемагати «останній, що надіслали». За нашим досвідом гонка — ще й найчастіша першопричина в UI-тестах; канонічне джерело обережніше — воно каже «одна з головних причин», а сам приклад із живим пошуком опори в реєстрі не має.

    Селектори й локатори при динамічній розмітці

    Канон селекторів — глава про DOM, селектори та події; тут лише той зріз, який ламається саме через асинхронність. Селектор — шаблон, за яким добирають елементи; локатор в автотесті будується поверх тієї самої механіки й описує, як знайти елемент у поточному стані дерева.

    Динамічна розмітка бʼє по локаторах двома способами. Перший — згенеровані імена: й CSS-модулі дають класи на кшталт Button_root__a1b2c, які змінюються з кожним ; рятує часткове зіставлення за атрибутом — ^= («починається з»), $= («закінчується на»), *= («містить підрядок»). Другий — позиційність: :nth-child добирає за місцем у дереві, а коли рядок довантажився вище, селектор указує вже на інший елемент.

    Тому найстабільніший спосіб знайти елемент — той, яким його знаходить користувач і читач екрана: за роллю та видимим текстом. Роль відповідає семантиці елемента, а доступне імʼя формується з видимого тексту, aria-label чи повʼязаного <label> — жодне з цього не залежить від верстки . Компроміс — data-testid: явний контракт між тестом і кодом, який не чіпають при зміні стилів.

    // крихко: клас переживе рівно один білд
    await page.locator('.Button_root__a1b2c').click();
    
    // стабільно: роль і доступне імʼя не залежать від верстки
    await page.getByRole('button', { name: 'Купити' }).click();

    Наша практика (не канон). Робоча ієрархія вибору — роль плюс доступне імʼя → текст або лейбл → data-testid → CSS за стабільним атрибутом → XPath. Саме таке ранжування наше: джерела дають перелік рекомендованих локаторів без порядку переваги. Сюди ж — окрема пастка асинхронності: скелетон (сірий плейсхолдер у формі майбутнього контенту) теж є елементом DOM, і локатор «за формою блоку» чіпляється саме за нього. Тому перевірку прив'язують до того, чого в плейсхолдері бути не може: конкретного тексту, ціни, імені. А «індикатор зник» тримають у парі з «контент зʼявився» — між цими двома подіями буває проміжок, і перевірка лише першої з них ненадійна.

    Очікування й синхронізація як відповідь на все перелічене

    Усе попереднє зводиться до одного: тест і застосунок живуть у різному часі, а синхронізація — це вміння діяти рівно тоді, коли застосунок готовий. Механічно вона стоїть на двох речах.

    Перша — перевірки придатності до дії (actionability), які інструмент робить сам. Перед кліком Playwright чекає, поки локатор укаже рівно на один елемент, а сам елемент буде видимим, стабільним, увімкненим і отримуватиме події; для введення додається перевірка «редагований». Означення варті запамʼятовування: видимий — має непорожній і не має visibility: hidden (нульовий розмір і display: none невидимі, а opacity: 0 вважається видимим); стабільний — зберігає той самий bounding box щонайменше два послідовні кадри анімації; отримує події — є ціллю вказівникової події в точці дії, тобто перевірка ловить оверлей, модалку чи тост, що перехоплює клік. Не дочекався в межах таймауту — TimeoutError. Опція force вимикає необовʼязкові перевірки — зокрема перестає контролювати, чи справді елемент отримує клік; здебільшого це спосіб зробити тест зеленим, приховавши дефект.

    Друга — явні очікування прикладних сигналів, про які інструмент знати не може: чи навішений обробник після гідратації, чи завершився AJAX, чи оновилися дані. Web-first перевірки самі повторюють спробу, доки умова не виконається, — саме тому ручні паузи не потрібні. А коли зміна в інтерфейсі надто тонка, синхронізуються на мережевій відповіді, і тут є деталь, яку часто пропускають: проміс очікування створюють до дії, що тригерить запит, інакше швидка відповідь прослизне у вікно між дією і викликом очікування — і це знову гонка.

    // проміс створено ДО кліку, інакше швидка відповідь прослизне
    const response = page.waitForResponse(
      (r) => r.url().includes('/api/profile') && r.status() === 200,
    );
    await page.getByRole('link', { name: 'Профіль' }).click();
    await response;
    
    // і однаково перевіряємо кінцевий, змістовний стан
    await expect(page.getByRole('heading', { name: 'Профіль' })).toBeVisible();

    Жорстке очікування програє двічі: замало — тест флакне на повільному CI, забагато — прогін марно розтягується; головна вада в тому, що воно синхронізується з годинником, а не із застосунком. Наша практика (не канон). Єдиний випадок, де до паузи тягнуться закономірно, — перевірка відсутності зміни: чекати нема на що. Але джерело тут не арбітр: дока Selenium такого винятку не формулює й не забороняє його — про виправданість чи заборону короткої паузи в ній не сказано нічого. Тому й ми не подаємо це правилом: чесніший хід — знайти спостережуваний сигнал, після якого відсутність зміни вже щось означає (прихід тієї самої застарілої відповіді, завершення запиту, зміна лічильника), і перевіряти стан після нього. Не варто змішувати й моделі очікувань: неявне очікування Selenium реагує тільки на «елемент не знайдено» і в суміші з явним дає непередбачувані сумарні таймаути. Окремо глушать анімації — інакше вони ламають і клік (елемент їде), і візуальне порівняння.

    Наша практика (не канон). Найнадійніший сигнал готовності — той, який застосунок виставляє сам: домовлений із командою атрибут на кшталт data-hydrated чи aria-busy="false". Це річ конкретного застосунку, а не стандарт, тож канонічного джерела під цю практику немає — але вона знімає рівно ту невизначеність, якої не закриває .

    Тротлінг мережі як спосіб побачити проміжні стани навмисно

    Проміжні стани, з яких складається ця глава, на швидкому Wi-Fi просто не встигають проявитися. (throttling) робить їх видимими: меню Throttling у панелі мережі емулює повільні зʼєднання за готовими пресетами.

    Дві деталі, які економлять час. Перша: набір пресетів змінювався між версіями Chrome — у поточних це, зокрема, Fast 4G, Slow 4G і 3G, а раніше були Fast 3G / Slow 3G. Тому для відтворюваних умов надійніше задавати власний профіль із фіксованими значеннями завантаження, віддачі й затримки, ніж покладатися на назву пресета. Друга: поруч живе режим Offline і Disable cache — дешева перевірка деградації, чи повідомляє застосунок про втрату мережі.

    Головне ж — як читати результат. Якщо тест падає на повільному профілі, він не «флакне на швидкому» — він просто ще не впав: очікування в ньому недостатньо прив'язані до подій. Сам тротлінг стабільності не додає — це інструмент відтворення й діагностики.

    Наша практика (не канон). Тротлінг оголює стани завантаження: чи є спінер і скелетон, чи не «моргає» порожній екран, чи лишається кнопка активною, поки запит іще летить (класика подвійного надсилання). Це чесний спосіб знайти гонку до того, як її знайде CI, — тож перед тим як списати падіння на «повільний ранер», відтвори його локально на повільному профілі. Канонічного джерела під ці прийоми в реєстрі немає, це наш робочий досвід.

    Типові помилки

    • «Сторінка відкрилась — можна перевіряти». Насправді load гарантує документ і підресурси, а дані приїжджають окремим запитом після нього; у SPA при клієнтському переході він узагалі не повторюється.
    • «fetch не кинув помилки — значить, усе гаразд». Насправді 404 і 500 дають успішний проміс: без перевірки response.ok помилка сервера проходить мовчки, і в інтерфейсі просто нічого не оновлюється.
    • «Кнопку видно — можна клікати». Насправді на SSR-сторінці розмітка приходить раніше за обробники: до кінця гідратації клік провалюється в порожнечу, і саме тому падіння плаваюче.
    • «element not found — треба правити локатор». Насправді елемента може фізично не бути в DOM: він лінивий, віртуалізований або дані ще не приїхали. Правити треба стратегію очікування, а не селектор.
    • «Мережа затихла — сторінка готова». Насправді полінг, вебсокет чи аналітика не дають їй затихнути ніколи, а затихнути вона може й тоді, коли дані ще обробляються в JS; документація Playwright прямо позначає це очікування як DISCOURAGED.
    • «Тест зелений локально — значить, стабільний». Насправді на повільному профілі він падає, і це не властивість CI, а властивість тесту: очікування прив'язані до годинника, а не до подій.

    Підсумок

    • «Показано» і «готово» — різні події. Між ними лежать окремий запит за даними, побудова DOM, layout, paint і навішування обробників; тест має чекати на потрібну стадію, а не на «сторінку».
    • DOM — результат, а не джерело. Локатор шукає в поточному стані дерева, і воно змінюється скриптами весь час; те, що видно у відповіді сервера, і те, що бачить браузер, збігається лише в найпростіших випадках.
    • Універсального сигналу готовності немає. DOMContentLoaded, load, readyState === "complete" і «мережа затихла» кожен по-своєму недостатні; готовність — поняття конкретного застосунку.
    • Гонка виникає між задачами, а не всередині них. Порядок завершення асинхронних операцій не гарантований, тож фіксована пауза її не усуває, а маскує.
    • Надійне очікування прив'язане до умови. Автоочікування закриває «елемент зʼявився і став клікабельним», решту — гідратацію, приїзд даних, оновлення стану — доводиться виражати явно.

    Можливі питання

    • «Чим SPA відрізняється від MPA і що це змінює для тестування?» Чекають не список властивостей, а наслідок: у SPA документ один, тому чіткого моменту «сторінка завантажилась» при переході немає, а стан не скидається сам — звідси вимоги до ізоляції.
    • «Що таке гідратація і чому клік по видимій кнопці може нічого не зробити?» Перевіряють, чи розділяєш розмітку й інтерактивність. Сильна відповідь називає вікно між появою HTML і навішуванням обробників і згадує, що стандартного сигналу «гідратацію завершено» немає.
    • «Як поводиться fetch при відповіді 404?» Питання-детектор: слабка відповідь каже «кине помилку». Правильна — проміс резолвиться, статус перевіряють вручну, а реджект буває, коли відповіді не було зовсім.
    • «Чому sleep у тесті — антипатерн і чим його замінити?» Дивляться, чи бачиш причину, а не ритуал: пауза синхронізується з годинником, а не із застосунком. Плюсом — очікування конкретної відповіді, створене до дії, і перевірка кінцевого змістовного стану.
    • «Тест падає лише на CI. З чого почнеш?» Часта форма на мідл-рівні. Сильна відповідь пропонує відтворити умови (повільний профіль мережі), а не збільшувати таймаут, і називає підозрюваних: гонка запитів, гідратація, лінивий контент.

    Джерела

    Запит без перезавантаження: AJAX і fetch

    • MDN — Using the Fetch API — Fetch як заміна XHR, успішний проміс на 404/500, response.ok для 2xx, відкритий перелік причин реджекту, одноразовість потоку тіла, скасування через AbortController.
    • MDN Glossary — SPA (Single-page application) — оновлення частини документа замість повного перезавантаження.
    • MDN — XMLHttpRequest — старий механізм фонового запиту і скасування через abort().
    • MDN — XMLHttpRequest: readyState property — пʼять станів, HEADERS_RECEIVED після всіх редиректів, часткове тіло в LOADING, двозначність DONE.
    • Playwright — Network — очікування конкретної відповіді за URL і статусом замість сліпого очікування.
    • Chrome DevTools — Network features reference — фонові запити в тому самому журналі мережі.
    • Playwright — Auto-waiting (actionability) — перевірки придатності перед дією замість припущення, що потрібний вміст уже на місці.

    SPA і MPA: що переміщується, а що ні

    • MDN Glossary — SPA (Single-page application) — один документ, оновлення вмісту через JavaScript, переходи без перезавантаження.
    • MDN — History APIpushState/replaceState без запиту на сервер і те, що вони не генерують popstate.
    • MDN — Window: load event — момент спрацювання load: документ разом із підресурсами.
    • Playwright — class Page — навігаційні сигнали й очікування контенту після переходу.
    • Playwright — Authentication — свіжий контекст браузера як спосіб не тягнути стан між тестами.
    • MDN — Web Storage API — сховище, яке переживає кроки тесту, якщо його не скинути.
    • web.dev — Rendering on the web — ціна клієнтського рендерингу: обсяг JavaScript і чутливість сторінки.
    • Playwright — Auto-waiting (actionability) — очікування придатності елемента до дії замість сигналу «сторінка завантажилась».

    CSR, SSR і гідратація: розмітка є, а обробники ще ні

    • web.dev — Rendering on the web — означення CSR, SSR, статичного рендерингу й гідратації; «виглядає інтерактивною, але не реагує на введення»; порожній DOM при чистому CSR і ціна регідратації.
    • MDN Glossary — SPA (Single-page application) — чому вміст такої сторінки перевіряють лише в браузері, що виконує JavaScript.
    • MDN — Document: DOMContentLoaded event — що саме гарантує подія: розібраний HTML, не дані.
    • MDN — Window: load event — межа гарантії load.
    • Playwright — class Page — стани завантаження сторінки як сигнали й networkidle з позначкою DISCOURAGED.

    Конвеєр рендерингу: коли елемент справді готовий

    DOM як результат, а не джерело

    Подієвий цикл: чому перевірка ловить проміжний стан

    Гонка станів у завантаженні даних

    Селектори й локатори при динамічній розмітці

    Очікування й синхронізація як відповідь на все перелічене

    Тротлінг мережі як спосіб побачити проміжні стани навмисно

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.