Playwright: стабільні E2E-тести без sleep
Чому тести «миготять» і як Playwright допомагає цього уникнути: локатори за роллю, автоочікування, web-first assertions, ізоляція даних і трасування падінь.
Головна біда E2E-тестів — нестабільність (flaky): сьогодні тест проходить, завтра падає без жодних змін у коді. Найчастіші причини — жорсткі очікування, крихкі селектори і спільні тестові дані. Playwright розв'язує більшість із них «з коробки», якщо ним правильно користуватися.
Забудьте про sleep
// ❌ Погано: вгадуємо час. На повільному CI — падає, на швидкому — марнуємо секунди
await page.click('#submit');
await page.waitForTimeout(3000);
expect(await page.textContent('.toast')).toBe('Збережено');
// ✅ Добре: Playwright сам чекає, поки елемент з'явиться і текст збіжеться
await page.getByRole('button', { name: 'Зберегти' }).click();
await expect(page.getByRole('status')).toHaveText('Збережено');Перед кожною дією Playwright автоматично чекає, поки елемент буде видимим, стабільним (без анімації), увімкненим і зможе отримати клік. А expect(...) з локатором — це web-first assertion: він повторює перевірку до таймауту, а не перевіряє один раз.
Важливо
expect(await locator.textContent())— перевірка один раз, без повторів.await expect(locator).toHaveText()— з повторами. Різниця в одномуawaitвирішує, чи буде тест стабільним.
Локатори, що переживуть редизайн
Пріоритет вибору локатора:
| Спосіб | Приклад | Коли |
|---|---|---|
| Роль + доступне ім'я | getByRole('button', { name: 'Оплатити' }) |
За замовчуванням |
| Підпис поля | getByLabel('Email') |
Поля форм |
| Текст | getByText('Кошик порожній') |
Повідомлення, статичний текст |
| Тестовий ID | getByTestId('cart-total') |
Коли немає ролі й тексту |
| CSS / XPath | locator('.btn-primary >> nth=2') |
Майже ніколи |
Локатори за роллю перевіряють ще й доступність: якщо кнопку не знайти за роллю, її, ймовірно, не знайде і скрінрідер.
const row = page.getByRole('row', { name: /Замовлення #1042/ });
await row.getByRole('button', { name: 'Скасувати' }).click();
await expect(row.getByRole('cell', { name: 'Скасовано' })).toBeVisible();Ізоляція тестів
Кожен тест має бути незалежним: власний контекст браузера (Playwright створює новий за замовчуванням) і власні тестові дані.
import { test as base, expect } from '@playwright/test';
type Fixtures = { user: { email: string; password: string } };
export const test = base.extend<Fixtures>({
user: async ({ request }, use) => {
// Створюємо користувача через API — швидко і без залежності від UI реєстрації
const email = `qa+${Date.now()}-${Math.random().toString(36).slice(2, 6)}@example.com`;
const res = await request.post('/api/test/users', { data: { email, password: 'Test-12345' } });
expect(res.ok()).toBeTruthy();
await use({ email, password: 'Test-12345' });
await request.delete(`/api/test/users/${encodeURIComponent(email)}`);
},
});ПорадаНе логіньтеся через UI в кожному тесті. Збережіть стан автентифікації один раз у
setup-проєкті (storageState) і перевикористовуйте його. Вхід через форму перевіряйте окремим тестом.
Мокання мережі там, де це доречно
Сторонні сервіси (платіжні шлюзи, карти, аналітика) — часте джерело нестабільності. Їх можна підмінити:
await page.route('**/api/exchange-rates', (route) =>
route.fulfill({ json: { USD: 41.5, EUR: 48.2 } }),
);Але не мокайте все: E2E цінний саме тим, що перевіряє реальну інтеграцію фронтенду й бекенду.
Коли тест таки впав: trace viewer
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 1 : 0,
use: {
trace: 'on-first-retry', // запис трасування при повторі
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});Трасування містить кожну дію, DOM-знімок до і після, мережеві запити й консоль. Відкривається командою npx playwright show-trace trace.zip — і в більшості випадків причина видна за хвилину.
УвагаРетраї — це діагностика, а не лікування. Якщо тест проходить лише з другої спроби — він нестабільний, і це треба виправити. Слідкуйте за звітом
flakyпісля кожного прогону.
Чекліст стабільного тесту
- жодного
waitForTimeout; - локатори за роллю, підписом або test-id;
- лише web-first assertions (
await expect(locator)...); - власні дані, створені через API;
- не залежить від порядку запуску інших тестів;
- проходить 20 разів поспіль:
npx playwright test my.spec.ts --repeat-each=20.
Тест клікає кнопку, яка з'являється після завантаження списку. Іноді падає з «element is not attached to the DOM». Чому і як виправити?
Список перемальовується після відповіді сервера, і збережений раніше елемент зникає з DOM. Не зберігайте елементи в змінних через elementHandle — використовуйте локатори: Playwright шукає елемент заново перед кожною дією. Додатково можна дочекатися стану списку через await expect(list).toHaveCount(n).
Збережіть статтю собі
Увійдіть, щоб імпортувати її у власні нотатки, відмічати вивчене, ставити лайки й питати спільноту. Реєстрація безкоштовна: вхід через Google займає кілька секунд.
Увійти через Google Інші способи входу