Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді з Vue

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

108 питань

Тест має ламатися, коли ламається поведінка, і не ламатися, коли код переписали без зміни поведінки. Тести, що перевіряють деталі реалізації, роблять навпаки.

Деталі реалізації, яких не варто торкатися:

  • внутрішній стан через wrapper.vm: expect(wrapper.vm.isOpen).toBe(true). Перейменували змінну чи перенесли стан у composable - тест впав, хоча все працює. Перевіряйте, що меню видно;
  • виклики приватних методів: «після кліку викликався handleClick» - а не «після кліку з'явилось повідомлення»;
  • структура дочірніх компонентів і CSS-класи, що відповідають за вигляд;
  • кількість рендерів і порядок внутрішніх викликів, якщо це не вимога продуктивності.

Що тестувати:

  • вхід: props, введення й дії користувача, відповіді API, стан сторів;
  • вихід: що відрендерено, які події випромінено, які запити й дії стора викликано, куди перейшов маршрутизатор.

Тести-знімки (snapshots) зберігають HTML компонента у файл і порівнюють з ним при наступних запусках:

expect(wrapper.html()).toMatchSnapshot()

Чим вони небезпечні:

  • падають від будь-якої зміни - додали клас, змінили текст, поміняли порядок атрибутів. Після кількох таких падінь команда звикає оновлювати знімки не дивлячись (vitest -u) - і знімок більше нічого не перевіряє;
  • не кажуть, що саме важливо: у знімку на 200 рядків незрозуміло, яка частина - вимога, а яка - випадковість;
  • великі знімки ніхто не рецензує в пул-реквестах.

Коли знімки доречні:

  • маленькі й стабільні результати: відформатований рядок, згенерований фрагмент розмітки;
  • вбудовані знімки (toMatchInlineSnapshot) - лежать прямо в тесті й видні при рецензії;
  • як тимчасова страховка перед великим рефакторингом.

Ще кілька пасток:

  • покриття заради покриття: 100% рядків не означає, що перевірено важливе. Краще 70% з тестами граничних випадків і помилок;
  • тестування бібліотек: не потрібно перевіряти, що v-model чи Vue Router працюють - це вже протестовано;
  • дублювання логіки в тесті: обчислювати очікуване значення тим самим алгоритмом, що й код, - тест завжди «зелений», навіть якщо алгоритм хибний. Очікування задають явними значеннями.

Докладніше в документації: Тестування у Vue

Задача: при реєстрації показати «цей email уже зайнятий» ще до відправки форми. Наївне рішення - запит на кожен введений символ - створює три проблеми:

  • навантаження: 20 символів - 20 запитів;
  • гонка відповідей: відповідь на olena@ може прийти пізніше за відповідь на olena@example.com, і на екрані залишиться неправильний результат;
  • перевірка незавершеного значення - марна робота.

Рішення - затримка + скасування + перевірка формату:

import { ref, watch, onWatcherCleanup } from 'vue'

const email = ref('')
const status = ref<'idle' | 'checking' | 'free' | 'taken' | 'error'>('idle')

watch(email, (value) => {
  if (!/^\S+@\S+\.\S+$/.test(value)) {
    status.value = 'idle'
    return
  }

  const controller = new AbortController()
  const timer = setTimeout(async () => {
    status.value = 'checking'
    try {
      const res = await fetch(`/api/email-available?email=${encodeURIComponent(value)}`, {
        signal: controller.signal,
        headers: { Accept: 'application/json' },
      })
      status.value = (await res.json()).available ? 'free' : 'taken'
    } catch (e) {
      if ((e as Error).name !== 'AbortError') status.value = 'error'
    }
  }, 400)

  onWatcherCleanup(() => {      // Vue 3.5+: викликається перед наступним запуском
    clearTimeout(timer)         // ще не почали - не починаємо
    controller.abort()          // уже почали - скасовуємо запит
  })
})

