08 · JavaScript/TypeScript для AQA
Дебаг JS/TS: console, breakpoints, source maps
Зміст
Тест упав, а повідомлення — expect(received).toBe(expected) з двома схожими рядками, і незрозуміло, звідки взялося не те значення. Перший інстинкт — насипати console.log по всьому файлу, перезапустити, подивитися, прибрати, додати ще один, знову перезапустити. Так можна, але це найповільніший зі способів, і на співбесіді за нього не хвалять. Дебаг — це вміння зупинити код у потрібній точці й побачити повний стан, а не вгадувати його по відбитках у консолі.
Для AQA це щоденний інструмент, а не екзотика. Тест впав у CI, локально зелений; локатор (locator) знаходить не той елемент; await кудись загубився і асерт біжить по undefined. У всіх цих випадках виграє той, хто вміє швидко локалізувати проблему: поставити breakpoint, покроково пройти виконання, глянути стек викликів, а для браузерних тестів — ще й переграти впалий прогін кадр за кадром. Вага теми на співбесіді низька, але сам навик економить години, тож розберемо арсенал: від console до source maps і trace viewer.
console: не лише log
console.log знають усі, але в об'єкта console є методи, які часто рятують швидше за нього. Головна ідея: вивід має бути читабельним, інакше ти витрачаєш час не на баг, а на розшифровку власного логу.
console.log друкує аргументи через пробіл. Корисний трюк — передати об'єкт у фігурних дужках як console.log({ user, token }): тоді у виводі буде видно і назви змінних, і їхні значення, а не просто голі дані без підпису.
console.table показує масив об'єктів як таблицю з колонками — незамінно, коли ти дивишся на тестові дані або на набір рядків з API. Другим аргументом можна передати перелік колонок, щоб не тонути в зайвих полях.
const users = [
{ id: 1, name: 'Ada', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
];
console.table(users, ['name', 'role']);
console.dir друкує об'єкт як розкладку властивостей, а не як текст. У браузері це принципова різниця: console.log(elementHandle) показує HTML-розмітку елемента, а console.dir(elementHandle) — сам об'єкт з його властивостями. У Node другим аргументом можна зняти обмеження глибини: console.dir(obj, { depth: null }) розкриє глибоко вкладений об'єкт, який console.log обрізає до [Object].
Ще кілька, які варто знати: console.error і console.warn пишуть у stderr і виділяються кольором; console.trace друкує стек викликів до поточної точки (звідки нас сюди покликали); console.count('мітка') рахує, скільки разів виконання дійшло до цього рядка — швидкий спосіб зловити зайву ітерацію чи подвійний виклик.
Але console має стелю: він показує стан лише в тій точці, куди ти вручну поставив рядок, і лише те, що встиг вивести. Щоб побачити геть усе й у будь-який момент — потрібні breakpoints.
Breakpoints у VS Code
Breakpoint (точка зупину) — це позначка на рядку, на якій виконання завмирає й віддає тобі керування. У цей момент доступний увесь стан: значення змінних, аргументи, this, стек викликів, і можна порахувати будь-який вираз у консолі дебагера. Це якісно інша річ, ніж console.log: ти не вгадуєш наперед, що вивести, — ти вже стоїш усередині й дивишся на все.
У VS Code breakpoint ставиться кліком у полі ліворуч від номера рядка (з'явиться червона крапка). Далі запускаєш дебаг (F5) — і на цьому рядку код зупиниться. Керування паузою:
- Step over — виконати поточний рядок і стати на наступний, не заходячи всередину викликаних функцій.
- Step into — зайти всередину функції на поточному рядку.
- Step out — доперти поточну функцію до кінця й вийти на рівень вище.
- Continue — бігти далі до наступного breakpoint.
Два різновиди, які економлять найбільше часу:
- Умовний breakpoint (conditional) — зупиняється лише коли вираз істинний. Дебажиш цикл на тисячу елементів, а баг на конкретному — став умову
item.id === 42і не тисни Continue вручну сто разів. - Logpoint — це breakpoint, який не зупиняє виконання, а друкує повідомлення. По суті
console.log, який не треба вписувати в код (а потім не забути прибрати). Ідеально, коли лізти в чужий файл правками не хочеться.
Замість кліку в полі можна вставити прямо в код інструкцію debugger; — якщо процес запущено під дебагером, виконання зупиниться на ній так само, як на breakpoint. Але це якраз те, що легко забути в коді (див. розділ «Типові помилки»).
Практична порада для тестового проєкту: у VS Code є JavaScript Debug Terminal — окремий термінал, який автоматично під'єднує дебагер до будь-якого процесу Node, запущеного в ньому. Відкрив його, поставив breakpoint у тесті, запустив npx playwright test звичайною командою — і зупинка спрацює без жодного launch.json.
Node під капотом: --inspect і --inspect-brk
VS Code не робить магії — під ним працює інспектор Node.js. Коли Node запускають з прапорцем --inspect, він відкриває сервер налагодження на порту 9229 за протоколом V8 Inspector (той самий Chrome DevTools Protocol, яким користуються й DevTools браузера). До цього порту під'єднується клієнт: VS Code, або DevTools Chrome за адресою chrome://inspect.
node --inspect app.js
Різниця між двома прапорцями критична:
--inspect— відкриває інспектор, але код починає виконуватися одразу. Якщо баг у самому старті програми, ти просто не встигнеш під'єднатися.--inspect-brk— відкриває інспектор і завмирає на першому ж рядку, чекаючи, поки під'єднається дебагер. Саме те, що треба, коли проблема в ініціалізації або в короткому скрипті, який відпрацьовує за мілісекунди.
node --inspect-brk app.js
Для тестів прямий node --inspect-brk руками потрібен рідко — VS Code і вбудовані інструменти ранерів роблять це за тебе. Але розуміти механізм важливо: коли дебаг «не чіпляється», проблема майже завжди в цьому шарі — не той порт, не той процес, або ранер запускає тести в окремому воркері, до якого інспектор головного процесу не дотягується.
Source maps: чому TS дебажиться, хоча виконується JS
Ось де в AQA виникає найбільше плутанини. Браузер і Node не вміють виконувати TypeScript — його спершу компілюють у JavaScript. Тобто реально біжить test.js, а ти пишеш і хочеш дебажити test.ts. Якби не було зв'язку між ними, breakpoint у .ts-файлі не мав би куди «прилипнути», а стек-трейс помилки показував би номери рядків у згенерованому .js, які не збігаються з твоїм джерелом.
Цей зв'язок і є source map (карта коду) — окремий файл (зазвичай test.js.map) або вбудований коментар, який каже: «рядок 12 у скомпільованому JS відповідає рядку 34 у вихідному TS». Дебагер читає цю карту й перекладає позиції туди-назад, тож ти ставиш breakpoint у .ts, а зупиняєшся ніби теж у .ts, хоча під капотом виконується інший файл.
Звідки карти беруться. Якщо компілюєш через tsc, за них відповідає опція sourceMap: true у tsconfig.json (детальніше про конфіг — у главі tsconfig і TypeScript у тестовому проєкті). Сучасні тестові ранери й швидкі транспілятори (esbuild, tsx, swc) зазвичай вмикають source maps самі — тому в Playwright стек-трейс помилки й breakpoints одразу вказують на .ts, і руками зазвичай нічого налаштовувати не треба.
Окремий, дуже практичний випадок — стек-трейси помилок. Коли Node виконує скомпільований JS і падає, за замовчуванням трейс показує рядки саме .js. Прапорець --enable-source-maps вмикає підтримку карт для трейсів, і тоді номери рядків у помилці вказують на оригінальний TS. Це різниця між «помилка десь у чужому мініфікованому файлі» й «помилка в моєму login.page.ts, рядок 40». Як читати сам стек-трейс — тема глави Помилки та винятки: try/catch і стек-трейси.
Логування в тестах
У тестовому коді console.log веде себе очікувано: він друкує в термінал, звідки ти запустив прогін. Але є пастка, специфічна саме для браузерних тестів: логи всередині браузера й логи тестового коду — це два різні світи. console.log у твоєму .ts-файлі виконується в Node і йде в термінал. А console.log, який пише сама сторінка (код застосунку у браузері), у термінал не потрапляє сам собою — його треба явно перехопити.
У Playwright це робиться підпискою на подію:
page.on('console', (msg) => console.log(`[browser] ${msg.type()}: ${msg.text()}`));
page.on('pageerror', (err) => console.log(`[browser error] ${err.message}`));
Тепер повідомлення й помилки зі сторінки видно в терміналі поряд з логами тесту — часто це найшвидший спосіб зрозуміти, що застосунок сам лаявся в консоль, а ти цього не бачив.
Кілька дисциплінарних правил логування в тестах:
- Не лишай
console.logу закомічених тестах. Він засмічує вивід прогону, і серед сотні рядків реальний сигнал губиться. Для тимчасового дебагу — логпоінти або локальні логи, які прибираєш перед комітом. - Структуровані кроки замість голих логів. У Playwright обгортай логічні блоки в
test.step('логінимось', async () => { ... })— вони читабельно лягають у звіт і trace viewer. У CodeceptJS цю роль грають людиночитні крокиI.*таI.say('...'). - Керуй «шумом» через рівні. Якщо треба заглянути глибше у внутрішню механіку інструмента, вмикай його власне детальне логування змінною середовища (наприклад,
DEBUG=pw:apiу Playwright), а не рефактор коду під логи.
Дебаг саме тестів: headed і trace viewer
Дебаг браузерного тесту має свою специфіку: багато проблем не в логіці коду, а у взаємодії з реальною сторінкою — елемент ще не з'явився, перекритий іншим, локатор чіпляє не той вузол. Тут покроковий дебагер у Node мало допомагає, бо треба бачити саму сторінку.
Перший інструмент — headed-режим: запуск із видимим вікном браузера замість фонового headless. У Playwright це прапорець --headed, а --debug (або змінна PWDEBUG=1) додатково відкриває Playwright Inspector — панель, де тест іде покроково, підсвічуючи локатори прямо на сторінці. Метод page.pause(), вставлений у тест, зупиняє прогін у цій точці й дає поклацати локатори вручну. У CodeceptJS аналог — виклик pause() у сценарії плюс запуск із --debug і --verbose.
Другий, і для CI-фейлів найважливіший, інструмент — trace viewer (переглядач трейсу). Це post-mortem-дебаг: Playwright записує трасування прогону (снапшоти DOM, усі дії, мережеві запити, консоль, час), і потім ти відкриваєш його як «машину часу» — переграєш впалий тест кадр за кадром, наводишся на будь-яку дію й бачиш стан сторінки саме в той момент. Записується трейс за конфігом (trace: 'on-first-retry' — типовий вибір: записувати лише при першому перезапуску), а відкривається командою npx playwright show-trace trace.zip.
Trace viewer розв'язує найболючіший клас проблем AQA — «локально зелений, у CI червоний». Ти не можеш відтворити фейл на своїй машині, але маєш повний запис того, що сталося на агенті: видно, що кнопка була перекрита банером, або що запит повернув 500, або що сторінка ще крутила спінер. Це і є та причина, чому трейс варто вмикати в CI завжди.
Детальний розбір trace viewer, UI-режиму та стратегій запису трейсів — окрема глава розділу «Інструменти автоматизації» (Playwright: дебаг і trace viewer). Тут важливо зафіксувати сам принцип: для тестів дебаг — це не лише breakpoints у коді, а й інструменти, що показують саму сторінку, у реальному часі або в записі.
Типові помилки
Breakpoint не спрацьовує — виглядає як зламаний дебагер, а насправді немає source map або ти дебажиш не той файл. Найчастіше breakpoint поставлено в .ts, а карти немає чи вона не збігається, тож дебагеру нема куди «прилипнути» — крапка стає сірою (unbound). Перевір, що source maps увімкнені й що ти запускаєш саме той файл, у якому стоїш.
console.log(obj) показує [Object] чи [Array] — виглядає як порожній об'єкт, а насправді вивід обрізано за глибиною. Це не «даних немає», це обмеження друку вкладеності. Розкрий через console.dir(obj, { depth: null }) або JSON.stringify(obj, null, 2).
У браузерному тесті об'єкт у консолі «змінюється сам» — виглядає як гонка, а насправді консоль показує живе посилання. У DevTools браузера розкритий об'єкт може підвантажувати значення в момент розгортання, а не логування, тож ти бачиш пізніший стан. Щоб зафіксувати саме поточний, логуй копію або примітив.
Логів зі сторінки не видно в терміналі — виглядає, ніби застосунок мовчить, а насправді ти не перехопив браузерну консоль. Повідомлення сторінки не течуть у Node самі — потрібна підписка page.on('console', ...).
debugger; «не зупиняє» — виглядає як баг інструкції, а насправді процес запущено без інспектора. Без під'єднаного дебагера (звичайний node file.js без --inspect) інструкція debugger; — просто no-op. І навпаки: забутий у коді debugger; під час прогону з інспектором підвісить тест, а забутий console.log — засмітить CI-вивід. Обидва — часта причина «червоного» рев'ю.
Підсумок
console— це не лишеlog:tableдля наборів даних,dirзdepth: nullдля глибоких об'єктів,error/trace/countдля діагностики; але консоль показує стан лише там, куди ти вручну вставив рядок.- Breakpoint дає повний стан у точці паузи без правок коду; умовні breakpoints і logpoints економлять найбільше часу. Під усім цим — інспектор Node на порту
9229, який--inspectвідкриває, а--inspect-brkще й ставить на паузу з першого рядка. - Source map зв'язує скомпільований JS з вихідним TS — без неї breakpoints і стек-трейси вказують не туди; ранери зазвичай вмикають карти самі, а
--enable-source-mapsполагодить трейси помилок у Node. - У браузерних тестах логи коду (Node) і логи сторінки (браузер) — різні світи; щоб бачити другі, потрібне
page.on('console', ...). - Дебаг тесту — це не лише код: headed-режим і Inspector показують сторінку в реальному часі, а trace viewer переграє впалий CI-прогін кадр за кадром і закриває клас «локально зелений, у CI червоний».
Що питають на співбесіді
Тема має низьку вагу, тож питають радше побіжно — як маркер, що кандидат реально писав і чинив код, а не лише читав про нього.
- «Як ти дебажиш тест, що падає?» Інтерв'юер перевіряє системність: чи є в кандидата підхід, окрім «насиплю
console.logі перезапущу». Сильна відповідь називає breakpoint, перегляд стану й стеку, а для браузерних тестів — headed-режим і trace viewer, і розрізняє, де баг у коді тесту, а де у взаємодії зі сторінкою. - «Чим
console.logгірший за breakpoint?» Дивляться, чи розумієш різницю:logпоказує заздалегідь обраний зріз стану й вимагає перезапуску на кожну правку; breakpoint дає весь стан одразу і не змінює код. - «Що таке source maps і навіщо вони при TypeScript?» Очікують зв'язку «виконується JS, пишемо TS»: карта перекладає позиції між ними, тому breakpoints і стек-трейси показують оригінальний код.
- «Тест зелений локально, червоний у CI — як шукатимеш причину?» Тут цінують згадку про артефакти прогону, насамперед trace viewer і скриншоти/відео як спосіб побачити чужий агент, а не наосліп перезапускати джоб.
Джерела
- MDN Web Docs — Console API: методи
log,table,dir,error,traceта інші. - Node.js — Debugging Guide: прапорці
--inspect,--inspect-brk, порт9229і протокол інспектора. - TypeScript Handbook — tsconfig
sourceMap: генерація карт коду при компіляції. - Playwright — Debugging Tests і Trace Viewer: headed-режим, Inspector,
page.pause(), запис і перегляд трейсів. - Специфікація Source Map — ECMA-426 Source Map Format (TC39): формат карти відповідності між згенерованим і вихідним кодом.
Чому «насипати console.log» — слабка стратегія дебагу, і що робити натомість?
console.log показує тільки той зріз стану, який ти вгадав наперед, а кожна нова гіпотеза коштує правки коду й перезапуску. Breakpoint працює інакше: виконання завмирає на рядку, і в цю мить доступний увесь контекст — локальні змінні, аргументи, this, стек викликів, і будь-який вираз можна порахувати в консолі дебагера. Тобто ти не передбачаєш, що вивести, а вже стоїш усередині й дивишся на все одразу. Практичний наслідок — одна пауза замінює десяток циклів «додав лог, перезапустив, прибрав». На співбесіді системна відповідь через breakpoint читається як досвід, а не переказ туторіалу.
Які методи console, крім log, ти реально використовуєш?
Найкорисніші — console.table для масиву об'єктів (виводить його як таблицю з колонками, зручно дивитися на тестові дані чи рядки з API), і console.dir для об'єкта, коли треба саму розкладку властивостей, а не текстове представлення. Для діагностики стану виконання є console.trace — він друкує стек викликів до поточної точки, тобто показує, хто нас сюди покликав. console.count('мітка') рахує, скільки разів код дійшов до рядка — швидкий спосіб зловити зайву ітерацію чи подвійний виклик обробника. Ще console.error і console.warn пишуть у stderr і візуально виділяються, тож у шумному виводі їх видно одразу.
Навіщо писати console.log({ user, token }), а не console.log(user, token)?
Обгортка у фігурні дужки друкує не лише значення, а й імена змінних поряд. Коли в терміналі десяток однотипних логів, голі значення без підпису — це головоломка «а це чиє», яку доводиться розгадувати за позицією. Механізм простий: { user, token } — це скорочений синтаксис об'єкта, тож у виводі буде видно ключ і значення. Практично це економить час на самому читанні логу: ти витрачаєш зусилля на баг, а не на розшифровку власного виводу.
У чому різниця між console.log і console.dir для об'єкта?
console.log намагається показати «людське» представлення, а console.dir завжди друкує об'єкт як дерево властивостей. У браузері різниця принципова: якщо передати DOM-елемент, console.log покаже його як HTML-розмітку, а console.dir — сам JS-об'єкт з його полями й методами. У Node console.dir бере другим аргументом опції, і саме там живе рятівний { depth: null }, який знімає обмеження глибини друку. Коротко: треба поколупати структуру об'єкта — бери dir, треба побачити елемент як розмітку — log.
console.log(obj) показує [Object] замість вмісту — що сталося і як побачити повне?
Це не «даних немає», а обмеження глибини друку: вивід згорнув вкладений об'єкт до заглушки [Object] (масив — до [Array]), бо за замовчуванням він не розкриває структуру глибше певного рівня. Легко переплутати з порожнім об'єктом і піти шукати баг не там. Розкрити можна двома способами: console.dir(obj, { depth: null }) у Node зніме ліміт вкладеності, або JSON.stringify(obj, null, 2) серіалізує все в читабельний рядок. Друге не працює з циклічними посиланнями й губить функції, тож для складних об'єктів надійніший dir.
Що таке breakpoint і які є способи покрокового проходу?
Breakpoint (точка зупину) — позначка на рядку, на якій виконання завмирає й віддає керування тобі; у цей момент видно весь стан і можна рахувати вирази. Далі паузою керуєш кількома командами. Step over виконує поточний рядок і стає на наступний, не заходячи всередину викликаних функцій. Step into навпаки заходить у функцію на поточному рядку, щоб пройти її зсередини. Step out доганяє поточну функцію до кінця й піднімає тебе на рівень вище. Continue біжить далі до наступного breakpoint. Комбінація цих кроків дає точний контроль: пропускаєш нецікаве через over, пірнаєш у підозріле через into.
Коли потрібні умовний breakpoint і logpoint?
Умовний breakpoint зупиняється тільки тоді, коли заданий вираз істинний — це рятує в циклах: дебажиш тисячу елементів, а баг на конкретному, ставиш умову item.id === 42 замість того, щоб сто разів тиснути Continue. Logpoint — це breakpoint, який не зупиняє виконання, а друкує повідомлення; по суті console.log, який не треба вписувати в код і потім не забути прибрати. Обидва економлять найбільше часу саме тому, що прибирають ручну рутину. Logpoint особливо доречний, коли лізти правками в чужий чи згенерований файл не хочеться взагалі.
Що робить інструкція debugger; і чому вона іноді «не зупиняє»?
debugger; — це вставлена прямо в код точка зупину: якщо процес запущено під дебагером, виконання завмре на ній так само, як на breakpoint з поля редактора. Пастка в тому, що без під'єднаного інспектора (звичайний запуск без --inspect) ця інструкція — просто no-op, вона нічого не робить. Тому «debugger; не працює» майже завжди означає, що процес стартував без інспектора, а не що зламалась сама інструкція. Зворотний бік теж болючий: забутий у коді debugger; під час прогону з дебагером підвісить тест намертво.
Що таке JavaScript Debug Terminal у VS Code і чим він зручний для тестів?
Це окремий термінал VS Code, який автоматично під'єднує дебагер до будь-якого Node-процесу, запущеного всередині нього. Зручність у тому, що не треба писати й підтримувати launch.json: відкрив цей термінал, поставив breakpoint у тесті, запустив звичну команду прогону — і зупинка спрацює. Для тестового проєкту це найкоротший шлях до дебагу: команда запуску та сама, що й завжди, а дебагер чіпляється сам. Механізм під ним стандартний — той самий інспектор Node, просто VS Code бере на себе під'єднання.
Що таке інспектор Node.js, на якому порту він працює і що це за протокол?
Інспектор Node.js — це вбудований сервер налагодження: коли Node стартує з прапорцем --inspect, він відкриває його на порту 9229. Спілкування йде за протоколом V8 Inspector — тим самим Chrome DevTools Protocol, яким користуються й DevTools браузера. До цього порту під'єднується клієнт: VS Code або Chrome за адресою chrome://inspect. Розуміти цей шар важливо, бо коли «дебаг не чіпляється», причина майже завжди тут — не той порт, не той процес, або ранер запускає тести в окремому воркері, до якого інспектор головного процесу не дотягується.
У чому різниця між --inspect і --inspect-brk?
Обидва відкривають інспектор, але по-різному стартують виконання. --inspect відкриває порт і одразу біжить далі — якщо баг на самому старті програми, ти просто не встигнеш під'єднатися. --inspect-brk відкриває інспектор і завмирає на першому ж рядку, чекаючи, поки під'єднається дебагер. Друге — саме те, що треба для проблем в ініціалізації або в короткому скрипті, який відпрацьовує за мілісекунди й закінчується раніше, ніж ти встиг щось зробити. Правило просте: баг «на початку» — --inspect-brk, баг «десь у процесі» — --inspect.
Що таке source map і навіщо вона при TypeScript?
Ні браузер, ні Node не виконують TypeScript — його спершу компілюють у JavaScript, тож реально біжить test.js, а пишеш і хочеш дебажити test.ts. Source map (карта коду) — це файл (зазвичай test.js.map) або вбудований коментар, який зіставляє позиції: «рядок такий-то в JS відповідає рядку такому-то в TS». Дебагер читає цю карту й перекладає позиції в обидва боки, тому breakpoint у .ts «прилипає», а зупинка виглядає теж у .ts, хоча під капотом крутиться JS. Без карти breakpoint у TS не мав би куди чіплятися, а стек-трейс помилки показував би номери рядків згенерованого JS, які не збігаються з твоїм джерелом.
У тестовому проєкті source maps треба налаштовувати руками?
Здебільшого ні. Сучасні ранери й швидкі транспілятори — esbuild, tsx, swc — вмикають карти самі, тому в Playwright стек-трейс помилки й breakpoints одразу вказують на .ts без жодного налаштування. Якщо ж компіляція йде класичним tsc, за карти відповідає опція sourceMap: true у tsconfig.json. Практичний наслідок: коли карти «є за замовчуванням», зайве їх чіпати, а от коли breakpoint став сірим (unbound), варто перевірити, чи вони взагалі генеруються й чи не застаріли після зміни збірки.
Що робить прапорець --enable-source-maps у Node?
Він вмикає підтримку карт для стек-трейсів помилок. За замовчуванням, коли Node виконує скомпільований JS і падає, трейс показує рядки саме .js — тобто згенерованого коду, у якому важко орієнтуватися. З --enable-source-maps номери рядків у помилці перекладаються на оригінальний TS. Це різниця між «помилка десь у чужому згенерованому файлі» і «помилка в моєму login.page.ts, рядок 40». Важливо не плутати цей прапорець з breakpoints: карти для дебагера й карти для трейсів — суміжні, але вмикаються різними механізмами.
Чому console.log зі сторінки застосунку не видно в терміналі тесту?
Бо логи тестового коду й логи сторінки — це два різні світи. console.log у твоєму .ts-файлі виконується в Node і йде в термінал, звідки ти запустив прогін. А console.log, який пише сама сторінка (код застосунку в браузері), у Node не тече сам собою — його треба явно перехопити. У Playwright це підписка на подію: page.on('console', msg => ...) для повідомлень і page.on('pageerror', err => ...) для помилок сторінки. Часто це найшвидший спосіб зрозуміти, що застосунок сам лаявся в консоль, а ти цього просто не бачив.
Чим headed-режим і Playwright Inspector допомагають у дебагу тесту?
Багато проблем браузерного тесту не в логіці коду, а у взаємодії з реальною сторінкою: елемент ще не з'явився, перекритий іншим, локатор чіпляє не той вузол — і покроковий дебагер у Node тут мало допомагає, бо треба бачити саму сторінку. Headed-режим (--headed) запускає видиме вікно браузера замість фонового headless. --debug (або змінна PWDEBUG=1) додатково відкриває Playwright Inspector — панель, де тест іде покроково й підсвічує локатори прямо на сторінці. А page.pause(), вставлений у тест, зупиняє прогін у цій точці й дає поклацати локатори вручну, перевіряючи, що саме вони знаходять.
Що таке trace viewer і чому це головний інструмент для «локально зелено, у CI червоно»?
Trace viewer — це post-mortem-дебаг: Playwright записує трасування прогону (снапшоти DOM, усі дії, мережеві запити, консоль, час), і потім ти відкриваєш його як «машину часу» через npx playwright show-trace. Наводишся на будь-яку дію — і бачиш стан сторінки саме в той момент, тобто переграєш впалий тест кадр за кадром. Це розв'язує найболючіший клас проблем AQA: ти не можеш відтворити фейл на своїй машині, але маєш повний запис того, що сталося на агенті — видно, що кнопку перекрив банер, що запит повернув 500 або що сторінка ще крутила спінер. Записується трейс за конфігом (trace: 'on-first-retry' — типовий вибір: писати лише при першому перезапуску), і саме тому в CI його варто вмикати завжди.
Тест зелений локально, червоний у CI — як системно шукатимеш причину?
Спершу класифікую, де взагалі проблема: у логіці коду тесту, у взаємодії з UI чи в самому середовищі. Якщо баг у логіці — ставлю breakpoint у VS Code і дивлюся стан у точці паузи. Якщо у взаємодії зі сторінкою — запускаю headed з Inspector або ставлю page.pause(), щоб побачити реальну сторінку. Для суто CI-фейлу головний доказ — trace viewer: він показує чужий агент зсередини, тож не треба наосліп перезапускати джоб. Головна дисципліна — не заявляти причину без доказу: не «схоже на флак», а «на трейсі видно, що кнопку перекрив cookie-банер, клік пішов не туди».
Три кейси з робочого життя AQA: як за симптомом фейлу обрати інструмент дебагу, як витягти читабельний стан із console, коли log замало, і як побачити те, що застосунок ховає в браузерній консолі й у CI-прогоні. Скрізь — що дивитися і чому.
Кейс 1. Матриця «симптом → інструмент»: з чого починати
Найгірше — гатити наосліп: додати console.log, перезапустити, прибрати, додати ще. Спершу класифікуй, де взагалі проблема, і бери відповідний інструмент. Ось як це лягає в таблицю рішень.
| Симптом фейлу | Де шукати | Інструмент |
|---|---|---|
Асерт біжить по undefined, значення «не те» | логіка коду тесту | breakpoint у VS Code, Step into |
| Баг у короткому скрипті на самому старті | ініціалізація | node --inspect-brk |
| Локатор знаходить не той елемент | взаємодія з UI | headed + Inspector, page.pause() |
| Застосунок «мовчить», хоча має лаятись | консоль сторінки | page.on('console' / 'pageerror') |
| Зелено локально, червоно в CI | чужий агент | trace viewer |
Breakpoint у .ts став сірим | збірка / карти | перевірити source maps |
Що дивитися і чому:
- Спершу питання «де проблема», а не «який рядок додати». Одна класифікація економить години: покроковий дебагер безсилий проти перекритої кнопки, а trace viewer нічого не скаже про логічну помилку в чистій функції.
undefinedв асерті — класичний загубленийawait. Breakpoint на рядку асерту й погляд на значення до порівняння швидко покаже, що проміс не дочекалися, а не що дані «зникли».- «Зелено локально» майже ніколи не дебажиться локально. Відтворити фейл на своїй машині не вийде за побудовою — потрібен запис того, що сталося на агенті, тобто трейс.
Кейс 2. Коли console.log замало: table, dir, count
Тест перевіряє набір користувачів з API, а log вивалює нечитабельну кашу, і вкладені поля згорнуті в [Object]. Три методи роблять вивід придатним для читання.
const users = [
{ id: 1, name: 'Ada', role: 'admin', profile: { city: 'Kyiv' } },
{ id: 2, name: 'Bob', role: 'user', profile: { city: 'Lviv' } },
];
// таблиця з потрібними колонками — не тонемо в зайвих полях
console.table(users, ['name', 'role']);
// повний вміст вкладеного об'єкта: log обрізав би profile до [Object]
console.dir(users[0], { depth: null });
А ось діагностика «скільки разів код сюди дійшов» — коли є підозра на подвійний виклик обробника чи зайву ітерацію:
function handleResponse(res: unknown) {
console.count('handleResponse'); // handleResponse: 1, handleResponse: 2 ...
// ...
}
Що дивитися і чому:
[Object]у виводі — це ліміт глибини, а не порожнеча. Легко переплутати з «даних немає» й піти шукати баг не там;console.dir(obj, { depth: null })абоJSON.stringify(obj, null, 2)розкриють вміст.console.tableекономить саме читання. Для масиву об'єктів колонки з підписами швидші за очі, ніж потік однотипних рядків; другий аргумент відсіює зайві поля.console.countловить кратність. Якщо лічильник росте вдвічі швидше за очікуване — знайдено подвійну підписку чи зайвий рендер, і це видно без жодного breakpoint.- Дисципліна: усе це — тимчасовий дебаг. Перед комітом його прибирають, інакше вивід прогону засмічується й реальний сигнал губиться серед сотні рядків.
Кейс 3. Playwright: перехопити консоль сторінки, зупинитись, лишити трейс
Тест падає на асерті видимості, але сам застосунок нічого не каже в термінал — бо його консоль живе в браузері, а не в Node. Спершу перехоплюємо її, щоб побачити, чи не лаявся застосунок сам.
import { test, expect } from '@playwright/test';
test('дашборд показує нову фічу', async ({ page }) => {
// логи й помилки СТОРІНКИ течуть у термінал поряд з логами тесту
page.on('console', (msg) => console.log(`[browser] ${msg.type()}: ${msg.text()}`));
page.on('pageerror', (err) => console.log(`[browser error] ${err.message}`));
await page.goto('https://app.example.com/dashboard');
// зупинка для ручного клацання локаторів; прибрати перед комітом
// await page.pause();
await expect(page.getByTestId('new-feature-button')).toBeVisible();
});
Часто вже перший рядок [browser error] показує, що на сторінці впав JS і кнопка просто не відмалювалась — тоді баг у застосунку, а не в локаторі. Якщо ж треба поколупати локатори руками, розкоментовуємо page.pause() і запускаємо в headed:
npx playwright test --headed --debug
Для CI-фейлу, який не відтворюється локально, головне — не логи, а запис. Вмикаємо трейс у конфігу й потім переграємо прогін:
// playwright.config.ts
export default {
use: { trace: 'on-first-retry' }, // писати трейс лише при першому перезапуску
};
npx playwright show-trace trace.zip
Що дивитися і чому:
- Логи тесту й логи сторінки — різні світи.
console.logу.tsіде в Node, а повідомлення сторінки безpage.on('console', ...)у термінал не потраплять; половина «мовчазних» фейлів насправді криклива, просто крик не перехопили. page.pause()показує реальну сторінку. Inspector підсвічує, що саме знаходить локатор — часто виявляється, що він чіпляє не той вузол або елемент ще перекритий банером.trace: 'on-first-retry'— розумний компроміс. Писати трейс на кожен тест дорого, а на першому ретраї — рівно там, де фейл уже стався; у trace viewer видно снапшот DOM, мережу й консоль у момент падіння.- Трейс — це доказ замість «здається». Замість наосліп перезапускати джоб, наводишся на впалу дію й бачиш: кнопку перекрив cookie-банер, запит повернув 500, сторінка ще крутила спінер.
console — вивід під контролем
- Розумію, чому
console.logпоказує лише заздалегідь обраний зріз стану й вимагає перезапуску на кожну правку. - Можу пояснити різницю
console.logvsconsole.dirдля DOM-елемента: розмітка проти дерева властивостей. - Знаю, що
[Object]/[Array]у виводі — це ліміт глибини друку, а не порожнеча; розкриваю черезconsole.dir(obj, { depth: null })чиJSON.stringify. - Знаю призначення
console.table(масив об'єктів колонками),console.trace(стек викликів),console.count(лічильник влучань у рядок).
Breakpoints і покрокове виконання
- Можу пояснити, чому breakpoint дає весь стан у точці паузи без правок коду, а
console.log— ні. - Знаю різницю
Step over/Step into/Step outі коли яку команду тиснути. - Розумію, навіщо умовний breakpoint у циклі (
item.id === 42) і чим logpoint кращий за вписаний у кодconsole.log. - Знаю, що
debugger;без під'єднаного інспектора — no-op, а забутий у коді під дебагером підвісить тест. - Розумію, чим зручний JavaScript Debug Terminal у VS Code: чіпляє дебагер до Node-процесу без
launch.json.
Node-інспектор під капотом
- Знаю, що під VS Code працює інспектор Node.js на порту
9229за протоколом V8 Inspector (він же Chrome DevTools Protocol). - Можу пояснити різницю
--inspect(біжить одразу) vs--inspect-brk(завмирає на першому рядку до під'єднання).
Source maps: TS vs JS
- Розумію, що виконується скомпільований JS, а пишемо й дебажимо TS, і зв'язок між ними дає source map.
- Знаю, що ранери (esbuild, tsx, swc) зазвичай вмикають карти самі, а класичний
tsc— черезsourceMap: true. - Розумію, що
--enable-source-mapsлагодить саме стек-трейси помилок у Node, показуючи рядки TS замість JS. - Знаю, що сірий (unbound) breakpoint — сигнал відсутньої/застарілої карти або дебагу не того файлу.
Логи в тестах
- Розумію, що логи тестового коду (Node → термінал) і логи сторінки (браузер) — різні світи.
- Знаю, що логи сторінки треба перехоплювати явно:
page.on('console', ...)іpage.on('pageerror', ...).
Дебаг браузерного тесту: headed і trace viewer
- Знаю, що дає headed-режим (
--headed),--debug/PWDEBUG=1(Playwright Inspector) іpage.pause(). - Можу пояснити, що trace viewer — post-mortem «машина часу» (снапшоти DOM, дії, мережа, консоль), і чому в CI трейс вмикають завжди.
Чим breakpoint принципово кращий за console.log для дебагу?
Питання
Чому console.log — слабша стратегія дебагу за breakpoint?