08 · JavaScript/TypeScript для AQA
Live coding для AQA: типові задачі та підходи
Зміст
Live coding — це формат, де тебе просять розвʼязати невелику задачу прямо в редакторі чи на дошці, вголос пояснюючи, що робиш. Для AQA-вакансій це майже завжди не «напиши алгоритм Дейкстри», а щось приземлене: розверни рядок, знайди дублікати в масиві, згрупуй тестові дані, зроби retry для флакі-запиту. Задачі прості, але саме на них валяться кандидати — бо хвилюються, забувають про крайові випадки й пишуть код, який на першому ж порожньому масиві падає.
Головна пастка live coding у тому, що оцінюють не сам розвʼязок, а те, як ти мислиш. Той самий FizzBuzz можна зробити за 30 секунд і отримати «ну ок», а можна за три хвилини — уточнивши вимоги, проговоривши межі й перевіривши результат — і отримати «беремо». Ця глава показує канонічні задачі й, головне, патерн міркування, який перетворює написання коду на демонстрацію тестувальницького мислення. Мова прикладів — TypeScript, бо саме її очікують на JS/TS-стеку; синтаксис методів масивів і рядків розібрано в главах Обʼєкти, масиви та методи роботи з даними і Рядки, дати й регулярні вирази.
Що насправді оцінюють
Інтервʼюер дивиться не тільки на робочий код. За той самий час він зчитує чотири речі, і три з них — не про алгоритми:
- Чи уточнюєш вимоги. Задачу формулюють навмисно неповно. «Розверни рядок» — а що з емодзі? З порожнім рядком? Реверс за символами чи за словами? Кандидат, який одразу питає, показує зрілість; кандидат, який кинувся кодити, показує, що так само кинеться писати автотест без розуміння очікуваного результату.
- Чи бачиш крайові випадки. Це прямий проксі твоєї основної навички. Порожнє,
null, дублікати, одиничний елемент, дуже великий вхід, unicode — те саме, що ти щодня закладаєш у тест-дизайн (класи еквівалентності й граничні значення живуть у розділі «Тест-дизайн»). - Чи пояснюєш вголос. Мовчазний геній, який за пʼять хвилин видає ідеальний код без єдиного слова, програє тому, хто думає вголос. Автоматизатор постійно пояснює рішення на код-рев'ю — інтервʼю це репетирує.
- Чи перевіряєш себе. Дописав — і одразу прогнав подумки на кількох входах, зокрема на крайових. Це рефлекс тестувальника, і його видно неозброєним оком.
Тому далі кожну задачу подано не як «ось відповідь», а разом із межами, які варто проговорити.
Патерн міркування: вимоги → крайові випадки → рішення → перевірка
Це кістяк, на який нанизується будь-яка задача. Він же — те, чим QA відрізняється від розробника на тому самому завданні: розробник біжить до робочого коду, тестувальник спершу окреслює простір входів.
Проговори це буквально такими словами на співбесіді:
- Вимоги. «Уточню: вхід — це завжди рядок? Реверс посимвольний? Регістр важливий?» Дві-три хвилини тут економлять переписування потім.
- Крайові випадки. Назви їх до коду: порожній вхід, один елемент, дублікати, від'ємні числа, дуже великий масив,
undefinedсеред даних. Навіть якщо не оброблятимеш усі — покажи, що бачиш. - Рішення. Спершу найпростіше, що працює й читається. Не оптимізуй передчасно; якщо спитають про складність — тоді й обговориш.
- Перевірка. Прогони код подумки на happy-path і на кількох краях. Вголос: «порожній рядок → повертає порожній, ок; один символ → сам себе, ок».
Далі — задачі, кожна крізь цю призму.
Рядки: реверс, паліндром, частотний словник
Реверс рядка. Класика, у якій ховається справжнє питання про unicode.
function reverse(input: string): string {
return [...input].reverse().join('');
}
Чому [...input], а не input.split('')? Спред перебирає рядок за код-поінтами (Unicode code points), тож емодзі й символи з сурогатних пар не розваляться навпіл. split('') ріже за 16-бітними одиницями й на "👍" дасть кашу. Проговорити цю різницю — і задача-«розминка» вже показала, що ти знаєш, де в рядках болить. Крайові випадки: порожній рядок (повертає порожній), один символ, рядок із комбінованими діакритиками (тут навіть код-поінти не рятують — але про це чесно сказати як про межу знань).
Паліндром. Питання-пастка — не «як порівняти з реверсом», а «що вважати паліндромом». Майже завжди мають на увазі: ігнорувати регістр, пробіли й пунктуацію.
function isPalindrome(input: string): boolean {
const normalized = input.toLowerCase().replace(/[^a-z0-9]/gi, '');
let left = 0;
let right = normalized.length - 1;
while (left < right) {
if (normalized[left] !== normalized[right]) return false;
left++;
right--;
}
return true;
}
Два вказівники замість normalized === [...normalized].reverse().join('') — не заради швидкості (обидва читаються нормально), а щоб показати, що ти можеш і без «магії» методів. Але якщо напишеш через реверс і поясниш — теж валідно. Крайові: порожній рядок (зазвичай вважають паліндромом), один символ, "A man, a plan, a canal: Panama" — саме на цьому вході видно, чи проговорив ти нормалізацію.
Частотний словник. Порахувати, скільки разів кожен елемент зустрічається — база для купи реальних перевірок (розподіл статусів у відповіді API, дублікати в експорті).
function frequency(items: string[]): Map<string, number> {
const counts = new Map<string, number>();
for (const item of items) {
counts.set(item, (counts.get(item) ?? 0) + 1);
}
return counts;
}
Map кращий за звичайний обʼєкт: ключі будь-якого типу, немає колізій із успадкованими властивостями штибу constructor, і порядок вставки збережено. Оператор ?? дає нуль для ще не баченого ключа. Крайові: порожній масив (порожня мапа), усі елементи однакові, різний регістр (уточни, чи "A" і "a" — те саме).
Масиви: дублікати, унікальні, перетин, сортування обʼєктів
Унікальні значення — однорядковий ідіом, який мусиш видавати не думаючи:
const unique = [...new Set(items)];
Знайти дублікати — трохи хитріше, бо треба саме ті, що повторюються:
function findDuplicates(items: number[]): number[] {
const seen = new Set<number>();
const dupes = new Set<number>();
for (const item of items) {
if (seen.has(item)) dupes.add(item);
else seen.add(item);
}
return [...dupes];
}
Два Set — щоб не отримати сам дублікат кількаразово в результаті. Тут добре проговорити складність: один прохід, O(n), замість наївного подвійного циклу O(n²).
Перетин двох масивів — спільні елементи. Ключова оптимізація: другий масив у Set, щоб перевірка була константною.
function intersection(a: number[], b: number[]): number[] {
const setB = new Set(b);
return [...new Set(a)].filter((x) => setB.has(x));
}
Зовнішній new Set(a) прибирає дублікати з результату — уточни в інтервʼюера, чи це бажано. Крайові для всіх трьох: порожній масив, повний збіг, жодного спільного елемента, дублікати на вході.
Сортування обʼєктів — тут ховається найвідоміша пастка JavaScript. Метод sort без компаратора сортує як рядки, тож [10, 9, 1].sort() дасть [1, 10, 9]. І він мутує вихідний масив.
const users = [
{ name: 'Ada', age: 36 },
{ name: 'Alan', age: 41 },
{ name: 'Grace', age: 36 },
];
// за віком зростанням, копія без мутації оригіналу
const byAge = [...users].sort((a, b) => a.age - b.age);
Компаратор повертає число: відʼємне — a перед b, додатне — навпаки, нуль — порядок збережено. Для чисел — різниця, для рядків — localeCompare (щоб коректно з українськими літерами). Із ES2019 sort гарантовано стабільний, тож елементи з однаковим age збережуть відносний порядок — на це можна спертися. Крайові: порожній масив, один елемент, однакові ключі (перевірка стабільності), змішані типи.
Масив обʼєктів як тестові дані: фільтрація, групування, агрегація
Це найближча до реальної роботи AQA задача: тобі дають масив обʼєктів (відповідь API, вибірку з БД) і просять щось із ним зробити. Тут метод reduce — головний інструмент, і вміння його пояснити цінується.
Фільтрація тривіальна (filter), тож інтервʼюер зазвичай веде до групування.
Групування — розкласти елементи по кошиках за ключем:
interface TestRun {
suite: string;
status: 'passed' | 'failed' | 'skipped';
durationMs: number;
}
function groupByStatus(runs: TestRun[]): Map<string, TestRun[]> {
return runs.reduce((groups, run) => {
const bucket = groups.get(run.status) ?? [];
bucket.push(run);
groups.set(run.status, bucket);
return groups;
}, new Map<string, TestRun[]>());
}
Сучасний рушій (Node 21+) уміє Object.groupBy(runs, (r) => r.status) — але не в кожному середовищі воно є, тож на співбесіді безпечніше показати варіант через reduce, а про Object.groupBy згадати як новіший цукор. Це саме та обережність із доступністю API, яку від тебе й чекають.
Агрегація — порахувати підсумок по групі: кількість, суму, середнє.
function averageDuration(runs: TestRun[]): number {
if (runs.length === 0) return 0; // ділення на нуль!
const total = runs.reduce((sum, run) => sum + run.durationMs, 0);
return total / runs.length;
}
Порожній масив тут — не формальність, а справжня діра: без перевірки отримаєш NaN від ділення на нуль. Назвати цей крайовий випадок до того, як інтервʼюер тицьне в нього, — половина успіху. Комбінована задача «згрупуй прогони за суїтом і порахуй середню тривалість кожного» перевіряє все одразу; розкладай її на групування, а потім агрегацію кожної групи — не намагайся зробити в один прохід, поки не запрацює простий варіант.
Async-задачі: retry, послідовність, ліміт паралельності
Асинхронні задачі відсіюють тих, хто плутається в await. Механіку промісів розібрано в главі Асинхронність: Promises, async/await і паралельність; тут — типові live-coding формулювання. Усім трьом потрібен хелпер затримки:
const delay = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
Retry із затримкою — прямий болючий кейс автоматизатора: повторити нестабільну операцію кілька разів, перш ніж здатися.
async function retry<T>(
action: () => Promise<T>,
attempts = 3,
delayMs = 500,
): Promise<T> {
let lastError: unknown;
for (let i = 0; i < attempts; i++) {
try {
return await action();
} catch (error) {
lastError = error;
if (i < attempts - 1) await delay(delayMs);
}
}
throw lastError;
}
Ключові моменти для проговорювання: не спати після останньої спроби (нема сенсу); зберегти й прокинути останню помилку, а не проковтнути її; дженерик <T>, щоб типізація не губилась. Логічне продовження, яке часто просять, — експоненційний бекоф: delayMs * 2 ** i. Тут же доречно згадати тестувальницьку мудрість: retry маскує флак, а не лікує його (таксономія флакі-тестів — тема розділу «Автоматизація: стратегія»).
Послідовні запити — виконати по черзі, коли кожен наступний залежить від попереднього. Головна пастка — forEach не чекає await всередині:
// НЕ спрацює: forEach ігнорує проміси, усе стартує разом
ids.forEach(async (id) => { await fetchUser(id); });
// правильно: for...of дає справжню послідовність
async function fetchSequentially(ids: number[]): Promise<User[]> {
const users: User[] = [];
for (const id of ids) {
users.push(await fetchUser(id));
}
return users;
}
Ліміт паралельності — найскладніша з трійки. Promise.all запускає все одразу; якщо треба «не більше N одночасно» (щоб не завалити сервер), потрібен пул:
async function withLimit<T>(
tasks: (() => Promise<T>)[],
limit: number,
): Promise<T[]> {
const results: T[] = new Array(tasks.length);
let cursor = 0;
async function worker(): Promise<void> {
while (cursor < tasks.length) {
const index = cursor++;
results[index] = await tasks[index]();
}
}
const pool = Array.from({ length: Math.min(limit, tasks.length) }, worker);
await Promise.all(pool);
return results;
}
Тут важлива ідея: tasks — це масив функцій, що повертають проміси, а не готових промісів; інакше вони стартують до того, як пул почне їх притримувати. limit воркерів розбирають завдання зі спільного курсора. Порядок результатів збережено через індекс. Крайові: порожній список, limit більший за кількість задач, задача, що падає (уточни — чи зупиняти весь пул). Якщо не встигаєш дописати пул — чесно скажи ідею вголос; часто цього достатньо.
FizzBuzz як задача на дизайн
FizzBuzz («Fizz» на кратних 3, «Buzz» на кратних 5, «FizzBuzz» на кратних 15, інакше число») сам по собі тривіальний. Але його часто дають, щоб подивитися на розширюваність: «а тепер додай правило Bazz на кратних 7». Наївний варіант із каскадом if доведеться переписувати; тому цінують рішення, де правила — це дані.
class FizzBuzz {
constructor(private rules: [number, string][] = [[3, 'Fizz'], [5, 'Buzz']]) {}
convert(n: number): string {
const output = this.rules
.filter(([divisor]) => n % divisor === 0)
.map(([, word]) => word)
.join('');
return output || String(n);
}
}
const fb = new FizzBuzz([[3, 'Fizz'], [5, 'Buzz'], [7, 'Bazz']]);
// fb.convert(15) → 'FizzBuzz'; fb.convert(21) → 'FizzBazz'; fb.convert(1) → '1'
Тут «FizzBuzz» на 15 виходить автоматично — рядки склеюються, бо 15 кратне і 3, і 5. Нове правило — один елемент у масиві, жодного if переписувати. Це демонструє два принципи, які цінує ревʼюер тестового коду: конфігурація замість гілок і одна причина для зміни. Синтаксис класів — у главі this, класи та ООП у тестовому коді. Крайові: 0 (кратне всьому — поверне склейку всіх слів, уточни, чи це очікувано), відʼємні числа, дуже велике n.
Типові помилки
- Виглядає як «зрозумів задачу», а насправді не уточнив очікуваний результат. Кинувся кодити реверс — і зробив посимвольний, коли просили за словами. Дві хвилини питань дешевші за переписування під тиском.
- Виглядає як робочий код, а насправді падає на порожньому вході.
averageбез перевірки довжини даєNaN,[].reduceбез початкового значення кидає виняток. Порожнє — крайовий випадок номер один, завжди називай його першим. - Виглядає як сортування, а насправді лексикографічне.
[2, 10, 1].sort()→[1, 10, 2]. Забутий компаратор — найчастіша мовчазна помилка на співбесідах. - Виглядає як async-цикл, а насправді все стартувало разом.
awaitусерединіforEachігнорується; проміси не чекаються, а помилки в них стають unhandled rejection. Для послідовності —for...of. - Виглядає як паралельність із лімітом, а насправді
Promise.allнад готовими промісами. Якщо передати вже створені проміси, вони стартують негайно, і жоден «ліміт» їх не притримає. Ліміт працює лише над функціями, що відкладають старт. - Виглядає як мовчазна зосередженість, а насправді втрачені бали. Не пояснювати хід думки — програшна стратегія навіть із правильним кодом.
Підсумок
- Live coding для AQA оцінює мислення, а не алгоритми: уточнення вимог, бачення крайових випадків, пояснення вголос і самоперевірка важать більше за швидкість.
- Патерн вимоги → крайові випадки → рішення → перевірка — це той самий тест-дизайн, лише застосований до власного коду; проговорюй його явно.
- Порожній вхід, один елемент, дублікати, unicode й
null— універсальний список меж; називай їх до написання коду, а не після тику інтервʼюера. - Знай напамʼять ідіоми:
[...new Set()]для унікальних,Setдля перетину/дублікатів заO(n),reduceдля групування й агрегації, компаратор дляsort. - В async уникай
awaitуforEach, розумій різницю послідовногоfor...ofіPromise.all, а ліміт паралельності будуй над функціями, не над промісами.
Що питають на співбесіді
- «Розверни рядок» — інтервʼюер чекає, що спитаєш про unicode й порожній вхід; сам однорядковик менш важливий за ці питання.
- «Знайди дублікати / перетин масивів» — дивляться, чи згадаєш
Setі чи оціниш складність; наївнийO(n²)покаже прогалину. - «Згрупуй ці дані й порахуй підсумок» — перевірка
reduceі того, чи обробиш порожню групу безNaN; проксі реальної роботи з відповіддю API. - «Зроби retry для нестабільного запиту» — чекають коректну затримку (не після останньої спроби), прокидання помилки і зауваження, що retry маскує флак.
- «Обмеж кількість одночасних запитів» — найскладніше; навіть якщо не допишеш пул, цінують правильно сформульовану ідею з функціями-задачами.
- «Ось FizzBuzz — тепер додай правило» — перевіряють, чи закладеш розширюваність через дані, а не каскад
if.
На що дивиться інтервʼюер понад код: чи ти уточнюєш перед тим, як писати; чи проговорюєш крайові випадки; чи перевіряєш себе на межах наприкінці. Це рівно ті рефлекси, які потім підуть у тест-дизайн і код-рев'ю. Якщо застряг — думай уголос: мовчазний глухий кут гірший за озвучену гіпотезу.
Джерела
- MDN: Array — методи (
map,filter,reduce,sort) — довідник із методів масивів і поведінкиsort. - MDN: Set — унікальність, дублікати, перетин.
- MDN: Promise —
all/allSettled/raceдля паралельних і послідовних сценаріїв. - MDN: async function — коректна робота з
await, зокрема пастка циклів. - Силабус ISTQB CTFL 4.0 — розділ 4 (техніки чорної скриньки: класи еквівалентності й граничні значення), що стоять за списком крайових випадків.
Що таке live coding на співбесіді AQA і що на ньому насправді перевіряють?
Це формат, де тобі дають невелику задачу й просять розвʼязати її прямо в редакторі, коментуючи кожен крок. Для автоматизатора це майже ніколи не олімпіадний алгоритм, а щось побутове: перевернути рядок, вимести дублікати з масиву, розкласти тестові дані по групах. Але дивляться не на сам код: інтервʼюер зчитує, чи ти уточнюєш умову, чи бачиш крайові випадки, чи міркуєш уголос і чи перевіряєш себе наприкінці. Це ті самі рефлекси, що потім працюють у тест-дизайні та на код-рев'ю, тому задача-«розминка» насправді є проксі твоєї основної навички. Кандидат, який мовчки видав ідеальний однорядковик, часто програє тому, хто три хвилини розкладав задачу вголос.
Тобі дали задачу «обробити масив». Що зробиш перед тим, як писати перший рядок?
Спершу окреслю простір входів і виходів, а не кинуся кодити — саме цим QA і відрізняється від розробника на тому самому завданні. Уточню тип і формат входу, чого чекають на виході, і чи можливі особливі значення. Потім уголос назву крайові випадки: порожній масив, один елемент, дублікати, null або undefined серед даних, дуже великий вхід. Навіть якщо не оброблятиму їх усі, показую, що бачу межі — це прямий сигнал зрілості. Тільки після цього беруся за найпростіше робоче рішення, а наприкінці проганяю його подумки на happy-path і на кількох краях.
Розверни рядок. Що б ти уточнив і де тут пастка?
Спершу спитаю, чи реверс посимвольний чи за словами, і що робити з порожнім входом — умову навмисно дають неповною. Базове рішення в TypeScript — розкласти рядок у масив, перевернути й склеїти назад. Головний нюанс — unicode: якщо різати рядок через split(''), він поділиться за 16-бітними одиницями, і емодзі чи символ із сурогатної пари розваляться навпіл. Тому надійніше перебирати через спред [...input], який іде за код-поінтами й не ламає такі символи. Чесно варто додати межу власних знань: комбіновані діакритики навіть код-поінти можуть рознести, і тут потрібен уже сегментатор графем. Крайові — порожній рядок повертає порожній, один символ дає сам себе.
Як перевірити, що рядок — паліндром, і в чому головна пастка задачі?
Пастка не в порівнянні з реверсом, а в тому, що взагалі вважати паліндромом. Майже завжди мають на увазі ігнорувати регістр, пробіли й пунктуацію, тож перший крок — нормалізувати рядок: привести до нижнього регістру й лишити тільки букви та цифри. Далі можна порівняти з його ж реверсом, але охайніше — двома вказівниками з країв до центру: біжимо, поки символи збігаються, і на першій розбіжності повертаємо false. Двовказівниковий варіант показує, що вмієш і без «магії» методів, хоча реверс із поясненням теж валідний. Класичний перевірочний вхід — фраза на кшталт "A man, a plan, a canal: Panama": саме на ньому видно, чи ти проговорив нормалізацію. Порожній рядок і один символ зазвичай вважають паліндромами.
Порахуй, скільки разів кожен елемент зустрічається. Чому для лічильника краще Map, а не обʼєкт?
Це частотний словник — база для купи реальних перевірок штибу розподілу статусів у відповіді API чи пошуку дублікатів в експорті. Проходжу масив і для кожного елемента збільшую лічильник; оператор ?? дає нуль для ще не баченого ключа. Map тут кращий за звичайний обʼєкт із кількох причин: ключем може бути значення будь-якого типу, немає колізій зі службовими іменами штибу constructor чи toString, і порядок вставки збережено. Обʼєкт же має прототип, тож ключ, що збігається з успадкованою властивістю, може дати сюрприз. Крайові: порожній масив дає порожню мапу, а різний регістр варто уточнити окремо — чи "A" і "a" рахуються як одне.
Дай унікальні значення й окремо — тільки дублікати. Чим ці задачі різняться?
Унікальні значення — це ідіома, яку треба видавати не думаючи: обгорнути масив у Set і розкласти назад через спред. Set за визначенням тримає лише неповторні значення, тож дублікати відсіюються самі. А от знайти саме ті елементи, що повторюються, трохи хитріше: тримаю два Set — «бачені» й «дублікати», і коли елемент трапляється вдруге, кладу його в другий. Два набори потрібні, щоб один елемент, який повторюється тричі, не потрапив у результат кілька разів. Тут доречно проговорити складність: один прохід дає O(n) проти наївного подвійного циклу з O(n²). Згадка про Set і про оцінку складності — саме те, чого чекає інтервʼюер.
Знайди спільні елементи двох масивів. Як зробити це ефективно?
Наївне рішення — для кожного елемента першого масиву пробігати другий — дає квадратичну складність. Оптимізація в тому, щоб покласти другий масив у Set: тоді перевірка «чи є елемент» стає константною, і весь перетин рахується за лінійний час. Зазвичай ще прибирають дублікати з результату, обгорнувши перший масив у Set перед фільтрацією, — але це варто уточнити в інтервʼюера, бо іноді дублікати в перетині потрібні. Проговори припущення вголос: чи вважати [1, 1, 2] та [1, 2] спільним [1, 2] чи [1, 1, 2]. Крайові для таких задач однакові: порожній масив, повний збіг, жодного спільного елемента, дублікати на вході.
Відсортуй масив обʼєктів за полем. Які дві класичні пастки JavaScript тут чигають?
Перша пастка — метод sort без компаратора сортує елементи як рядки, тож [10, 9, 1].sort() дасть [1, 10, 9], а не те, що очікуєш. Тому для чисел завжди передаю компаратор — різницю a.age - b.age, а для рядків localeCompare, щоб коректно врахувати українські літери. Друга пастка — sort мутує вихідний масив, а не повертає копію, тому спершу роблю поверхневу копію через спред і сортую вже її, аби не зіпсувати оригінал. Компаратор працює за правилом: відʼємне число ставить a перед b, додатне — навпаки, нуль лишає порядок. Забутий компаратор — найчастіша мовчазна помилка на співбесідах, і назвати обидві пастки вголос коштує дорожче за сам код.
Чи гарантовано стабільне сортування в JavaScript і чому це важливо?
Так, із ES2019 специфікація вимагає, щоб sort був стабільним: елементи, які компаратор вважає рівними, зберігають свій відносний порядок. На практиці це означає, що якщо відсортувати прогони за тривалістю, а потім за статусом, то всередині кожного статусу давніший порядок за тривалістю не зруйнується — на це можна спертися при багаторівневому сортуванні. До ES2019 поведінка залежала від рушія, тож старі приклади цього не гарантували. Для перевірки стабільності хороший крайовий вхід — кілька елементів з однаковим ключем сортування: якщо їхній порядок зберігся, компаратор і стабільність працюють як треба. Це той нюанс, що відрізняє поверхневу відповідь від глибокої.
Згрупуй масив обʼєктів за ключем. Який інструмент і що згадати про доступність API?
Групування — це розкласти елементи по кошиках за спільним ключем, і головний інструмент тут reduce з Map як акумулятором: для кожного елемента дістаю відповідний кошик (або створюю порожній через ??), додаю елемент і повертаю мапу далі. Уміння пояснити крок reduce уголос цінується окремо, бо багато хто його боїться. Варто згадати й новіший синтаксичний цукор Object.groupBy, який робить те саме одним викликом, — але одразу додати, що він доступний не в кожному середовищі (потрібен свіжий рушій), тож на співбесіді безпечніше показати варіант через reduce. Саме ця обережність із сумісністю API — те, чого від автоматизатора й чекають. Крайовий випадок — порожній вхід, який має дати порожню структуру без падіння.
Порахуй середню тривалість по групі прогонів. Який крайовий випадок тут номер один?
Порожній масив — і це не формальність, а справжня діра: якщо просто поділити суму на довжину, на порожньому вході отримаєш NaN від ділення на нуль. Тому першим рядком ставлю перевірку довжини й повертаю нуль (або кидаю зрозумілу помилку — уточни, що очікують) до будь-яких обчислень. Далі сумую тривалості через reduce з початковим значенням і ділю на кількість. Назвати цей крайовий випадок до того, як інтервʼюер тицьне в нього пальцем, — половина успіху. Комбіновану задачу «згрупуй за суїтом і порахуй середнє кожної групи» варто розкладати на два кроки: спершу групування, потім агрегація кожної групи, а не намагатися зробити все в один прохід, поки простий варіант не запрацює.
Напиши retry для нестабільного запиту. Які моменти обовʼязково проговорити?
Retry — прямий болючий кейс автоматизатора: повторити операцію кілька разів, перш ніж визнати провал. Загальна форма — цикл на задану кількість спроб, де кожну спробу обгортаю в try/catch; при успіху одразу повертаю результат, при помилці — зберігаю її й чекаю перед наступним колом. Ключових деталей три: не спати після останньої спроби, бо чекати вже нема сенсу; зберегти й прокинути останню помилку, а не мовчки проковтнути її; і зробити функцію дженериком, щоб тип результату не губився. Логічне продовження, яке часто просять, — експоненційний бекоф, коли затримка множиться з кожним колом. І доречно додати тестувальницьку думку: retry маскує флак, а не лікує його, тож у продакшн-тестах ним не варто затуляти справжню нестабільність.
Чому await усередині forEach не працює і як виконати запити послідовно?
forEach не вміє чекати на проміси: він викликає передану async-функцію для кожного елемента, але не зупиняється на await усередині неї, тож усі ітерації стартують практично одночасно, а сам forEach завершується до того, як бодай один проміс виконався. Гірше, що помилки в цих промісах нікому не адресовані й перетворюються на unhandled rejection. Якщо потрібна справжня послідовність — коли кожен наступний крок залежить від попереднього, — використовую цикл for...of: він поважає await і виконує ітерації по черзі. Це одна з найтиповіших пасток на співбесідах саме тому, що код виглядає правильним і навіть іноді «майже працює». Якщо ж послідовність не потрібна, а треба лише дочекатися всіх, тоді підходить Promise.all над масивом промісів.
Обмеж кількість одночасних запитів до N. Чому важливо будувати ліміт над функціями, а не над промісами?
Promise.all запускає все відразу, тож коли треба «не більше N водночас» (щоб не завалити сервер), потрібен пул воркерів. Критична ідея: на вхід має йти масив функцій, що повертають проміси, а не масив уже створених промісів. Проміс починає виконуватися в момент створення, тому якщо передати готові проміси, вони всі стартують негайно, і жоден «ліміт» їх уже не притримає — обмеження працює лише над відкладеним стартом. Сам пул — це N воркерів, які розбирають задачі зі спільного лічильника-курсора, а результати кладуться за індексом, щоб зберегти порядок. Крайові: порожній список, limit більший за кількість задач, і задача, що падає, — тут варто уточнити, чи зупиняти весь пул. Навіть якщо не встигаєш дописати код, чітко сформульована ідея з функціями-задачами часто зараховується.
Тобі дали FizzBuzz і кажуть «а тепер додай нове правило». Що перевіряють і як відповісти сильно?
Сам FizzBuzz тривіальний, тож коли просять додати правило (скажімо, слово на кратних 7), перевіряють розширюваність, а не вміння написати каскад if. Наївний варіант із гілками доведеться переписувати під кожне нове правило, і саме це вважають слабким рішенням. Сильна відповідь виносить правила в дані — масив пар «дільник → слово», — а сама функція перебирає їх, збирає слова для всіх дільників, на які число ділиться, і склеює; якщо жодне не підійшло, повертає число. Тоді «FizzBuzz» на 15 виходить автоматично, бо склеюються обидва слова, а нове правило — це просто ще один елемент масиву без жодного нового if. За таким рішенням ревʼюер тестового коду читає дві важливі речі: поведінку задають конфігурацією, а не розгалуженнями, і код має єдину причину змінюватися. Крайові — нуль, який кратний усьому, та відʼємні числа.
Ти застряг посеред задачі й не памʼятаєш синтаксис. Що робити?
Найгірше — замовкнути й мовчки битися об стіну: інтервʼюер бачить глухий кут, але не бачить думки, тож не може ані підказати, ані зарахувати хід міркувань. Правильна стратегія — думати вголос: озвучити, що саме гальмує, яку ідею намагаюся реалізувати й де саме забув деталь. Часто цього достатньо, щоб інтервʼюер підкинув потрібний метод або підтвердив напрям, — а озвучена гіпотеза завжди краща за озвучене мовчання. Якщо забув конкретний метод, можна назвати його роль («потрібно щось, що прибирає дублікати — це Set») і рухатися далі, домовившись уточнити синтаксис потім. Здатність не панікувати й вести діалог у складний момент оцінюють не менше за готовий код, бо на реальній роботі застрягання — теж норма.
Три розбори, які показують не «правильну відповідь», а хід до неї: як провести задачу крізь патерн вимоги → крайові випадки → рішення → перевірка, як зробити async-задачу робочою в реальному Playwright-контексті і як обрати правильний спосіб паралельності за таблицею рішень.
Кейс 1. Проводимо задачу крізь патерн: групування + середнє
Умова, яку почуєш майже дослівно: «Ось масив прогонів тестів, згрупуй їх за суїтом і порахуй середню тривалість кожного суїта». Спокуса — одразу почати писати reduce. Сильний кандидат натомість спершу вголос проходить чотири кроки.
Крок 1. Вимоги. «Уточню: тривалість завжди число в мілісекундах? Суїт — це рядок, чи може бути порожнім? На виході потрібна мапа суїт → середнє чи масив обʼєктів? Середнє округлювати?» Дві-три хвилини тут рятують від переписування.
Крок 2. Крайові випадки — називаю до коду. Порожній вхідний масив; суїт з єдиним прогоном; суїт, де всі тривалості нульові; поле durationMs, якого раптом немає в елементі. Ключовий серед них — порожня група, бо середнє від неї дає NaN.
Крок 3. Рішення — найпростіше, що працює. Розкладаю на два зрозумілі кроки замість одного розумного проходу: спершу групую, потім рахую середнє кожної групи.
interface TestRun {
suite: string;
status: 'passed' | 'failed' | 'skipped';
durationMs: number;
}
function groupBySuite(runs: TestRun[]): Map<string, TestRun[]> {
return runs.reduce((groups, run) => {
const bucket = groups.get(run.suite) ?? [];
bucket.push(run);
groups.set(run.suite, bucket);
return groups;
}, new Map<string, TestRun[]>());
}
function averageDurationBySuite(runs: TestRun[]): Map<string, number> {
const result = new Map<string, number>();
for (const [suite, group] of groupBySuite(runs)) {
// порожня група сюди не потрапить, але поодинокий елемент — так
const total = group.reduce((sum, run) => sum + run.durationMs, 0);
result.set(suite, total / group.length);
}
return result;
}
Крок 4. Перевірка — проганяю вголос. «Порожній вхід → зовнішній reduce не викликається жодного разу, повертається порожня мапа, ділення на нуль не трапляється, ок. Один прогін у суїті → сума ділиться на один, повертає саму тривалість, ок. Два прогони 100 і 300 → середнє 200, ок».
Що тут читає інтервʼюер:
- Порожня група оброблена конструкцією, а не забута. Всередині циклу
group.lengthзавжди щонайменше один, бо групи створюються тільки для наявних елементів — і це варто сказати вголос, а не сподіватися, що помітять. - Два кроки замість одного «хитрого». Розкладена задача читається й дебажиться простіше; передчасна спроба порахувати все в один прохід — часта причина застрягання.
reduceпояснено. Акумулятор — мапа, кожен елемент кладеться у свій кошик; уміння проговорити це цінується окремо.
Кейс 2. Async на практиці: retry поверх flaky-запиту в Playwright
«Напиши retry для нестабільного запиту» — і одразу продовження: «а тепер прикрути його до реального API-виклику». Ось де теорія промісів стає робочим кодом автоматизатора.
const delay = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
async function retry<T>(
action: () => Promise<T>,
attempts = 3,
delayMs = 500,
): Promise<T> {
let lastError: unknown;
for (let i = 0; i < attempts; i++) {
try {
return await action();
} catch (error) {
lastError = error;
// не спимо після ОСТАННЬОЇ спроби — чекати вже нема сенсу
if (i < attempts - 1) await delay(delayMs * 2 ** i); // експоненційний бекоф
}
}
throw lastError; // прокидаємо останню помилку, а не ковтаємо
}
Тепер той самий хелпер у контексті Playwright API — повторюємо запит, поки ресурс «прогрівається» після деплою:
import { test, expect } from '@playwright/test';
test('статус готовності стає 200 після кількох спроб', async ({ request }) => {
const res = await retry(async () => {
const r = await request.get('https://app.example.com/health');
// кидаємо, щоб retry вважав не-200 невдачею й повторив
if (r.status() !== 200) throw new Error(`health = ${r.status()}`);
return r;
});
expect(res.status()).toBe(200);
});
Що дивитися і чому:
- Умова
i < attempts - 1прибирає зайву паузу. Без неї код чесно спить після фінального провалу перед тим, як кинути помилку, — марна затримка, яку інтервʼюер помічає одразу. throwусерединіaction, а не мовчазнийreturn r.retryреагує лише на викинуту помилку, тож не-200 треба перетворити на виняток; інакше перша ж відповідь503вважатиметься успіхом і повтору не буде.- Retry маскує флак, а не лікує. У health-чеку прогрівання середовища це доречно, але прикривати retry-єм справжню нестабільність продукту — значить ховати баг; про це варто сказати вголос як про межу застосування.
Кейс 3. Таблиця рішень: який спосіб паралельності обрати
Найчастіша плутанина в async-задачах — коли брати for...of, коли Promise.all, а коли будувати пул із лімітом. Розвʼязує її одне питання: чи залежать виклики один від одного і чи можна перевантажити сервер.
| Ситуація | Правильний інструмент | Чому не інакше |
|---|---|---|
| Кожен наступний виклик залежить від результату попереднього | for...of з await усередині | Promise.all запустив би все відразу й зламав залежність; forEach узагалі не чекає |
| Виклики незалежні, сервер витримає всі одночасно | Promise.all над масивом промісів | Послідовний for...of тут марно повільний — немає причини чекати по черзі |
| Виклики незалежні, але «не більше N водночас» | Пул воркерів над масивом функцій | Promise.all не має ліміту; готові проміси стартують негайно й ліміт їх не притримає |
| Потрібні всі результати, навіть якщо частина впала | Promise.allSettled | Promise.all відхиляється на першій же помилці й губить решту результатів |
Найпідступніший рядок — третій. Ось чому ліміт працює лише над функціями:
async function withLimit<T>(
tasks: (() => Promise<T>)[], // саме функції, не готові проміси!
limit: number,
): Promise<T[]> {
const results: T[] = new Array(tasks.length);
let cursor = 0;
async function worker(): Promise<void> {
while (cursor < tasks.length) {
const index = cursor++;
results[index] = await tasks[index]();
}
}
const pool = Array.from({ length: Math.min(limit, tasks.length) }, worker);
await Promise.all(pool);
return results;
}
Що дивитися і чому:
tasks— масив функцій, а не промісів. Проміс стартує в момент створення, тож якби на вході були вже створені проміси, усі 100 запитів пішли б одразу, іlimitстав би декорацією. Функція відкладає старт до моменту, коли воркер її викличе.- Порядок результатів збережено через
index. Воркери розбирають задачі зі спільного курсора в довільному темпі, але кожен кладе результат за своїм індексом, тож вихід відповідає входу. - Крайові варто назвати вголос. Порожній список;
limitбільший за кількість задач (тоді воркерів створюється рівно стільки, скільки задач); задача, що падає, — уточни, чи зупиняти весь пул. Навіть недописаний пул із правильно сформульованою ідеєю функцій-задач зараховують.
Що оцінюють на live coding
- Розумію, що дивляться не на сам код, а на мислення: уточнення вимог, бачення крайових випадків, пояснення вголос і самоперевірку.
- Можу проговорити патерн вимоги → крайові випадки → рішення → перевірка й свідомо застосувати його до будь-якої задачі.
- Тримаю в голові універсальний список меж: порожній вхід, один елемент, дублікати, unicode,
null/undefined, дуже великий вхід.
Рядки
- Знаю різницю між
[...str](за код-поінтами) іstr.split('')(за 16-бітними одиницями) і чому емодзі ламаються саме на другому. - Розумію, що в задачі на паліндром головне питання — не порівняння, а нормалізація: регістр, пробіли, пунктуація.
- Знаю, чому для частотного словника
Mapкращий за обʼєкт: будь-які ключі, немає колізій зі службовими іменами, збережений порядок вставки.
Масиви й Set
- Видаю не думаючи ідіому
[...new Set(items)]для унікальних значень. - Можу знайти саме дублікати через два
Set(бачені та повторені) за один прохідO(n)замість наївногоO(n²). - Роблю перетин масивів ефективним, кладучи один масив у
Setзаради константної перевірки.
Сортування
- Знаю, що
sortбез компаратора сортує як рядки, тож[10, 9, 1].sort()дає[1, 10, 9]. - Памʼятаю, що
sortмутує оригінал, і сортую копію через[...arr].sort(...). - Можу пояснити, що з ES2019
sortгарантовано стабільний, і на це можна спертися при багаторівневому сортуванні.
Тестові дані: фільтрація, групування, агрегація
- Можу згрупувати елементи за ключем через
reduceзMap-акумулятором і пояснити кожен крок. - Знаю про
Object.groupByяк новіший цукор і згадую про його обмежену доступність у середовищах. - Завжди перевіряю порожній масив перед агрегацією, бо середнє без цього дає
NaNвід ділення на нуль.
Async
- Пишу retry, що не спить після останньої спроби, зберігає й прокидає останню помилку і типізований дженериком; памʼятаю, що retry маскує флак, а не лікує.
- Знаю, чому
awaitусерединіforEachне працює, і роблю послідовність черезfor...of. - Будую ліміт паралельності над масивом функцій, що повертають проміси, а не над готовими промісами.
FizzBuzz і дизайн
- Розумію, що FizzBuzz дають на розширюваність, і виношу правила в дані замість каскаду
if. - Можу пояснити, чому «FizzBuzz» на 15 виходить автоматично зі склеювання слів, а нове правило — це один елемент масиву.
Що інтервʼюер оцінює на live coding для AQA передусім?
Питання
Патерн міркування на live coding для AQA?