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

Playwright

Авторизація і storageState

Авторизація: storageState і setup-проєкт

StorageState — JSON-знімок стану контексту: cookies і localStorage для кожного origin. Логін виконують один раз і зберігають стан через page.context().storageState({ path: 'playwright/.auth/user.json' }), а тести підключають файл через use: { storageState }. Папку з файлами стану додають у .gitignore, бо там живі токени.

Рекомендований спосіб — setup-проєкт: окремий проєкт у конфізі з testMatch: /.*\.setup\.ts/, від якого інші проєкти залежать через dependencies. На відміну від globalSetup, setup-проєкт — це повноцінні тести: з фікстурами, ретраями, trace і відображенням у звіті. globalSetup лишається для речей без браузера, як підготовка БД чи запуск контейнерів.

Для кількох ролей роблять окремі файли стану (admin.json, user.json) і підключають їх через окремі проєкти або test.use у файлі. Коли в одному тесті потрібні дві ролі одночасно, створюють два контексти через browser.newContext({ storageState }). SessionStorage у storageState не зберігається — його прокидають вручну через addInitScript.

Логін через API в setup-проєкті

Setup-проєкт не обов'язково має логінитися через UI. Якщо логін-форму вже покрито окремими тестами, швидше отримати сесію через API:

// auth.setup.ts
import { test as setup, expect } from '@playwright/test';

setup('логін адміністратора', async ({ request }) => {
  const res = await request.post('/api/auth/login', {
    data: { email: process.env.ADMIN_EMAIL, password: process.env.ADMIN_PASSWORD },
  });
  await expect(res).toBeOK();
  await request.storageState({ path: 'playwright/.auth/admin.json' });
});

Це працює, якщо сесія тримається на cookies. Якщо застосунок зберігає токен у localStorage, після API-логіну треба відкрити сторінку й записати токен через page.evaluate, а потім зберегти page.context().storageState().

Важливе обмеження спільного стану: усі воркери використовують одного користувача. Якщо тести змінюють стан акаунта (баланс, налаштування, кошик), вони почнуть заважати один одному паралельно. Тоді переходять на акаунт на воркер: worker-фікстура логінить окремого користувача з індексом test.info().parallelIndex і зберігає стан у файл з цим індексом.

Ще один нюанс — термін дії токена. Якщо сесія живе 15 хвилин, а прогін триває 40, тести наприкінці почнуть падати з 401. Тоді або збільшують TTL на тестовому середовищі, або перелогінюються у фікстурі.

На співбесіді: У setup-проєкті я логінюсь через API і зберігаю storageState, бо це швидше за UI. Якщо тести змінюють дані акаунта, використовую окремого користувача на кожен воркер через parallelIndex. Також враховую TTL сесії відносно тривалості прогону.

Кілька ролей і розлогінений стан

Якщо файл тестує лише одну роль, достатньо test.use({ storageState: 'playwright/.auth/admin.json' }) на початку файлу чи describe. Якщо в одному тесті потрібні дві ролі одночасно, зручно зробити фікстури, що повертають готові сторінки:

type Roles = { adminPage: Page; clientPage: Page };

export const test = base.extend<Roles>({
  adminPage: async ({ browser }, use) => {
    const ctx = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
    await use(await ctx.newPage());
    await ctx.close();
  },
  clientPage: async ({ browser }, use) => {
    const ctx = await browser.newContext({ storageState: 'playwright/.auth/client.json' });
    await use(await ctx.newPage());
    await ctx.close();
  },
});

Тест тоді виглядає як сценарій: клієнт подає заявку на кредит, адміністратор її схвалює, клієнт бачить новий статус.

Протилежна ситуація: глобально налаштований storageState, але треба тест для неавторизованого користувача. Тоді стан скидають: test.use({ storageState: { cookies: [], origins: [] } }).

SessionStorage у storageState не потрапляє. Якщо застосунок тримає сесію там, її зчитують у setup через page.evaluate і відновлюють через context.addInitScript, який виконується перед скриптами сторінки.

Ролі також можна розвести по проєктах у конфізі: проєкт admin з одним файлом стану і testMatch для адмінських тестів, проєкт client — з іншим.

На співбесіді: Для сценаріїв з двома ролями я роблю фікстури adminPage і clientPage, кожна з власним контекстом і storageState. Для тестів неавторизованого користувача скидаю стан через порожній storageState. SessionStorage відновлюю через addInitScript.

globalSetup чи setup-проєкт: порівняння

Обидва механізми виконують підготовку до тестів, але різниця суттєва:

МожливістьglobalSetupsetup-проєкт
Видно в HTML-звітінітак
Trace і скріншотилише вручнуяк у звичайних тестах
Фікстури page, requestнітак
Ретраїнітак
Можна запустити частинунічерез dependencies

globalSetup — це функція в окремому файлі, яка виконується один раз перед усім прогоном. Вона отримує конфіг і може передати значення тестам через process.env. Якщо вона повертає функцію, та стане teardown.

Для setup-проєкту прибирання робиться через властивість teardown у проєкті: вказується назва проєкту, який запуститься після завершення всіх залежних.

projects: [
  { name: 'setup', testMatch: /global\.setup\.ts/, teardown: 'cleanup' },
  { name: 'cleanup', testMatch: /global\.teardown\.ts/ },
  { name: 'e2e', dependencies: ['setup'], use: { storageState: 'playwright/.auth/user.json' } },
],

Мій вибір: усе, що стосується браузера й логіну, — у setup-проєкт. globalSetup лишаю для інфраструктури: підняти контейнери, накотити міграції, перевірити доступність середовища.

Нюанс: якщо запустити тест з фільтром, наприклад npx playwright test checkout, setup-проєкт все одно виконається, бо він у dependencies. Пропустити його можна прапорцем --no-deps, коли стан уже збережений.

На співбесіді: Setup-проєкт кращий для логіну, бо має trace, ретраї, фікстури й видно у звіті, а прибирання робиться через властивість teardown. globalSetup я лишаю для інфраструктурної підготовки без браузера.