core code helper

Playwright: стабільні E2E-тести без sleep

Чому тести «миготять» і як Playwright допомагає цього уникнути: локатори за роллю, автоочікування, web-first assertions, ізоляція даних і трасування падінь.

0 імпортували Імпортувати собі

Головна біда 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 Інші способи входу