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, setChecked | visible, stable, receives events, enabled |
| hover, dragTo | visible, stable, receives events |
| fill, clear | visible, enabled, editable |
| selectOption | visible, enabled |
| screenshot | visible, 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 я використовую для незалежних перевірок однієї сторінки, щоб побачити всі розбіжності за один прогін, але не для передумов. До важливих асертів додаю власне повідомлення, щоб падіння у звіті було зрозуміле без коду.