07 · Інструменти автоматизації
Playwright: мережа — перехоплення, мокання, очікування відповідей
Зміст
Поки тест лише клікає, він залежить від усього, що стоїть за кнопкою: від бекенда, від сторонньої аналітики, від платіжного , від того, чи є сьогодні в базі замовлення в статусі «скасовано». Частина потрібних сценаріїв на живому оточенні або недосяжна (як віддати 500 з платіжного провайдера?), або нестабільна. Інша частина падає не тому, що застосунок зламався, а тому, що відповідь прийшла на 300 мс пізніше, ніж тест устиг подивитися.
Мережа — та точка, де AQA перестає бути «клікером» і починає керувати умовами експерименту. Playwright дає API для спостереження й зміни мережевого трафіку браузера: під контроль потрапляють усі запити сторінки, зокрема XHR і fetch. Тому головне в цій темі — не рецепт page.route, а усвідомлена ціна такого вклинювання. Механіка браузерного перехоплення як явища розібрана в главі Перехоплення й мокання мережі; тут — те, що з цієї механіки робить конкретний інструмент.
Де тест вклинюється в мережу
(network interception) — це вклинювання тесту між браузером і мережею: замість покладатися на реальний сервер, тест сам вирішує, пропустити запит, підмінити відповідь чи обірвати зʼєднання. Модель однакова в усіх інструментах такого класу — від тестового до дебаг-: реєструється правило «для запитів за цим шаблоном URL викликай цей обробник», а обробник ухвалює рішення.
Окремої інфраструктури для цього не потрібно: достатньо оголосити маршрут (route) для (browser context) через browserContext.route() або звузити його до однієї сторінки через page.route(). Маршрут, заданий на рівні контексту, діє й на попапи та відкриті з них сторінки, а не лише на початкову вкладку — саме тому глушіння аналітики чи зручно вішати на контекст, а не дублювати в кожному тесті.
Перехоплення вибіркове: запити, що не підійшли під жоден шаблон, спокійно йдуть на реальний сервер. Це важлива інтуїція — маршрут не «вимикає мережу», він накриває лише те, що ви описали.
Коли правил кілька, порядок не випадковий, і його варто знати дослівно. Маршрути сторінки мають пріоритет над маршрутами контексту. Серед кількох маршрутів, що збіглися з тим самим шаблоном, спрацьовує найпізніше зареєстрований: у доці це сформульовано як «they run in the order opposite to their registration», щоб останній зареєстрований завжди міг перекрити попередні. Практичний наслідок прямий: загальний кладуть у налаштування або в хук, а вужче правило реєструють пізніше — у самому тесті, — і воно переможе.
Передати керування наступному обробнику в ланцюгу можна лише явно: route.fallback() викликає інші збіги перед відправленням запиту, тоді як route.continue() ланцюг зупиняє — «other matching handlers won't be invoked». Прибирають маршрут разом з обробником окремим викликом page.unroute().
Три рішення обробника: fulfill, continue, abort
Коли маршрут спрацював, обробник отримує обʼєкт Route і має рівно три варіанти дій. Це і є весь словник теми — решта деталей нашаровується на ці три рішення.
fulfill — підмінити. Сервер не викликається взагалі; браузер отримує рукотворну відповідь. Дока формулює це без двозначності: «No requests to the API will be made».
await page.route('**/api/v1/fruits', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 21, name: 'Strawberry' }]),
});
});
Окремий, часто недооцінений режим — виконати справжній запит і пропатчити відповідь. Дока розводить ці два випадки прямо: іноді запит зробити необхідно, але відповідь треба підправити для відтворюваності. Тоді беруть справжню відповідь через APIRequestContext і віддають її в route.fulfill(), перевизначаючи лише потрібні поля:
await page.route('**/api/v1/fruits', async (route) => {
const real = await page.request.fetch(route.request());
const body = await real.json();
body.push({ id: 100, name: 'Loquat' });
await route.fulfill({ response: real, body: JSON.stringify(body) });
});
Різниця не косметична: у першому режимі ви тестуєте свої уявлення про бекенд, у другому контакт із ним зберігається, а детермінованим робиться лише той фрагмент даних, який заважає.
continue — пропустити далі, дорогою змінивши URL, метод, заголовки або тіло. Запит іде на реальний сервер.
abort — обірвати. Це спосіб змоделювати недоступність сервісу. Код помилки тут — не довільний рядок, а значення із закритого набору, повністю переліченого в довіднику класу Route; за замовчуванням failed. Коди не синоніми, і саме тут ховається цінність для тест-дизайну: connectionrefused — «A connection attempt was refused» (сервіс не слухає), namenotresolved — «The host name could not be resolved» (DNS не резолвиться), internetdisconnected — «The Internet connection has been lost» (мережа зникла), timedout — «An operation timed out» (операція не вклалася в час), connectionreset — обрив, що відповідає TCP RST, connectionclosed — TCP FIN. Це шість різних негативних сценаріїв, а не один «інтернет відвалився».
// сервіс рекомендацій лежить — сторінка мусить лишитися робочою
await page.route('**/api/v1/recommendations', (route) =>
route.abort('connectionrefused')
);
Заголовки: що підміняється, а що мовчки ні
Підміна заголовків — часта практична потреба: підкинути фіча-флаг, підмінити токен, прибрати заголовок. Робиться вона через continue з опціями:
await page.route('**/*', async (route) => {
const headers = { ...route.request().headers(), 'x-feature-flag': 'new-cart' };
await route.continue({ headers });
});
Тут же живуть дві пастки, які довідник називає прямо, і обидві дають хибно зелений тест.
Перша — редиректи. «The headers option applies to both the routed request and any redirects it initiates. However, url, method, and postData only apply to the original request and are not carried over to redirected requests.» Тобто заголовки переживають ланцюжок редиректів, а підмінені URL, метод і тіло — ні: після першого 3xx поїде оригінал.
Друга — заборонені заголовки. Частину заголовків (Cookie, Host, Content-Length та інші) підмінити не можна: «If an override is provided for a forbidden header, it will be ignored and the original request header will be used». Жодної помилки при цьому не буде. Тест, який «підмінив» Cookie, щоб перевірити поведінку іншого користувача, насправді перевірив поведінку того самого — і показав зелене.
Якщо один і той самий заголовок потрібен усім запитам, дешевше не перехоплювати нічого, а задати його конфігурацією: request успадковує з конфігу baseURL, extraHTTPHeaders і налаштування проксі. Конфігурація й фікстури — тема глави Playwright: фікстури, хуки і конфігурація.
Мокати чи не мокати: ціна детермінізму
Мережевий (stub) — це інструментальна реалізація (test double): компонента, який замінює реальну залежність «специфічним для тесту еквівалентом» і зобовʼязаний повторити її API, а не поведінку. У класичній таксономії стаб — це точка керування: він заганяє систему в гілку, у яку інакше не потрапити. Мок (mock) — інше: він перевіряє непрямі виходи під час взаємодії, і це не «стаб плюс асерти», а принципово інший спосіб уживання. Повна таксономія дублерів — тема глави Тестові дублери: stub, mock, fake, spy; варто знати, що в силабусі ISTQB CTFL її немає взагалі: стаби, драйвери, симулятори й названі там елементами тестового оточення (§1.4.3), а не видами дублерів.
Що дає мокання в e2e:
- детермінізм — відповідь однакова на кожному прогоні;
- доступ до — порожній список,
500, протермінований токен, платіж із ; - швидкість і незалежність — тест не чекає на повільний сервіс і не залежить від готовності бекенда.
Що воно забирає — і це головна частина відповіді на співбесіді. Замокавши все, ви тестуєте свої припущення про бекенд, а не бекенд. Дублер — знімок чужої поведінки на момент написання, і страховкою від його старіння є звірка дублера зі справжнім застосунком: це поширений спосіб реалізувати контрактні тести — так їх робить Pact, перевіряючи, що всі виклики до тестових дублерів повертають те саме, що повернув би реальний застосунок. Без такої звірки зʼявляється (schema drift) — усталеної галузевої назви явище не має, сама назва тут наша, а механізм простий: бекенд перейменував поле, продакшн зламався, а зелена.
Ще жорсткіше це сформульовано в першоджерелі таксономії: має існувати хоча б один тест без дублерів, бо з дублером ми перевіряємо іншу конфігурацію, ніж піде в продакшн. Там же названо й другий бік — надмірне вживання дублерів робить тести крихкими через переспецифікацію.
Робочий орієнтир: мокати зовнішнє й некероване (платіжні шлюзи, аналітику, сторонні віджети), не мокати без потреби власний основний API в і мокати на найтоншому потрібному рівні. Мокання на рівні сервісів (WireMock, msw, mock-сервери) і — предмет розділу про API-тестування.
Очікувати відповідь, а не годинник
Перехоплення потрібне не лише щоб підміняти. Часто тесту треба просто знати, що обмін відбувся: що після кліку пішов POST, що відповідь має статус 201, що аналітика надіслала подію. Для пасивного спостереження без втручання підписуються на події request і response. Щоб синхронізуватися з конкретною відповіддю після дії, беруть page.waitForResponse() замість фіксованої паузи — URL при цьому збігається за спрощеним glob-патерном, тим самим, що й у page.route().
Тут є один синтаксичний нюанс, на якому спотикаються часто: очікування створюють до дії, і без await. Дока підписує це прямо в прикладі — «Start waiting for response before clicking. Note no await». Якщо поставити await одразу, тест зупиниться до кліку й дочекається , бо запит ще не почався.
const responsePromise = page.waitForResponse('**/api/v1/orders');
await page.getByRole('button', { name: 'Оформити' }).click();
const response = await responsePromise;
expect(response.status()).toBe(201);
Той самий порядок стосується й маршрутів: правило спрацьовує лише для запитів, зроблених після його реєстрації, тож маршрут ставлять до дії, яка тригерить запит, — інакше тест перехопить порожнечу.
І застереження, яке рятує від цілого класу (flakiness): очікування мережі — це не спосіб дочекатися інтерфейсу. Відповідь 200 ще не означає, що список перемалювався. networkidle як сигнал готовності дока прямо позначає як DISCOURAGED і радить замість нього вебасерти. Правильна пара — дочекатися мережі там, де потрібен саме факт запиту, і покластися на та web-first перевірки там, де потрібен стан UI: Playwright: перевірки і автоочікування.
HAR: записати сесію і відтворити її
HAR (HTTP Archive) — JSON-формат експорту зафіксованої мережевої сесії, який читають різні інструменти: DevTools вміють вивантажити HAR і завантажити його назад для аналізу. Записаний HAR можна використати і як джерело підмінених відповідей — це режим запису-відтворення: дані реалістичні, бо колись справді прийшли з сервера, але тест від сервера вже не залежить.
Два рішення тут треба ухвалити свідомо. Перше — безпека. Типовий експорт HAR із DevTools «санітизований»: із нього прибрано Cookie, Set-Cookie й Authorization, щоб не злити секрети в баг-репорт. Повний запис, навпаки, містить усе — токени, кукі, тіла відповідей із персональними даними. Тому .har, який їде в репозиторій, треба чистити руками, а не сподіватися на дефолт того інструмента, яким його записали.
Друге — старіння. HAR-записи протухають так само, як будь-який інший дублер, і лишають тест зеленим на даних, яких бекенд уже не віддає. HAR добре працює там, де відповідь громіздка й стабільна (довідники, каталоги), і погано там, де контракт активно змінюють.
APIRequestContext: підготувати стан, а не клікати
Створювати дані через інтерфейс — зазвичай найдорожчий спосіб дійти до потрібного стану: пʼять екранів реєстрації заради перевірки одного статусу замовлення. Підготовка даних і середовища — не імпровізація перед прогоном, а планована активність : вимоги до визначають на етапі дизайну, а створення потрібних фікстур — окрема діяльність з реалізації тестів.
Playwright дає для цього APIRequestContext, і дока сама перелічує три його застосування: тестувати серверне API, готувати серверний стан до відкриття застосунку і перевіряти серверні пост-умови після дій у браузері. Тобто «засій через API» — офіційно підтримуваний сценарій, а не обхідний шлях.
test('нове замовлення видно у списку', async ({ page, request }) => {
const created = await request.post('/api/v1/orders', {
data: { sku: 'A-100', qty: 2 },
});
expect(created.ok()).toBeTruthy();
const order = await created.json();
await page.goto(`/orders/${order.id}`);
await expect(page.getByTestId('order-status')).toHaveText('Нове');
});
Далі — чотири деталі, кожна з яких легко дає хибно зелений тест.
Який саме контекст ви взяли. page.request — це скорочення для page.context().request, той самий екземпляр; він користується тим самим , що й браузерний контекст, і оновлює його з Set-Cookie. Ізольований контекст із власним сховищем кукі створюється лише через apiRequest.newContext(). Для негативних перевірок доступів це критично: якщо ходити через page.request, авторизація «протікає» з браузера, і перевірка «чужий користувач не бачить замовлення» може виявитися хибно зеленою.
. За замовчуванням обʼєкт відповіді повертається для будь-якого коду — клієнт не кидає ні на 404, ні на 500. Кидання вмикає failOnStatusCode, і межа при цьому — 2xx і 3xx: з увімкненим прапорцем 301 помилкою теж не вважається.
Редиректи. Вони йдуть автоматично, за замовчуванням до 20. Якщо перевіряти треба сам 3xx, редиректи потрібно вимкнути, інакше тест мовчки пройде весь ланцюжок і побачить 200.
Таймаут. У запитів є дефолтний таймаут, який не знімається передачею AbortSignal; змінюють його setDefaultTimeout(), а повністю вимикають лише значенням 0. Тобто таймаут діє завжди — навіть якщо тест про нього не згадував.
І бонус, який змикає цю главу з наступною: стан сховища взаємозамінний між браузерним контекстом і API-контекстом, тож логін можна зробити , а перевіряти — в UI. Механіка — глава Playwright: автентифікація і повторне використання стану. Тестування API як окрема дисципліна (перевірки тіла, схеми, контракти) належить розділу про API-тестування; тут API — лише засіб підготувати стан.
Типові помилки
Виглядає як «мок не працює», а насправді маршрут зареєстровано після дії. Правило накриває лише запити, зроблені після його реєстрації. Якщо page.route() стоїть після page.goto(), перехоплювати вже нічого.
Виглядає як «перевірив поведінку іншого користувача», а насправді підміна Cookie проігнорована. Заборонені заголовки не підміняються, і помилки при цьому не буде — піде оригінальне значення.
Виглядає як «підмінив URL запиту», а насправді підміна не дожила до редиректу. Заголовки переживають редиректи, url, method і postData — ні.
Виглядає як очікування, а насправді гонка через await. Проміс waitForResponse створюють до дії й без await; інакше тест починає чекати на запит, який ще не пішов.
Виглядає як баг , а насправді перехоплення його вимкнуло. Увімкнення маршрутизації вимикає HTTP-кеш («Enabling routing disables http cache»). Тест, який перевіряє 304 або поведінку Cache-Control, під активним маршрутом перевіряє інший застосунок — деталі механіки в главі Кешування.
Виглядає як «мережеві події зникли», а насправді їх забрав Service Worker. сторінки (зокрема власний воркер інструментів мокання на кшталт MSW) перебирає запити на себе й робить їх невидимими для browserContext.route() і page.route(); дока радить у такому разі вимкнути воркери налаштуванням serviceWorkers: 'block'.
Виглядає як зелена сюїта, а насправді дрейф схеми. Замоканий власний API робить тести незалежними від бекенда — зокрема від того, що бекенд змінив контракт. Лікує це не мокання, а звірка дублера з реальним застосунком.
Виглядає як зручний фікстур-файл, а насправді злитий токен. Повний HAR містить Cookie й Authorization; санітизований експорт DevTools — ні. Перед .har треба чистити.
Виглядає як «залишковий маршрут нікому не шкодить», а насправді він тече в наступний тест. Маршрут живе стільки, скільки живе сторінка чи контекст, на якому його зареєстрували: там, де контекст переживає окремий тест (спільна сторінка на кілька тестів, воркер-), не знятий маршрут перехопить і запити наступних — і дасть плаваючі падіння. Прибирає його page.unroute(), і це належить до ізоляції, а не до косметики.
Підсумок
- Обробник маршруту має рівно три рішення — fulfill (сервер не викликається), continue (далі зі змінами) і abort (мережева помилка); поверх них живе змішаний режим — виконати справжній запит і пропатчити відповідь.
- Перехоплення вибіркове й упорядковане: незбіглі запити йдуть у мережу, маршрути сторінки перекривають маршрути контексту, а серед збігів перемагає найпізніше зареєстрований.
- Підміняється не все: заборонені заголовки мовчки ігноруються, а підмінені
url,methodіpostDataне переживають редирект — обидві пастки дають зелений тест на непідміненому запиті. - Мережу чекають промісом, створеним до дії й без
await; мережева відповідь — сигнал про обмін, а не про готовність інтерфейсу. - Мок купує детермінізм за ціну довіри до власних припущень: має бути хоча б один тест без дублерів, а знімки (мок, HAR) потребують звірки з реальним застосунком.
Можливі питання
«Як замокати API-відповідь у Playwright?» Мінімальна відповідь — page.route() плюс route.fulfill(). Сильна відповідь одразу додає, що маршрут можна поставити на контекст, що незбіглі запити йдуть на реальний сервер і що є режим «зроби справжній запит, але пропатч відповідь». Інтервʼюер слухає, чи ви розрізняєте повний стаб і патч — це різні рівні довіри до бекенда.
«Чим мок відрізняється від стаба?» Питання має точну відповідь: місцем і моментом перевірки, а не тим, «наскільки розумний обʼєкт». Стаб — точка керування входами, мок перевіряє виходи під час взаємодії й задає очікування на виклики. Помилковий шлях — відповідати «мок розумніший за стаб».
«Що мокати в e2e, а що ні?» Тут перевіряють інженерну зрілість, а не знання API. Очікують межу: зовнішнє й некероване — так; власний основний API в наскрізному тесті — з великою осторогою; і обовʼязково згадку, що має лишитися хоча б один тест без дублерів, інакше сюїта верифікує ваші припущення.
«Як дочекатися завершення запиту?» Правильно: page.waitForResponse() з промісом, створеним до дії. Червоний для інтервʼюера — фіксована пауза; жовтий — networkidle як сигнал готовності, бо дока прямо радить замість нього вебасерти.
«Як змоделювати недоступний сервіс?» Очікують route.abort() — і бонусом розуміння, що коди помилок різні: connectionrefused, namenotresolved, timedout описують різні збої, і сторінка може реагувати на них по-різному.
«Ваші тести зелені, а на продакшні баг у списку замовлень. Як таке можливо?» Відповідь, яку хочуть почути, називає замокану відповідь, застарілий HAR і розходження дублера з реальним контрактом бекенда — і план: звірити дублер із реальним застосунком або мати рівень тестів, який ходить у справжній бекенд. Назви-терміна тут не чекають: усталеної галузевої назви явище не має, тож описувати варто механізм. Нестабільність через мережу — тема глави Боротьба з флаком засобами інструмента.
Джерела
- Playwright — Network — API спостереження й зміни трафіку, маршрут на контекст і на сторінку, дія на попапи, вибірковість перехоплення.
- Playwright API — class: Route — зворотний до реєстрації порядок обробників, різниця
route.continue()іroute.fallback(). - Playwright — class Page — пріоритет маршрутів сторінки над маршрутами контексту, найпізніше зареєстрований маршрут,
page.unroute(). - mitmproxy — How mitmproxy works — та сама модель вклинювання між клієнтом і сервером на рівні проксі.
Три рішення обробника: fulfill, continue, abort
- Playwright — Network — три рішення обробника; підміна відповіді на основі справжньої, отриманої через
APIRequestContext. - Playwright — Mock APIs — повний стаб («запит до API не відбувається взагалі») проти режиму «виконати запит і пропатчити відповідь».
- Playwright API — class: Route — закритий перелік кодів
abortіз дефолтомfailedі описом кожного класу збою.
Заголовки: що підміняється, а що мовчки ні
- Playwright — Network — пропускання запиту зі змінами, зокрема підміна або видалення заголовка.
- Playwright API — class: Route — заголовки переживають редиректи, а
url/method/postData— ні; підміна заборонених заголовків мовчки ігнорується. - Playwright — API testing — фікстура
requestуспадковуєbaseURLіextraHTTPHeadersіз конфігурації.
Мокати чи не мокати: ціна детермінізму
- xUnit Test Patterns — Test Double — дублер як «test-specific equivalent», стаб як точка керування, вимога мати хоча б один тест без дублерів, крихкість через переспецифікацію.
- Martin Fowler — Mocks Aren't Stubs — розведення стаба й мока: заготовлені відповіді проти заданих очікувань на виклики.
- Playwright — Mock APIs — мережевий стаб як реалізація дублера: запит до API не відбувається взагалі.
- ISTQB® Certified Tester Foundation Level Syllabus v4.0.1 — §1.4.3: стаби, драйвери, симулятори й віртуалізація сервісів як елементи тестового оточення (таксономії дублерів силабус не дає).
- Ian Robinson — Consumer-Driven Contracts: A Service Evolution Pattern — контракт як межа між і провайдером, від якої відривається дублер.
- Pact — офіційна документація (Introduction) — звірка дублера з реальним застосунком як механізм проти старіння мока.
Очікувати відповідь, а не годинник
- Playwright — Network — події
requestіresponseдля пасивного спостереження;page.waitForResponse()замість фіксованої паузи; glob-патерни URL у методах перехоплення. - Playwright — class Page — проміс очікування створюють до дії й без
await;networkidleпозначено як DISCOURAGED.
HAR: записати сесію і відтворити її
- Chrome DevTools — Network features reference — HAR як JSON-формат експорту сесії, імпорт назад у DevTools, санітизований експорт без
Cookie,Set-CookieйAuthorization. - Playwright — Network — використання HAR-файлів як джерела підмінених відповідей.
- Ian Robinson — Consumer-Driven Contracts: A Service Evolution Pattern — відрив зафіксованого знімка від чинного контракту.
- Pact — офіційна документація (Introduction) — звірка з реальним застосунком як спосіб виявити протухлий знімок.
APIRequestContext: підготувати стан, а не клікати
- Playwright — API testing — три застосування
APIRequestContext, зокрема підготовка серверного стану до відкриття застосунку; взаємозамінність стану сховища між API- і браузерним контекстом. - Playwright — class APIRequestContext —
page.requestяк скорочення й спільне сховище cookie, ізоляція черезapiRequest.newContext(),failOnStatusCodeз межею2xx/3xx, дефолт 20 редиректів, дефолтний таймаут. - ISTQB® Certified Tester Foundation Level Syllabus v4.0.1 — §1.4.3–1.4.4 і §4.1: вимоги до тестових даних визначають на етапі дизайну, а створення тестових фікстур — окрема діяльність реалізації тестів.
- Playwright API — class: Route — заборонені заголовки ігноруються мовчки; підміна
url/method/postDataне переноситься на редиректи. - Playwright — class Page — проміс очікування до дії без
await; увімкнення маршрутизації вимикає HTTP-кеш;page.unroute(). - Playwright — Network — Service Worker (зокрема воркер MSW) робить запити невидимими для маршрутів; рекомендація
serviceWorkers: 'block'. - Chrome DevTools — Network features reference — санітизований HAR без
Cookie,Set-CookieйAuthorization. - Pact — офіційна документація (Introduction) — звірка дублера з реальним застосунком як засіб проти розходження з контрактом.
- Playwright — Network — три рішення обробника, вибірковість перехоплення, очікування конкретної відповіді.
- Playwright API — class: Route — порядок обробників, межі підміни запиту.
- xUnit Test Patterns — Test Double — вимога мати хоча б один тест без дублерів.
- xUnit Test Patterns — Test Double — стаб як точка керування, мок як перевірка непрямих виходів, вимога тесту без дублерів.
- Martin Fowler — Mocks Aren't Stubs — мок як специфікація очікуваних викликів проти стаба із заготовленими відповідями.
- Playwright — Network —
page.route()іroute.fulfill(), обрив запиту, очікування конкретної відповіді. - Playwright API — class: Route — коди
abortяк різні класи мережевого збою. - Playwright — class Page —
networkidleяк DISCOURAGED і рекомендація вебасертів.
Відео до теми
Що таке перехоплення мережі й навіщо воно в e2e-тестах?
Перехоплення (network interception) — це коли тест між сторінкою та мережею й сам вирішує долю запиту: віддати вигадану відповідь, пропустити зі змінами або обірвати. Модель проста й однакова в усіх інструментах цього класу: реєструється правило «запит із таким URL → ось цей обробник», обробник ухвалює рішення. Playwright бачить увесь трафік сторінки, зокрема XHR і fetch, тому в тесті стають доступними стани, до яких на живому оточенні не дійти руками: платіжного провайдера, порожній список, 500 від сусіднього сервісу. Другий мотив — стабільність: тест перестає залежати від того, що сьогодні лежить у базі стейджа і чи не котиться там саме . Ціна цього теж існує, і про неї питають окремо: що більше замокано, то більше тест перевіряє наші уявлення про бекенд, а не бекенд.
Як замокати відповідь API у Playwright?
Базовий рецепт — page.route() із шаблоном URL плюс route.fulfill() в обробнику: до сервера запит не дійде взагалі, браузер отримає рівно той статус, content-type і тіло, які задав тест. Сильніша відповідь одразу додає три речі. Перша: правило можна повісити не на сторінку, а на — тоді воно накриє і попапи, і сторінки, відкриті з них. Друга: перехоплення вибіркове — усе, що не збіглося з шаблоном, спокійно піде на справжній сервер, тож маршрут не «вимикає мережу». Третя: крім повної підміни є змішаний режим — виконати реальний запит і підправити в його відповіді лише те, що заважає детермінізму. Саме за цією третьою деталлю інтервʼюер розрізняє, чи розумієте ви, скільки довіри до бекенда коштує кожен варіант.
Які рішення може ухвалити обробник маршруту?
Рівно три, і це весь словник теми. fulfill — віддати рукотворну відповідь, сервер при цьому не викликається взагалі. continue — пропустити запит далі, дорогою змінивши URL, метод, заголовки чи тіло; запит іде на справжній сервер. abort — обірвати запит із мережевою помилкою, тобто змоделювати недоступність сервісу. Поверх цих трьох живе четвертий, змішаний патерн: сходити на сервер по-справжньому, узяти реальну відповідь і віддати її в fulfill, перевизначивши окремі поля. Практичний наслідок: перш ніж писати обробник, треба відповісти на питання «мені потрібен контакт із бекендом чи ні» — від цього залежить, який із трьох викликів ви пишете.
Чи означає ввімкнене перехоплення, що тест працює без мережі?
Ні, і це важлива інтуїція. Маршрут накриває тільки ті запити, що підійшли під його шаблон; решта йде на реальний сервер, ніби перехоплення й не існує. Тому «замокав аналітику» не означає «застосунок тепер офлайн» — сторінка й далі тягне свій API, статику й шрифти. Наслідок для тест-дизайну: шаблон варто робити настільки вузьким, наскільки дозволяє задача, бо надто широкий (**/*) непомітно затягує в обробник усе, включно з навігаційними запитами й асетами. І окремо треба тримати в голові побічний ефект: увімкнена маршрутизація вимикає HTTP-кеш браузера, тож тест, що перевіряє 304 чи роботу Cache-Control, під активним маршрутом перевіряє вже інший застосунок.
Чим page.route() відрізняється від browserContext.route()?
Різниця в зоні дії й у пріоритеті. Маршрут сторінки живе тільки в межах цієї вкладки, маршрут контексту накриває весь контекст — зокрема попапи й сторінки, відкриті з поточної. Тому глушіння аналітики, чи сторонніх віджетів логічно вішати один раз на контекст, а не копіювати в кожен тест. Коли обидва рівні збіглися з одним запитом, перемагає маршрут сторінки — саме на цьому побудований звичний прийом: загальне правило лежить у чи хуку, а конкретний тест локально перекриває його своїм. Якщо ж перекриття не потрібне, а потрібно ланцюжком пройти обидва обробники, керування передають явно через route.fallback().
Мок ніби не спрацював: обробник є, а сторінка отримала справжні дані. Куди дивитися першим?
Найчастіша причина — порядок реєстрації: під правило потрапляють лише ті запити, які стартували вже після його появи. Коли page.route() опиняється нижче за page.goto(), початкове завантаження встигло відпрацювати, і обробникові дістається порожнеча. Звучить нудно, але економить години: спершу правило, потім дія, яка тригерить запит. Друга за частотою причина — шаблон просто не збігся: інший префікс шляху, версія API в URL, query-параметри, абсолютний URL стороннього домену замість очікуваного. Третя, найменш очевидна — трафік перебрав на себе Service Worker: зареєстрований сторінкою (а таким буває й службовий воркер бібліотек , скажімо MSW) обслуговує запити самотужки, і для маршрутів обох рівнів вони фактично не існують; лікується це блокуванням воркерів у налаштуваннях контексту.
Коли потрібен режим «виконати справжній запит і пропатчити відповідь»?
Коли реальний контакт із бекендом цінний, а детермінізму бракує лише в одному фрагменті даних. Типова ситуація: список приходить із сервера, і його загальна форма нас влаштовує, але для перевірки потрібен рівно один елемент у конкретному статусі — або, навпаки, треба прибрати випадковий елемент, через який тест плаває. Технічно це route.fulfill(), у який передають реальну відповідь, отриману через APIRequestContext для того самого запиту, і перевизначене тіло. Різниця з повним принципова: у повному стабі ви перевіряєте свою модель бекенда, у патчі зв'язок із бекендом лишається, а фіксується тільки те, що заважає. Мінус теж є — тест уже не працює без живого сервера, тобто ви втрачаєте частину швидкості й незалежності в обмін на правдивість.
Як змоделювати недоступний сервіс і чи важливо, який код помилки обрати?
Недоступність моделюють через route.abort(), і код помилки тут не довільний рядок, а значення із закритого набору; якщо не вказати, застосується дефолтний failed. Важливо, бо коди — це різні негативні сценарії, а не синоніми: connectionrefused описує сервіс, який не слухає порт; namenotresolved — нерозвʼязане DNS-імʼя; internetdisconnected — зникнення мережі в клієнта; timedout — операцію, що не вклалася у відведений час; connectionreset відповідає обриву на рівні TCP RST, а connectionclosed — закриттю через TCP FIN. Застосунок може реагувати на них по-різному: на відмову зʼєднання показати заглушку одразу, а на — крутити спінер до власного дедлайну, і саме друге частіше буває непокритим. Для тест-дизайну це шість окремих кейсів деградації, тому відповідь «викликаю abort і все» на співбесіді середнього рівня вже недостатня.
Як правильно дочекатися завершення конкретного запиту після дії?
Через page.waitForResponse() із шаблоном URL, і головне тут — порядок: очікування створюють до дії й без await. Тоді підписка вже активна на момент кліку, і після нього тест лише забирає готовий результат другим рядком з await. Якщо поставити await одразу, тест зупиниться ще до кліку, дія не відбудеться, запит не піде — і все закінчиться таймаутом, який виглядає як , а насправді є гонкою, вбудованою в код. Парний метод для вихідного запиту — page.waitForRequest(), а для пасивного спостереження без синхронізації підписуються на події request і response: так перевіряють, що аналітика надіслала подію або що після кліку взагалі пішов POST. Фіксована пауза замість цього — червоний .
Ви дочекалися відповіді 200, а асерт на елемент все одно падає. Чому?
Бо мережа й інтерфейс — різні речі. Статус 200 означає, що обмін завершився, а не що фронтенд уже розібрав дані, оновив стан і перемалював список; між цими подіями є рендер, на який мережеве очікування жодних гарантій не дає. Правильний розподіл такий: мережу чекають там, де потрібен сам факт запиту чи його вміст (пішов POST, статус 201, у тілі є id), а стан UI перевіряють вебасертами з . Окремо варто знати, що networkidle як універсальний сигнал готовності документація прямо не радить і пропонує замість нього перевірки, орієнтовані на інтерфейс: у застосунку з polling або довгими зʼєднаннями «тиша в мережі» може не настати ніколи, а в SPA настати раніше, ніж домалюється екран.
У тесті збіглося кілька маршрутів на один URL. Котрий спрацює?
Порядок детермінований і його варто знати дослівно. По-перше, маршрути сторінки мають пріоритет над маршрутами контексту. По-друге, серед кількох збігів одного рівня виконується найпізніше зареєстрований — обробники йдуть у порядку, зворотному до реєстрації, щоб останнє правило завжди могло перекрити попередні. Практичний наслідок: широкий мок кладуть у конфігурацію або в хук, а вужче правило реєструють пізніше, у самому тесті, і воно переможе. По-третє, ланцюжок за замовчуванням не продовжується: route.continue() його зупиняє й відправляє запит, а передати керування іншим збігам можна тільки явним route.fallback(). Плюс до цього — : page.unroute() знімає правило разом з обробником, і це не косметика, а частина ізоляції, бо забутий маршрут перехоплює запити наступних тестів у тому самому воркері.
Тест підмінив Cookie через continue, щоб перевірити іншого користувача, і став зеленим. Чому це нічого не доводить?
Бо частину заголовків підмінити не можна: Cookie, Host, Content-Length та інші заборонені для перевизначення, і якщо ви їх передаєте, значення просто ігнорується — поїде оригінальне. Помилки чи попередження при цьому не буде, тест не почервоніє, у трасі все виглядатиме як задумано. Тобто перевірка «чужий користувач не бачить цих даних» насправді зробила запит від того самого користувача й підтвердила очевидне: своє він бачить. Це класичний хибно зелений результат, і саме тому автентифікацію в тестах підміняють не заголовком у маршруті, а збереженим станом сховища або окремим контекстом. Якщо ж один нешкідливий заголовок треба додати всім запитам, дешевше не перехоплювати нічого, а задати extraHTTPHeaders у конфігурації.
Ви змінили URL і тіло запиту в continue, а сервер отримав оригінал. Що сталося?
Майже напевно між вами й сервером стався редирект. Правило асиметричне: заголовки застосовуються і до перехопленого запиту, і до всіх редиректів, які він спричинив, а от підміна URL, методу й тіла діє тільки на початковий запит і на редиректи не переноситься. Тому після першого 3xx поїде те, що згенерувала сторінка, а не те, що ви підставили — і тест перевірить непідмінений запит, лишившись зеленим. Діагностика проста: подивитися в трасі або в мережевій панелі, чи є в ланцюжку 3xx, і якщо є — цілити маршрутом у фінальний URL, а не у вхідний. Разом із мовчазним ігноруванням заборонених заголовків це дві пастки одного класу: підміна ніби відбулася, а насправді ні, і жодного сигналу про це немає.
Чим мок відрізняється від стаба?
Різниця не в «розумності», а в місці й моменті перевірки — це і є очікувана відповідь. Стаб віддає заготовлені відповіді й служить точкою керування: він заганяє систему в потрібну гілку, у яку інакше не потрапити, а перевірка залишається там, де й була — на результаті. Мок задає очікування щодо викликів і перевіряє непрямі виходи під час взаємодії: чи звернулися до залежності, скільки разів, з якими аргументами. Обидва — різновиди , тобто компонента, який заміняє реальну залежність специфічним для тесту еквівалентом і повторює її API, а не її поведінку. Мережевий стаб через route.fulfill() — саме стаб; мок з нього виходить, коли ви додатково факт і форму самого звернення, наприклад через підписку на подію request. Відповідь «мок розумніший» вважається неправильною.
Що мокати в e2e, а що ні?
Тут перевіряють інженерну зрілість, а не знання API, тому відповідь має містити межу, а не гасло. Мокати варто зовнішнє й некероване: платіжні , аналітику, сторонні віджети, сервіси, які ви не можете привести в потрібний стан. Власний основний API в мокати без потреби не варто — інакше перевіряє наші уявлення про бекенд замість інтеграції з ним. Компроміс формулюють через рівень: мокати на найтоншому потрібному шарі й тільки те, що заважає, а деградацію окремого сервісу моделювати abort замість підміни всього трафіку. Обовʼязкова частина відповіді — вимога мати хоча б один тест без дублерів: з дублером перевіряється інша конфігурація, ніж піде в продакшн. Другий бік — надмірна кількість дублерів робить тести крихкими через переспецифікацію: змінилася деталь взаємодії, яку ніхто не обіцяв, і сюїта червона.
Сюїта зелена, а на продакшні баг у списку замовлень. Як це можливо?
Найімовірніший механізм — сюїта перевіряє знімок бекенда, а не бекенд. Замокана відповідь або записаний HAR фіксують контракт на момент написання тесту; бекенд перейменував поле, змінив формат дати чи вкладеність — продакшн зламався, а мок і далі віддає старий формат, і тести зелені. Своєї усталеної галузевої назви це явище не має, тому на співбесіді краще описати механізм, ніж підставляти термін. План лікування складається з двох частин: звіряти дублер із реальним застосунком — саме так працюють контрактні тести, де перевіряють, що дублер повертає те саме, що повернув би справжній сервіс, — і тримати рівень тестів, який ходить у живий бекенд без підміни. Побічно варто перевірити й тривіальніші причини: чи тест взагалі виконував той сценарій, чи не проковтнув він хибно зелений результат через непідмінений заголовок.
Що таке HAR і які рішення треба ухвалити перед його використанням у тестах?
HAR (HTTP Archive) — JSON-формат, у який вивантажують зафіксовану мережеву сесію; його розуміють різні інструменти, зокрема мережева панель DevTools, яка вміє й експортувати архів, і завантажити його назад для аналізу. У Playwright такий архів можна зробити джерелом відповідей: спершу прогін із записом, далі прогони, де відповіді беруться з файлу, — це режим запису-відтворення, і дані в ньому правдиві, бо одного разу їх справді віддав бекенд, а на наступних прогонах походу в мережу вже не потрібно. Свідомо ухвалити треба три рішення. Перше — що робити із запитом, якого в архіві немає: обірвати чи пустити в мережу. Друге, і найважливіше, — безпека: типовий експорт із DevTools санітизований (без Cookie, Set-Cookie й Authorization), а повний запис містить усе, включно з токенами й персональними даними в тілах, тому файл перед чистять руками. Третє — старіння: HAR протухає так само, як будь-який дублер, тож він добре працює на громіздких і стабільних відповідях (довідники, каталоги) і погано там, де контракт активно змінюють.
Навіщо APIRequestContext, якщо тест і так має браузер?
Щоб не платити пʼятьма екранами інтерфейсу за один потрібний стан. Створення даних через UI — найдорожчий і найкрихкіший спосіб дійти до умов експерименту: будь-яка зміна форми реєстрації ламає тести, які взагалі не про реєстрацію. Документація прямо називає три ролі API-контексту: перевіряти саме серверне API, приводити бекенд у потрібний стан ще до першого відкриття сторінки й контролювати серверні пост-умови вже після дій у браузері — тобто «засій через API, перевір в UI» це підтримуваний сценарій, а не хак. Підготовка даних тут не імпровізація, а планована активність: вимоги до визначають ще на дизайні, а створення фікстур — окремий крок реалізації тестів. Бонус із тієї самої механіки: стан сховища взаємозамінний між браузерним і API-контекстом, тому логін можна зробити , а далі працювати в браузері вже авторизованим.
Чому негативна перевірка доступу через page.request буває хибно зеленою?
Бо page.request — це не окремий клієнт, а скорочення для контексту сторінки: той самий екземпляр, те саме , яке ще й оновлюється з Set-Cookie. Тому запит «від імені стороннього користувача», зроблений через page.request, поїде з кукі вже залогіненого браузера — і перевірка «чужий не бачить цього замовлення» насправді підтвердить, що власник своє замовлення бачить. Для ізоляції потрібен окремий контекст із власним сховищем, створений через apiRequest.newContext(), або явно порожній стан сховища. Той самий принцип працює і в інший бік: спільне сховище cookie — саме те, що робить зручним «залогінитися через API й одразу відкрити сторінку». Тобто це не дефект, а властивість, про яку треба знати, вибираючи контекст під задачу.
Запит через API-контекст повернув 500, а тест поїхав далі й не впав. Це нормально?
Так, і це одна з чотирьох деталей API-контексту, які легко дають хибно зелений результат. За замовчуванням обʼєкт відповіді повертається для будь-якого коду: клієнт не кидає ні на 404, ні на 500, тож без явного асерту на статус тест поїде далі з тілом помилки в руках. Кидання вмикають прапорцем failOnStatusCode, і межа при цьому проходить не там, де очікують: успішними вважаються і 2xx, і 3xx, тобто 301 помилкою не стане. Друга деталь — редиректи: клієнт іде за ними автоматично, за замовчуванням до двадцяти, тому перевірка самого 3xx вимагає їх вимкнути, інакше тест мовчки пройде ланцюжок і побачить фінальний 200. Третя — таймаут: він діє завжди, навіть коли тест про нього не згадував, змінюється окремим методом і вимикається лише значенням 0, а передача AbortSignal його не скасовує. Четверта — вибір контексту, від якого залежить, чиї кукі поїдуть із запитом.
Три кейси про мережу в Playwright: «який сценарій — яким інструментом», розбір хибно зеленого тесту з чотирма дефектами в одному сценарії і підготовка стану через API з ізольованим клієнтом для негативної перевірки доступу. Скрізь — що дивитися і чому.
Кейс 1. Таблиця рішень: сценарій → інструмент
Найчастіша помилка тест-дизайну тут — брати route.fulfill() на все, бо він перший, який вивчають. Насправді вибір диктує питання «чи потрібен мені контакт із бекендом», і відповідей на нього чотири.
| Що треба перевірити | Інструмент | Чому не інше |
|---|---|---|
Порожній список, 500 від сервісу, протермінований токен | fulfill — повний стаб | Реального запиту немає взагалі, стан задається одним рядком; на живому оточенні цих даних просто нема |
| Той самий список, але з одним елементом у потрібному статусі | справжній запит + fulfill із патчем | Повний стаб зафіксував би всю структуру відповіді, а фіксувати треба один елемент |
| Сусідній сервіс лежить, сторінка мусить лишитися робочою | abort із конкретним кодом | fulfill з 500 — це «сервер відповів помилкою», а не «сервер не відповів»; це різні гілки коду фронтенду |
| Фіча-флаг у заголовку для всіх запитів прогону | extraHTTPHeaders у конфігурації | Маршрут на **/* заради одного заголовка вимикає HTTP-кеш і тягне в обробник усі асети |
| Потрібне замовлення в базі до відкриття екрана | APIRequestContext | Мокати нічого не треба: стан справжній, перевірка йде проти живого бекенда |
| Громіздкий стабільний довідник | routeFromHAR | Ручний стаб на кілобайти JSON ніхто не оновлюватиме, а запис-відтворення дає реалістичні дані |
Три верхні рядки в коді виглядають так — зверніть увагу, що всі маршрути реєструються до переходу на сторінку:
import { test, expect } from '@playwright/test';
test('порожній список показує заглушку, а не спінер', async ({ page }) => {
await page.route('**/api/v1/orders', (route) =>
route.fulfill({ status: 200, contentType: 'application/json', body: '[]' })
);
await page.goto('/orders');
await expect(page.getByTestId('orders-empty')).toBeVisible();
});
test('замовлення в статусі «скасовано» показує причину', async ({ page }) => {
await page.route('**/api/v1/orders', async (route) => {
const real = await page.request.fetch(route.request());
const orders = await real.json();
// фіксуємо лише той елемент, через який тест плаває
orders[0] = { ...orders[0], status: 'cancelled', reason: 'Оплата не пройшла' };
await route.fulfill({ response: real, body: JSON.stringify(orders) });
});
await page.goto('/orders');
await expect(page.getByTestId('cancel-reason')).toHaveText('Оплата не пройшла');
});
// деградація сусіднього сервісу: із шести кодів беремо три найчастіші
const failures = ['connectionrefused', 'namenotresolved', 'timedout'] as const;
for (const code of failures) {
test(`сторінка виживає, коли рекомендації відпали (${code})`, async ({ page }) => {
await page.route('**/api/v1/recommendations', (route) => route.abort(code));
await page.goto('/product/A-100');
// головний контент на місці, блок рекомендацій згорнувся без падіння
await expect(page.getByTestId('add-to-cart')).toBeEnabled();
await expect(page.getByTestId('recommendations')).toBeHidden();
});
}
Що дивитися і чому:
fulfillіз500іabortперевіряють різні гілки. У першому випадку фронтенд отримав відповідь із кодом помилки, у другому — мережеву помилку без відповіді взагалі. Обробники в застосунку зазвичай різні, і саме друга гілка частіше буває непокритою.- Патч замість повного економить оновлення. Коли бекенд додасть поле, повний стаб про це не дізнається, а патч поїде з реальною структурою й новим полем усередині.
- Незбіглі запити йдуть у мережу. У тесті з рекомендаціями решта трафіку — товар, ціна, кошик — справжня, тому перевірка «сторінка робоча» лишається змістовною.
- Не те, що можна засіяти. Якщо потрібний стан досяжний через API, стаб лише додає ще один знімок, який колись розійдеться з контрактом.
Кейс 2. Ревʼю: чотири дефекти в одному короткому тесті
Такий тест приходить у із коментарем «перевіряє права менеджера, зелений». Сторінку тут створюють один раз на весь файл — типова оптимізація, коли логін дорогий; зверніть увагу, що це робиться явно, бо сам по собі serial-режим контекст між тестами не ділить.
// як не треба
import { test, expect, type Page } from '@playwright/test';
test.describe.configure({ mode: 'serial' });
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage(); // одна сторінка на всі тести файлу
});
test('менеджер бачить розділ звітів', async () => {
await page.goto('/dashboard'); // 1
await page.route('**/api/v1/reports**', async (route) => { // 2
const headers = { ...route.request().headers(), cookie: 'session=manager-42' };
await route.continue({ headers });
});
await page.waitForResponse('**/api/v1/reports**'); // 3
await page.getByRole('button', { name: 'Звіти' }).click();
await expect(page.getByTestId('reports-widget')).toBeVisible(); // 4
});
Що тут не так:
- Маршрут зареєстровано після
goto. Запити початкового завантаження пішли раніше, ніж зʼявилося правило, тому перехопити їх уже неможливо. Маршрут ставлять до дії, яка тригерить запит, — це стосується іgoto, і кліку. - Підміна
Cookieмовчки ігнорується. Це заборонений для перевизначення заголовок: значення відкинеться без помилки й без попередження, і запит поїде з сесією того користувача, під яким уже відкрито браузер. Тест «перевірив права менеджера», а насправді підтвердив, що адміністратор бачить звіти, — і показав зелене. Правильно тут не заголовок у маршруті, а окремий стан входу: свійstorageStateна роль. awaitперед дією — вбудована гонка. Тест зупиняється на очікуванні відповіді ще до кліку, кнопка не натиснута, запит не пішов — і все закінчується , який у звіті виглядає як . створюють до дії й безawait, а забирають після неї.- Маршрут ніхто не зняв. Сторінка живе довше за тест, тому правило переживає його межу й перехоплює запити наступних — плаваючі падіння з порядковою залежністю. Із дефолтною
pageконтекст свій на кожен тест і проблеми немає, але тільки-но сторінку починають переносити між тестами, черезpage.unroute()стає обовʼязком тесту.
Виправлений варіант — дефолтна фікстура page замість спільної сторінки, інша роль замість підміни заголовка, підписка до кліку:
// як треба
test.use({ storageState: 'auth/manager.json' });
test('менеджер бачить розділ звітів', async ({ page }) => {
await page.goto('/dashboard');
// підписка створена ДО кліку і без await
const reportsResponse = page.waitForResponse(
(res) => res.url().includes('/api/v1/reports') && res.request().method() === 'GET'
);
await page.getByRole('button', { name: 'Звіти' }).click();
expect((await reportsResponse).status()).toBe(200);
await expect(page.getByTestId('reports-widget')).toBeVisible();
});
Що дивитися і чому:
- на
200і асерт на віджет — різні перевірки. Перший каже, що обмін відбувся, другий — що інтерфейс це відмалював. Без другого тест зелений навіть тоді, коли віджет тихо впав на рендері. - Предикат замість glob дає точність. Коли на екрані кілька схожих URL, фільтр за методом і шляхом рятує від збігу з чужим запитом.
- Хибно зелений результат дорожчий за червоний. Червоний тест видно одразу; тест, який роками «перевіряв права», виявляється при першому інциденті — і всі перевірки прав доводиться переписувати.
Кейс 3. Засіяти стан через API — і не збрехати в негативній перевірці
Дійти до потрібного стану кліками дорого: пʼять екранів заради одного статусу. Замовлення створюємо запитом, а перевіряємо в інтерфейсі.
import { test, expect } from '@playwright/test';
test('нове замовлення видно у списку', async ({ page, request }) => {
const created = await request.post('/api/v1/orders', {
data: { sku: 'A-100', qty: 2 },
});
// клієнт не кидає ні на 404, ні на 500 — статус перевіряємо руками
expect(created.status(), await created.text()).toBe(201);
const order = await created.json();
await page.goto(`/orders/${order.id}`);
await expect(page.getByTestId('order-status')).toHaveText('Нове');
});
Тепер негативна частина — «чужий менеджер це замовлення не бачить». Саме тут легко отримати зелений тест ні про що, якщо взяти не той клієнт:
test('менеджер іншої команди отримує 403', async ({ request, playwright }) => {
const created = await request.post('/api/v1/orders', { data: { sku: 'A-100', qty: 1 } });
expect(created.status(), await created.text()).toBe(201);
const order = await created.json();
// окремий контекст = власне спільне сховище cookie й власний стан входу
const stranger = await playwright.request.newContext({
baseURL: process.env.BASE_URL,
storageState: 'auth/stranger.json',
});
const forbidden = await stranger.get(`/api/v1/orders/${order.id}`);
expect(forbidden.status()).toBe(403);
await stranger.dispose();
});
Що дивитися і чому:
- Три різні клієнти легко переплутати.
page.request— той самий екземпляр, що й у браузерного контексту: , яке ще й оновлюється зSet-Cookie. Фікстураrequest— окремий екземпляр на тест. Повністю свій клієнт з іншим станом входу дає лишеnewContext(). Негативну перевірку доступу черезpage.requestробити не можна: авторизація протече з браузера, і «чужий» запит поїде від власника. - Статус треба асертити явно. За замовчуванням відповідь повертається для будь-якого коду, тому
POST, який упав з500, не зупинить тест — далі впаде вжеcreated.json()з незрозумілою помилкою парсингу. Другий аргумент уexpect(тіло відповіді) економить пів години на розборі CI. failOnStatusCodeне покриває3xx. З увімкненим прапорцем успіхом вважаються і2xx, і3xx, тож301помилкою не стане. А якщо перевіряти треба сам факт редиректу, автопрямування потрібно вимкнути окремо — інакше клієнт мовчки пройде ланцюжок (за замовчуванням до двадцяти кроків) і покаже фінальний200.- Стан сховища взаємозамінний. Логін можна зробити , зберегти стан і піднімати з нього браузерні контексти — саме на цьому будується повторне використання автентифікації між тестами.
Де тест вклинюється в мережу
- Знаю різницю
page.route()vsbrowserContext.route(): перший діє в межах вкладки, другий накриває весь контекст, зокрема попапи й відкриті з них сторінки. - Розумію дві властивості правила: перехоплення вибіркове (незбіглі запити спокійно йдуть у мережу, маршрут її не «вимикає») і воно діє лише на запити, які стартували після реєстрації, тому маршрут ставлять до дії, що тригерить запит.
- Знаю порядок при кількох збігах: маршрути сторінки перекривають маршрути контексту, серед одного рівня перемагає найпізніше зареєстрований, а
route.continue()ланцюг зупиняє — передати керування далі можна лише черезroute.fallback().
Три рішення обробника
- Можу назвати всі три рішення обробника й розвести їх за наслідком:
fulfillвіддає відповідь без походу на сервер,continueвідправляє запит зі змінами,abortзавершує його мережевою помилкою. - Знаю змішаний патерн: виконати справжній запит, узяти реальну відповідь і віддати її в
fulfillз перевизначеним тілом — детермінізм лише там, де він потрібен. - Знаю, що коди
abort— закритий набір із дефолтомfailed, і розрізняюconnectionrefused,namenotresolved,timedout,internetdisconnectedяк різні сценарії деградації, а не синоніми.
Межі підміни: де тест мовчки бреше
- Знаю, що заборонені заголовки (
Cookie,Host,Content-Lengthта інші) не підміняються, а перевизначення ігнорується без жодної помилки. - Можу пояснити асиметрію редиректів: заголовки переживають ланцюжок, а підмінені
url,methodіpostData— ні. - Розумію, чому обидві пастки дають саме хибно зелений тест — перевірка виконалася на непідміненому запиті, — і знаю дешевшу альтернативу для постійного заголовка:
extraHTTPHeadersу конфігурації.
Очікування мережі
- Можу пояснити, чому
page.waitForResponse()створюють до дії й безawait, і що станеться, якщоawaitпоставити одразу. - Знаю різницю «дочекатися обміну» vs «дочекатися інтерфейсу»:
200не означає, що список перемалювався, томуnetworkidleяк сигнал готовності не рекомендують, а стан UI перевіряють вебасертами.
Ціна детермінізму
- Можу розвести і за місцем перевірки: стаб — точка керування входами, мок перевіряє непрямі виходи під час взаємодії.
- Знаю, що мокання дає (детермінізм, доступ до , швидкість) і що забирає — перевіряються власні припущення про бекенд; звідси вимога мати хоча б один тест без дублерів і зворотний бік надміру: крихкість через переспецифікацію.
- Можу описати механізм «зелена сюїта — баг у проді» через розходження дублера з чинним контрактом і назвати ліки: звірка дублера з реальним застосунком плюс орієнтир мокати зовнішнє й некероване, а не власний основний API.
HAR і побічні ефекти інструмента
- Розумію HAR як JSON-знімок сесії й режим запису-відтворення, і знаю два свідомих рішення: безпека (санітизований експорт DevTools без
CookieйAuthorization, повний запис — з ними, тому файл чистять перед ) і старіння знімка, через яке HAR годиться для громіздких стабільних відповідей, а не для контракту, що активно змінюють. - Знаю два побічні ефекти інструмента: ввімкнена маршрутизація вимикає HTTP-кеш (тест на
304чиCache-Controlпід маршрутом перевіряє інший застосунок), а Service Worker сторінки — зокрема MSW — робить запити невидимими для маршрутів. - Розумію, чому забутий маршрут тече в наступний тест того самого воркера, і знаю, що
page.unroute()— частина ізоляції, а не косметика.
APIRequestContext як підготовка стану
- Знаю три ролі API-контексту (перевірка самого API, приведення бекенда в потрібний стан перед відкриттям сторінки, контроль серверних пост-умов) і взаємозамінність стану сховища з браузерним контекстом.
- Розумію, що
page.requestкористується з браузером, тож негативна перевірка доступу через нього буває хибно зеленою — ізоляція лише через окремий API-контекст. - Памʼятаю чотири пастки клієнта: немає падіння на
404і500за замовчуванням,failOnStatusCodeвважає успіхом і2xx, і3xx, редиректи проходяться автоматично (дефолт — до 20), а дефолтний діє завжди й вимикається лише значенням0.
Квіз
Перед стартом
- Питань: 16
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Перехоплення мережі (network interception) — що це в одному реченні?