Як це працює: кожна зміна значення спершу «прибирає» попередній запуск - скасовує таймер і запит. Тому відповідь на застаріле значення або не надійде, або буде відкинута, і гонки не буде. До Vue 3.5 те саме робить третій аргумент колбека watch - onCleanup.

Що врахувати:

  • 400-500 мс - розумна затримка: користувач устигає дописати слово, і відповідь усе одно здається миттєвою;
  • AbortError - не помилка, його ігнорують;
  • перевірка на клієнті - підказка, а не гарантія: між перевіркою й відправкою адресу може зайняти інший користувач. Унікальність гарантує правило unique на сервері й унікальний індекс у базі;
  • перебір облікових записів: ендпойнт «чи зайнятий email» розкриває, хто зареєстрований. Обмежте частоту запитів (throttle) і подумайте, чи прийнятне це розкриття для вашого продукту;
  • у VeeValidate асинхронне правило в схемі запускається при кожній валідації поля - затримку й скасування там теж треба додати, бібліотека не робить їх сама.

Той самий прийом працює для пошуку з автодоповненням, перевірки промокоду, розрахунку вартості доставки.

Докладніше в документації: Vue: очищення побічних ефектів у watch

Задача: користувач півгодини заповнював форму, випадково клікнув посилання - і все втратив. Потрібні дві речі: знати, чи змінено форму, і перехопити вихід.

1. Чи змінена форма - порівняння з початковим знімком:

import { computed, reactive, ref } from 'vue'

const snapshot = ref(JSON.stringify(initialData))
const form = reactive(structuredClone(initialData))

const isDirty = computed(() => JSON.stringify(form) !== snapshot.value)

function markSaved() {
  snapshot.value = JSON.stringify(form)   // після успішного збереження
}

Порівняння зі знімком краще за прапорець «щось змінювали»: якщо користувач змінив поле й повернув старе значення, форма знову вважається чистою. Для великих форм серіалізація на кожну зміну може бути відчутною - тоді порівнюють лише змінені поля чи беруть meta.dirty з бібліотеки форм.

2. Перехід усередині SPA - guard Vue Router:

import { onBeforeRouteLeave } from 'vue-router'

onBeforeRouteLeave(() => {
  if (isDirty.value && !window.confirm('Є незбережені зміни. Вийти?')) {
    return false   // скасувати навігацію
  }
})

Замість confirm можна показати власне модальне вікно - guard може бути асинхронним і повернути false після відповіді користувача.

3. Закриття вкладки, перезавантаження, зовнішнє посилання - роутер про це не знає, потрібна подія браузера:

function onBeforeUnload(e: BeforeUnloadEvent) {
  if (!isDirty.value) return
  e.preventDefault()
  e.returnValue = ''   // для старіших браузерів
}

onMounted(() => window.addEventListener('beforeunload', onBeforeUnload))
onBeforeUnmount(() => window.removeEventListener('beforeunload', onBeforeUnload))

Браузер покаже власне стандартне повідомлення - свій текст задати неможливо, це захист від зловживань.

Підводні камені:

  • після збереження не оновили знімок - користувач щойно зберіг, а при виході отримує попередження;
  • редирект після збереження спрацьовує раніше за оновлення стану - guard блокує власний перехід застосунку. Оновлюйте знімок до навігації;
  • beforeunload постійно підписаний заважає браузеру використовувати back/forward cache - вішайте обробник лише коли форма змінена;
  • дані з сервера підвантажилися після створення знімка - форма одразу «брудна». Знімок робиться після завантаження;
  • не зловживайте: попередження для пошукового поля чи фільтрів дратує. Лише для форм, де втрата справді болить; для довгих форм краще чернетки в localStorage чи автозбереження.

Докладніше в документації: Vue Router: навігаційні guards у Composition API

Питання з реальних технічних співбесід - 108 питань у 9 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 36 Middle 37 Senior 35

Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії