QA Interview Hub · База знань · Тести

Playwright

Auto-waiting і web-first асерти

Web-first assertions

Web-first assertions — це expect з локатором: toBeVisible, toHaveText, toHaveValue, toHaveCount, toHaveURL тощо. Вони перевіряють умову в циклі, доки вона не стане істинною або не спливе таймаут — за замовчуванням 5 секунд, налаштовується через expect.timeout. Пишуться з await перед expect: await expect(locator).toHaveText('Готово').

Погана практика — expect(await locator.textContent()).toBe('Готово'): тут значення береться один раз, і порівнюються два рядки без повторів. Якщо сторінка ще не оновилась, тест упаде, і з'являється спокуса додати waitForTimeout. Так само замість isVisible() з перевіркою булевого значення використовують toBeVisible().

Додатково: expect.soft не зупиняє тест при падінні, а збирає всі помилки. expect.poll і expect(...).toPass() дають ретраї для власної логіки, наприклад для перевірки через API. Перевірку заперечення пишуть через not: await expect(locator).not.toBeVisible().

Actionability: що саме перевіряється перед дією

Набір перевірок залежить від дії. Усі дії чекають, що елемент приєднаний до DOM, а далі різниця така:

ДіяЩо чекає Playwright
click, check, uncheck, setCheckedvisible, stable, receives events, enabled
hover, dragTovisible, stable, receives events
fill, clearvisible, enabled, editable
selectOptionvisible, enabled
screenshotvisible, stable

«Stable» означає, що елемент не анімується два кадри поспіль. «Receives events» означає, що в точці кліку саме цей елемент, а не оверлей чи спінер. Якщо елемент перекритий, Playwright чекає й у логах пише, який елемент перехоплює клік.

Дві корисні опції. force: true вимикає частину перевірок — це обхід, який може приховати справжній баг, коли користувач теж не зможе клікнути. trial: true виконує всі перевірки, але саму дію не робить: так можна дочекатися готовності елемента, наприклад перед hover-меню.

Навігація має свої очікування. page.goto за замовчуванням чекає подію load. Опція waitUntil: 'networkidle' офіційно не рекомендується: на сторінках з polling чи аналітикою вона нестабільна. Після дії, що змінює URL, краще await expect(page).toHaveURL(/orders/) або page.waitForURL.

На співбесіді: Перед кліком Playwright чекає, що елемент видимий, стабільний, увімкнений і не перекритий, а перед fill — що поле редаговане. force: true я використовую дуже рідко, бо він може приховати реальний баг з оверлеєм. Замість networkidle чекаю конкретний результат: URL, елемент або відповідь API.

expect.poll і toPass: ретраї для своєї логіки

Web-first асерти ретраяться, лише коли в expect передано локатор або page. Коли треба чекати щось інше — статус у БД, відповідь API, файл — використовують expect.poll або toPass.

expect.poll багаторазово викликає функцію й застосовує матчер до результату:

await expect.poll(async () => {
  const res = await request.get(`/api/orders/${id}`);
  return (await res.json()).status;
}, { message: 'замовлення має стати PAID', timeout: 30_000 }).toBe('PAID');

За замовчуванням він бере таймаут expect (5 секунд), а інтервали зростають: 100, 250, 500, 1000 мс. Їх можна задати через intervals.

toPass ретраїть цілий блок коду з кількома діями й асертами, доки блок не пройде без помилок:

await expect(async () => {
  await page.getByRole('button', { name: 'Оновити' }).click();
  await expect(page.getByText('Синхронізовано')).toBeVisible({ timeout: 1000 });
}).toPass({ timeout: 20_000, intervals: [1000, 2000] });

Підступний момент: у toPass таймаут за замовчуванням 0, тобто він не бере expect.timeout і ретраїть, доки не скінчиться час тесту. Тому таймаут я задаю явно. Усередині також ставлю короткі таймаути внутрішнім асертам, інакше одна ітерація займе 5 секунд.

На співбесіді: Коли чекаю не UI, а, наприклад, асинхронну обробку платежу на бекенді, я використовую expect.poll з функцією, що опитує API. Якщо треба повторювати цілу послідовність дій, беру toPass і завжди задаю йому таймаут, бо за замовчуванням він обмежений лише таймаутом тесту.

Soft-асерти, повідомлення і власні матчери

expect.soft фіксує помилку, але не зупиняє тест. Тест все одно буде червоним, а у звіті будуть усі падіння разом. Це зручно для перевірки форми з багатьма полями чи сторінки профілю: за один прогін видно всі розбіжності.

await expect.soft(page.getByLabel('Ім\'я')).toHaveValue('Ростислав');
await expect.soft(page.getByLabel('Email')).toHaveValue('r@test.com');
await expect.soft(page.getByLabel('Телефон')).toHaveValue('+380501112233');
expect(test.info().errors).toHaveLength(0);
await page.getByRole('button', { name: 'Зберегти' }).click();

Останній рядок перед дією — практичний прийом: якщо soft-асерти вже впали, далі йти немає сенсу.

Soft не варто ставити на передумови. Якщо сторінка не відкрилась, усі наступні перевірки впадуть каскадом і зашумлять звіт.

Другим аргументом expect можна передати власне повідомлення: expect(locator, 'кнопка оплати має бути активна').toBeEnabled(). Воно з'являється у звіті й trace, і падіння читається без відкриття коду.

expect.configure створює налаштований екземпляр: const slowExpect = expect.configure({ timeout: 15_000 }) або { soft: true }. А expect.extend дозволяє писати власні матчери, наприклад toHaveValidIban, які повертають pass і message.

На співбесіді: expect.soft я використовую для незалежних перевірок однієї сторінки, щоб побачити всі розбіжності за один прогін, але не для передумов. До важливих асертів додаю власне повідомлення, щоб падіння у звіті було зрозуміле без коду.