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

    08 · JavaScript/TypeScript для AQA

    Рядки, дати й регулярні вирази

    Зміст

    Рядки, дати й числа — це те, що автотест перевіряє щодня: чи показує поле правильний формат телефону, чи збігається дата в API з тим, що на екрані, чи округлилася сума в чеку. Три теми, які на вигляд елементарні, а насправді дають найпідступніші баги в тестовому коді: регулярний вираз, який матчить забагато; дата, що «переходить» на інший день між твоєю машиною і CI; і сума 0.1 + 0.2, яка чомусь не дорівнює 0.3.

    Ця глава — про механіку цих трьох інструментів саме під кутом тестування. Не «як влаштований рядок у пам'яті», а «чому твоя асерція зелена на застарілих даних», «чому тест падає лише о 23:50» і «як правильно порівняти гроші». Вага теми на співбесідах середня, але питання про таймзони й toFixed ставлять регулярно — бо саме на них видно, чи людина розуміє, що робить її код, чи просто копіює зі Stack Overflow.

    Методи рядків

    Головне, що треба засвоїти про рядок у JavaScript: він незмінний (immutable). Жоден метод не змінює рядок на місці — усі повертають новий рядок. Тому text.trim() без присвоєння результату не робить нічого; це класична помилка новачка.

    Для асерцій найкорисніші кілька методів:

    МетодЩо робитьТипове застосування в тесті
    includes(sub)чи є підрядок, повертає booleanперевірка фрагмента повідомлення
    startsWith / endsWithпрефікс / суфіксвалідація формату, розширення файлу
    trim()прибирає пробіли по краяхтекст із UI майже завжди «брудний»
    toLowerCase / toUpperCaseзміна регіструпорівняння без урахування регістру
    slice(start, end)вирізає частинудістати шматок за позицією
    split(sep)ділить на масиврозібрати CSV-рядок, шлях, дату
    replace / replaceAllзамінанормалізація тексту перед звіркою
    padStart / padEndдоповнення до довжиниформатування чисел, "7""07"

    includes віддає boolean, тому в тестах він читабельніший за старий indexOf(...) !== -1 (той повертає позицію або -1). Текст, отриманий з інтерфейсу, майже завжди варто пропустити через trim() перед порівнянням — верстка легко додає невидимі пробіли й переноси рядків.

    const raw = await page.locator('.order-total').innerText(); // "  Разом: 1 234,50 грн  "
    const text = raw.trim();
    expect(text.startsWith('Разом')).toBeTruthy();
    expect(text.includes('1 234,50')).toBeTruthy();

    Одна пастка глибше рівня джуна, але про неї корисно знати: length рахує не «символи», а одиниці кодування UTF-16. Емодзі та деякі рідкісні символи займають дві одиниці, тому '😀'.length дорівнює 2, а не 1. Якщо треба порахувати саме видимі символи, беруть ітератор: [...'😀'].length дасть 1. Так само порівняння рядків з діакритикою (наприклад, é як один символ проти e плюс комбінований наголос) може несподівано провалитись — рятує normalize('NFC') перед звіркою.

    Шаблонні рядки

    Шаблонні літерали (template literals) пишуться у зворотних лапках і дозволяють вставляти вирази прямо в текст через синтаксис долара з фігурними дужками, а також робити багаторядкові рядки без конкатенації. Для тестового коду це щоденний інструмент: динамічні URL, повідомлення про помилку з контекстом, генерація унікальних тестових даних.

    const userId = 42;
    const env = process.env.BASE_URL ?? 'https://staging.example.com';
    
    await page.goto(`${env}/users/${userId}/profile`);
    
    const uniqueEmail = `qa+${Date.now()}@example.com`; // унікальний на кожен прогін

    Зворотні лапки читабельніші за склейку через плюс і не сплутають типи: усе всередині приводиться до рядка. Але саме тут ховається дрібна пастка — об'єкт у шаблоні перетвориться на [object Object], а не на свій вміст; для об'єктів потрібен JSON.stringify. Про приведення типів детальніше — у главі «Змінні, типи даних і приведення типів».

    Регулярні вирази: синтаксис, флаги, test і match

    Регулярний вираз (regular expression, regex) — це шаблон для пошуку й перевірки тексту. У JavaScript його створюють двома способами: літералом між слешами /шаблон/флаги або через конструктор new RegExp('шаблон', 'флаги'). Літерал зручніший для сталих шаблонів; конструктор потрібен, коли шаблон збирається з рядка динамічно.

    Мінімальний синтаксис, якого вистачає для 90% задач AQA:

    КонструкціяЗначення
    \d \w \sцифра / буквено-цифровий символ / пробіл
    .будь-який символ (крім переносу рядка)
    ^ $початок / кінець рядка
    * + ?нуль-або-більше / один-або-більше / нуль-або-один
    a{2,4}від 2 до 4 повторень
    [abc] [^abc]один із набору / будь-який, крім набору
    (...)група із захопленням
    a|b«або»

    Флаги (прапорці) змінюють поведінку всього виразу:

    • g — глобальний пошук, знаходить усі збіги, а не лише перший;
    • i — без урахування регістру;
    • m — багаторядковий режим: ^ і $ матчать межі кожного рядка;
    • s — крапка починає матчити й переноси рядків;
    • u — коректна робота з Unicode.

    Два методи, які використовують найчастіше:

    • regex.test(str) повертає boolean — «чи є збіг». Це основний інструмент валідації формату.
    • str.match(regex) повертає збіги. Без флага g — перший збіг разом із захопленими групами; з флагом g — масив усіх збігів без груп. Коли потрібні і всі збіги, і групи, беруть str.matchAll(regex) (обов'язково з g), що повертає ітератор.
    const isIsoDate = /^\d{4}-\d{2}-\d{2}$/;
    expect(isIsoDate.test('2026-07-18')).toBe(true);
    expect(isIsoDate.test('18.07.2026')).toBe(false);
    
    // дістати id із URL через захоплену групу
    const url = 'https://app.example.com/orders/10582/items';
    const orderId = url.match(/orders\/(\d+)/)?.[1]; // "10582"

    Головна пастка regex — жадібність (greedy). Квантифікатори за замовчуванням хапають якнайбільше. Вираз /".*"/ на рядку "a" та "b" захопить усе від першої до останньої лапки, а не окреме "a". Щоб зробити квантифікатор «лінивим», додають ?: /".*?"/. Друга типова пастка стосується test із флагом g: об'єкт regex запам'ятовує позицію останнього збігу в lastIndex, і повторні виклики test на тому самому об'єкті даватимуть різний результат. Для простої валідації флаг g просто не потрібен.

    Regex у роботі AQA: асерції, локатори, валідація

    Три місця, де regex реально економить час тестувальнику.

    Асерції за патерном, а не за точним рядком. Коли частина тексту динамічна (число, дата, id), точне порівняння крихке. Playwright приймає regex у більшості текстових асерцій:

    // зайве уточнення: точний текст зламається на будь-якій новій сумі
    await expect(page.locator('.balance')).toHaveText(/^Баланс: \d[\d\s]*,\d{2} грн$/);
    
    // "замовлення №<будь-яке число> створено"
    await expect(page.locator('.toast')).toContainText(/замовлення №\d+ створено/i);

    Локатори за текстом. page.getByText(/.../) і page.getByRole('button', { name: /зберегти/i }) дозволяють знаходити елемент за гнучким шаблоном — корисно, коли текст містить змінну частину або коли треба ігнорувати регістр. Про вибір елементів за текстом і стабільність селекторів загалом — глава «DOM, селектори та події».

    Валідація формату тестових даних. Regex зручний, щоб перевірити, що згенерований email, телефон чи ISO-дата виглядають правильно, перш ніж відправляти їх в API.

    Тут важлива тверезість: не намагайтеся описати регуляркою те, що має складну специфікацію. Канонічний приклад — email: «правильний» вираз за стандартом настільки монструозний, що на практиці перевіряють лише наявність @ і крапки, а решту віддають бекенду. Для перевірки структури JSON-відповіді regex теж не інструмент — там працюють зі схемою (JSON Schema, zod), а розбір даних детальніше розглянуто в главі «Об'єкти, масиви та методи роботи з даними». Regex — для рядків простої, регулярної структури, не для вкладених даних.

    Date та ISO 8601

    Об'єкт Date усередині — це одне число: кількість мілісекунд від «епохи» (1 січня 1970 року, 00:00 UTC). Усе інше — методи, що це число форматують чи розбирають.

    Найнадійніший спосіб задати дату — стандарт ISO 8601: 2026-07-18T10:30:00.000Z. Літера Z наприкінці означає UTC («зулу-час»). Специфікація мови гарантує, що рядок у форматі ISO 8601 буде розібрано однаково в будь-якому рушії. Будь-який інший формат ("07/18/2026", "18.07.2026") розбирається на розсуд конкретного рушія — десь спрацює, десь дасть Invalid Date, десь переплутає день і місяць. Правило просте: в API і тестах передавайте й порівнюйте дати в ISO 8601.

    const d = new Date('2026-07-18T10:30:00.000Z');
    d.getTime();        // 1784...000 — мілісекунди, зручно порівнювати
    d.toISOString();    // "2026-07-18T10:30:00.000Z" — назад у канонічний рядок
    
    // порівняння дат — через getTime(), а не через самі об'єкти
    const a = new Date('2026-07-18T00:00:00Z');
    const b = new Date('2026-07-18T00:00:00Z');
    expect(a.getTime()).toBe(b.getTime()); // === на об'єктах Date завжди false

    Дві вбудовані пастки Date. Перша: у конструкторі new Date(2026, 6, 18) місяць рахується від нуля, тобто 6 — це липень, а не червень. Так само getMonth() повертає число від 0 до 11. Друга: Date змінний — метод setDate мутує сам об'єкт, а не повертає новий, тож спільний об'єкт дати легко зіпсувати між тестами.

    Стандартний Date незручний для арифметики й форматування, тому в реальних проєктах часто беруть бібліотеку (date-fns, Luxon, Day.js). У мову також додають сучасніший API Temporal — але це вже поза межами джуна.

    Таймзони і «тест падає лише вночі»

    Це найчастіше «магічне» падіння, пов'язане з датами. Симптом: тест зелений цілий день, а вночі — або в CI — раптом червоний, хоч код не мінявся.

    Причина в тому, що одна й та сама мить часу має різну календарну дату залежно від таймзони. toISOString() завжди віддає час у UTC, а локальні методи (getDate(), getHours(), toLocaleString()) — у таймзоні тієї машини, де виконується код. Твій ноутбук у Києві живе в UTC+2/+3 і випереджає UTC, а CI-агент майже завжди в самому UTC. Тому в перші години після місцевої півночі локальна календарна дата вже нова, а в UTC ще триває попередня доба: о 01:30 за Києвом у UTC це ще 18 липня, 22:30. Асерція, яка порівнює «локальну» дату з датою в UTC, збігається вдень і розходиться в ці нічні години.

    вдень збігається

    вночі розходиться

    Одна мить: 19 лип, 01:30 за Києвом

    UTC-представлення:
    18 липня 22:30Z

    Локальне представлення
    на машині в Києві:
    19 липня

    toISOString → бере '2026-07-18'

    getDate/getFullYear → бере '2026-07-19'

    Асерція порівнює
    ці два рядки

    Зелено

    Червоно — 'падає лише вночі'

    вдень збігається

    вночі розходиться

    Одна мить: 19 лип, 01:30 за Києвом

    UTC-представлення:
    18 липня 22:30Z

    Локальне представлення
    на машині в Києві:
    19 липня

    toISOString → бере '2026-07-18'

    getDate/getFullYear → бере '2026-07-19'

    Асерція порівнює
    ці два рядки

    Зелено

    Червоно — 'падає лише вночі'

    Як лікувати, за зростанням надійності:

    • Порівнюйте в одній системі відліку. Якщо бекенд віддає UTC, то й у тесті беріть toISOString() чи getTime(), а не локальні getDate/getHours. Не змішуйте локальний і UTC-час в одній асерції.
    • Фіксуйте («заморожуйте») час. Тест, що залежить від «сьогодні», недетермінований за визначенням. Playwright має page.clock для підміни часу в браузері; у юніт-тестах час мокають (наприклад, фейковими таймерами). Тоді «сьогодні» завжди те саме, і межа доби не має значення.
    • Не обчислюйте очікувану дату тим самим кодом, що й продукт — інакше тест повторить його баг. Краще підставити дату як фікстуру.
    // Playwright: зафіксувати час, щоб тест не залежав від реального «зараз»
    await page.clock.setFixedTime(new Date('2026-07-18T09:00:00Z'));

    Ця ж тема — дати, таймзони, межі — розглядається під кутом тест-дизайну в розділі «Тест-дизайн» (тестові дані та граничні значення) і під кутом бази даних у розділі «Бази даних і SQL» (тип TIMESTAMP з таймзоною і без).

    Числа: parseInt, toFixed і чому 0.1 + 0.2 не дорівнює 0.3

    parseInt(str, radix) розбирає ціле число з початку рядка й зупиняється на першому нецифровому символі: parseInt('10px', 10) дасть 10, а parseInt('px10', 10)NaN. Завжди передавайте другий аргумент — основу системи числення (10). Без нього рядок, що починається з 0x, буде розібрано як шістнадцятковий; історично провідний нуль міг означати вісімкове число. Якщо потрібне дробове — це parseFloat; якщо треба сувора перевірка «весь рядок є число» — краще Number(str), що на смітті дає NaN замість мовчазного обрізання.

    toFixed(n) округлює до n знаків після коми й повертає рядок, а не число. Це часта пастка: (2).toFixed(2) дає "2.00", і подальше === 2 провалиться. Друга тонкість — округлення тут не завжди «математичне» через двійкове представлення: (1.005).toFixed(2) дає "1.00", а не "1.01", бо 1.005 насправді зберігається як трохи менше число.

    І головне питання співбесід: чому 0.1 + 0.2 !== 0.3? Бо числа в JavaScript — це числа з рухомою комою подвійної точності за стандартом IEEE 754 (той самий тип, що double). У двійковій системі дроби 0.1 і 0.2 не мають точного представлення — так само, як 1/3 не записати скінченним десятковим дробом. Тому вони зберігаються з крихітною похибкою, і сума дає 0.30000000000000004. Це не баг JavaScript — так само поводяться майже всі мови з типом double.

    expect(0.1 + 0.2).toBe(0.3);          // ПРОВАЛИТЬСЯ: 0.30000000000000004
    expect(0.1 + 0.2).toBeCloseTo(0.3);   // OK: порівняння з допуском

    Практичні наслідки для тестів:

    • Ніколи не порівнюйте дробові числа на точну рівність. У Playwright/Jest для цього є toBeCloseTo, у ручній логіці — перевірка, що різниця менша за малий поріг (epsilon).
    • Гроші не тримають у float. Правильно — рахувати в найдрібнішій одиниці (копійки, центи) цілими числами або спеціальним decimal-типом. Тому й у базі суму зберігають як DECIMAL, а не FLOAT (деталі — у розділі «Бази даних і SQL»).
    • toFixed — для показу, не для розрахунків. Якщо порівнюєте суму з UI, порівнюйте або рядок з рядком (обидва форматовані), або число з числом (обидва як числа), не змішуючи.

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

    • Виглядає як «trim() не працює», а насправді ви не присвоїли результат. Рядок незмінний — усі методи повертають новий, а старий лишається брудним.
    • Виглядає як баг продукту «дата на день раніше», а насправді тест змішав UTC і локальний час. Продукт віддає UTC, тест дістав getDate() у місцевій таймзоні — і на межі доби вони розходяться.
    • Виглядає як «regex не знаходить», а насправді флаг g лишив lastIndex посеред рядка від попереднього виклику test, або квантифікатор виявився жадібним і захопив зайве.
    • Виглядає як «сума неправильна», а насправді toFixed повернув рядок, і ви порівнюєте "2.00" з числом 2 — типи різні, асерція падає.
    • Виглядає як «математика зламалась», а насправді це IEEE 754: 0.1 + 0.2 дає 0.30000000000000004, і точна рівність з 0.3 неможлива.
    • Виглядає як «валідація email пропускає сміття», а насправді ви намагаєтесь однією регуляркою покрити те, що має складну специфікацію; таку перевірку лишають бекенду.

    Підсумок

    • Рядки незмінні: методи повертають новий рядок, тож результат треба присвоювати; текст із UI перед звіркою trim-те.
    • Regex test дає boolean для валідації, match/matchAll — для витягання; стережіться жадібності та стану lastIndex при флазі g.
    • Передавайте й порівнюйте дати в ISO 8601 та в одній системі відліку (UTC); не змішуйте локальні й UTC-методи.
    • «Тест падає лише вночі» — майже завжди таймзони; лікується фіксацією часу й порівнянням у UTC, а не затримками.
    • Дробові числа мають похибку IEEE 754: не порівнюйте їх на точну рівність, гроші рахуйте цілими, toFixed повертає рядок і потрібен лише для показу.

    Що питають на співбесіді

    • «Чому 0.1 + 0.2 !== 0.3 — найпоширеніше. Інтерв'юер хоче почути про двійкове представлення й IEEE 754, а не «це баг JS». Сильна відповідь одразу додає, як із цим жити: toBeCloseTo, гроші в копійках.
    • «Твій тест зелений локально, а в CI червоний і тільки для дат — у чому річ?» — перевірка розуміння таймзон. Очікують згадку UTC проти локального часу, CI в UTC і фіксацію часу як рішення.
    • «Чим test відрізняється від match? Що поверне match із флагом g і без?» — базове знання regex API; заразом можуть спитати про жадібні квантифікатори.
    • «Як перевірити, що рядок відповідає формату дати / телефону?» — тут дивляться і на вміння написати простий regex, і на тверезість не переускладнювати (email — не регуляркою).
    • «parseInt('08')? (2).toFixed(2) — це число чи рядок?» — маленькі пастки на уважність; головне — не плутати, що toFixed повертає рядок.

    Інтерв'юер за цими питаннями перевіряє не знання синтаксису напам'ять, а чи розумієте ви, чому код поводиться саме так, — бо саме нерозуміння механіки й породжує флакі-тести на датах і числах.

    Джерела

    Ця глава — програмний фундамент, а не розділ силабусу ISTQB CTFL 4.0; аспекти тестових даних (дати, таймзони, межі, спецсимволи) розкриває розділ «Тест-дизайн» (тестові дані й граничні значення).