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

    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, хоча під капотом виконується інший файл.

    помилка вказує
    на рядок JS

    карта перекладає
    рядок JS у рядок TS

    Пишеш
    test.ts

    Компілятор
    tsc / esbuild

    Виконується
    test.js

    test.js.map
    карта відповідності

    Дебагер

    Бачиш рядок
    у test.ts

    помилка вказує
    на рядок JS

    карта перекладає
    рядок JS у рядок TS

    Пишеш
    test.ts

    Компілятор
    tsc / esbuild

    Виконується
    test.js

    test.js.map
    карта відповідності

    Дебагер

    Бачиш рядок
    у test.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 у коді, а й інструменти, що показують саму сторінку, у реальному часі або в записі.

    логіка в коді тесту

    взаємодія з UI

    фейл лише в CI

    Тест упав

    Де проблема?

    Breakpoint у VS Code

    headed + Inspector,
    page.pause()

    Trace viewer:
    снапшоти, мережа, консоль

    Бачиш стан у момент паузи

    логіка в коді тесту

    взаємодія з UI

    фейл лише в CI

    Тест упав

    Де проблема?

    Breakpoint у VS Code

    headed + Inspector,
    page.pause()

    Trace viewer:
    снапшоти, мережа, консоль

    Бачиш стан у момент паузи

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

    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): формат карти відповідності між згенерованим і вихідним кодом.