У більшості enterprise-застосунків переважна більшість користувацьких сценаріїв вимагає автентифікації. Якщо кожен тест виконує вхід заново через графічну форму в хуку beforeEach, виникає катастрофічна деградація продуктивності тестового контуру:
/login, рендеринг форми, введення символів, очікування відповіді API та редирект на /dashboard займають від 2 до 4 секунд на кожен тест.+-----------------------------------------------------------------------------+
| Традиційний підхід (Антипатерн: Логін у кожному тесті) |
| |
| Test 1: [UI Login: 3s] -> [Test Logic: 1s] = 4s |
| Test 2: [UI Login: 3s] -> [Test Logic: 0.5s] = 3.5s |
| Test 3: [UI Login: 3s] -> [Test Logic: 2s] = 5s |
| РАЗОМ на 100 тестів: ~300 секунд (5 хвилин) марно на введення пароля! |
+-----------------------------------------------------------------------------+
+-----------------------------------------------------------------------------+
| Архітектура Playwright: Storage State Reuse 2026 |
| |
| Setup Project (1 раз): [API/UI Login: 1.5s] -> Збереження у storageState |
| │ |
| ┌──────────────────────────────┴────────────────┐ |
| ▼ ▼ |
| Worker 1 (Context with storageState) Worker 2 (Context with storageState)
| Test 1: [Test Logic: 1s] Test 2: [Test Logic: 0.5s] |
| Test 3: [Test Logic: 2s] Test 4: [Test Logic: 1.2s] |
| РАЗОМ на 100 тестів: ~35 секунд сумарного часу виконання |
+-----------------------------------------------------------------------------+Playwright кардинально змінює цей процес, розділяючи авторизацію та виконання сценаріїв через механізм Storage State та граф залежностей проєктів Project Dependencies.
В архітектурі Playwright процес тестування з авторизацією ділиться на дві незалежні фази:
cookies та localStorage) на диск у JSON-артефакт.BrowserContext, миттєво гідратуючи їх готовим знімком storageState.json. Контекст одразу відкриває захищені сторінки застосунку без проходження форми логіна.+-----------------------------------------------------------------------------+
| 1. SETUP PROJECT (setup) |
| |
| auth.setup.ts ──► POST /api/auth/login ──► Отримання JWT + Set-Cookie |
| │ |
| ▼ |
| page.context().storageState({ path: '.auth/admin.json' }) |
| page.context().storageState({ path: '.auth/user.json' }) |
+-----------------------------------------------------------------------------+
│
▼ (Запис JSON-артефактів на диск)
+-----------------------------------------------------------------------------+
| .auth/ Directory (Git-ignored) |
| - admin.json (Cookies + LocalStorage з роллю Administrator) |
| - user.json (Cookies + LocalStorage з роллю Standard Customer) |
+-----------------------------------------------------------------------------+
│
┌────────────────────────┴────────────────────────┐
▼ ▼
+------------------------------------+ +------------------------------------+
| Project: "admin-tests" | | Project: "customer-tests" |
| use: { | | use: { |
| storageState: '.auth/admin.json'| | storageState: '.auth/user.json' |
| } | | } |
| | | |
| Worker A: dashboard.spec.ts | | Worker C: profile.spec.ts |
| Worker B: settings.spec.ts | | Worker D: orders.spec.ts |
+------------------------------------+ +------------------------------------+storageState.json#JSON-файл стану має сувору структуру, що описує куки та сховища за доменними джерелами (Origins):
{
"cookies": [
{
"name": "session_token",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"domain": "localhost",
"path": "/",
"expires": 1789038400,
"httpOnly": true,
"secure": false,
"sameSite": "Lax"
}
],
"origins": [
{
"origin": "http://localhost:3000",
"localStorage": [
{
"name": "user_meta",
"value": "{\"id\":\"usr_01\",\"role\":\"admin\",\"theme\":\"dark\"}"
}
]
}
]
}playwright.config.ts#import { defineConfig, devices } from '@playwright/test';
import path from 'path';
/** Шляхи до серіалізованих станів ролей */
export const ADMIN_AUTH_FILE = path.join(__dirname, '.auth/admin.json');
export const USER_AUTH_FILE = path.join(__dirname, '.auth/user.json');
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? '100%' : undefined,
projects: [
// 1. Проєкт підготовки авторизації (Запускається першим)
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// 2. Тести з правами адміністратора
{
name: 'admin-chromium',
testMatch: /.*admin.*\.spec\.ts/,
use: {
...devices['Desktop Chrome'],
storageState: ADMIN_AUTH_FILE,
},
dependencies: ['setup'], // Сувора залежність від завершення setup
},
// 3. Тести з правами звичайного користувача
{
name: 'user-chromium',
testMatch: /.*user.*\.spec\.ts/,
use: {
...devices['Desktop Chrome'],
storageState: USER_AUTH_FILE,
},
dependencies: ['setup'],
},
// 4. Публічні тести (Гість / Unauthenticated)
{
name: 'public-chromium',
testMatch: /.*public.*\.spec\.ts/,
use: {
...devices['Desktop Chrome'],
// storageState не вказується — чистий контекст інкогніто
},
},
],
});auth.setup.ts#Введення логіна та пароля через візуальні форми в UI повільніше за прямий HTTP-запит у 10–20 разів. У проєкті setup найбільш ефективно відправити прямий запит до ендпоінта бекенда через об'єкт request, отримати куки та записати їх у storageState.
import { test as setup, expect } from '@playwright/test';
import { ADMIN_AUTH_FILE, USER_AUTH_FILE } from '../playwright.config';
setup.describe('Глобальна ініціалізація авторизаційних станів', () => {
setup('авторизація суперадміністратора через API та збереження сесії', async ({ request, page }) => {
// 1. Виконуємо швидкий HTTP-запит до API автентифікації (без відкриття браузерного UI)
const loginResponse = await request.post('/api/auth/login', {
data: {
email: process.env.E2E_ADMIN_EMAIL ?? '[email protected]',
password: process.env.E2E_ADMIN_PASSWORD ?? 'SecureEnterprisePassword2026!',
},
});
expect(loginResponse.ok()).toBeTruthy();
const { token, user } = (await loginResponse.json()) as { token: string; user: { id: string; role: string } };
// 2. Переходимо на базовий домен, щоб зареєструвати localStorage у правильному Origin
await page.goto('/favicon.ico');
// 3. Інжектуємо дані в localStorage через evaluate
await page.evaluate(
({ authToken, userData }) => {
localStorage.setItem('auth_token', authToken);
localStorage.setItem('user_profile', JSON.stringify(userData));
},
{ authToken: token, userData: user }
);
// 4. Зберігаємо об'єднаний стан (куки від API-відповіді + localStorage) у файл
await page.context().storageState({ path: ADMIN_AUTH_FILE });
});
setup('авторизація стандартного користувача через UI форму (якщо немає API)', async ({ page }) => {
// Альтернативний сценарій через UI (якщо авторизація включає CSRF або складні редиректи)
await page.goto('/login');
await page.getByLabel('E-mail').fill(process.env.E2E_USER_EMAIL ?? '[email protected]');
await page.getByLabel('Пароль').fill(process.env.E2E_USER_PASSWORD ?? 'StandardPassword2026!');
await page.getByRole('button', { name: 'Увійти в акаунт' }).click();
// Очікуємо успішного переходу на дашборд
await page.waitForURL('**/dashboard');
await expect(page.getByRole('heading', { name: 'Мої курси' })).toBeVisible();
// Зберігаємо стан сесії користувача
await page.context().storageState({ path: USER_AUTH_FILE });
});
});У тестах, які запускаються в проєкті admin-chromium, сторінка одразу відкривається в авторизованому стані:
import { test, expect } from '@playwright/test';
test.describe('Панель адміністрування', () => {
test('адміністратор може змінити ліміти генерації токенів', async ({ page }) => {
// Вхід уже виконано! Направляємося одразу на закритий екран налаштувань
await page.goto('/admin/security/token-limits');
// Перевіряємо наявність захищених елементів інтерфейсу
await expect(page.getByRole('heading', { name: 'Налаштування безпеки' })).toBeVisible();
const limitInput = page.getByLabel('Максимум токенів на воркер');
await limitInput.fill('64');
await page.getByRole('button', { name: 'Зберегти зміни' }).click();
await expect(page.getByRole('status')).toHaveText('Зміни успішно застосовано');
});
});Якщо сценарій перевіряє матрицю контролю доступу (RBAC) і вимагає перемикання ролей всередині одного тестового методу:
import { test as base, type Page } from '@playwright/test';
import { ADMIN_AUTH_FILE, USER_AUTH_FILE } from '../../playwright.config';
export interface RoleFixtures {
adminPage: Page;
userPage: Page;
}
export const test = base.extend<RoleFixtures>({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: ADMIN_AUTH_FILE });
const page = await context.newPage();
await use(page);
await context.close();
},
userPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: USER_AUTH_FILE });
const page = await context.newPage();
await use(page);
await context.close();
},
});
export { expect } from '@playwright/test';Файли авторизаційних станів містять живі JWT-токени, хеші сесій та персональні дані тестових облікових записів. Коміт цих файлів у систему контролю версій створює критичну вразливість безпеки, а також призводить до падіння тестів у інших розробників через закінчення терміну дії (expiration) токена.
# ❌ Антипатерн: відсутність директорії .auth у .gitignore
# ✅ Обов'язкове додавання в кореневий .gitignore проєкту:
.auth/
playwright/.auth/Якщо 20 паралельних тестів запускаються під одним обліковим записом адміністратора й один із тестів змінює профіль (наприклад, змінює мову інтерфейсу на німецьку або видаляє організацію), решта 19 тестів падають із непередбачуваними помилками (Race Condition). Рішення: використовувати спільну пред-авторизовану сесію суворо для сценаріїв читання (Read-Only). Для деструктивних сценаріїв (мутації налаштувань, видалення профілю) генерувати ізольованих тимчасових користувачів (Ephemeral Users) через API-фікстури.
Якщо в застосунку JWT має час життя 5 хвилин, а повний тестовий набір у CI виконується 12 хвилин, тести в кінці черги почнуть масово падати з кодом 401 Unauthorized.
Рішення: для тестового оточення сконфігурувати тестовий секретний ключ або спеціальний ендпоінт, що генерує токени зі збільшеним часом життя (наприклад, 2 години), або налаштувати періодичне оновлення токена (Refresh Token) у фікстурі.
Якщо не вказати dependencies: ['setup'] у проєктах тестів, Playwright почне виконувати основні тести одночасно з auth.setup.ts. Тести спробують прочитати ще не створений файл .auth/admin.json і миттєво впадуть із помилкою ENOENT: no such file or directory.
Реєстрація проєкту setup у playwright.config.ts із файлом ініціалізації авторизації.
Виконання HTTP-запиту на /api/auth/login або разове проходження форми авторизації в браузері.
Виклик page.context().storageState({ path: '.auth/role.json' }) для збереження кук та localStorage.
Додавання прапорця dependencies: ['setup'] та опції storageState у конфігурації робочих проєктів.
Сотні тестів миттєво стартують в авторизованому стані без жодної затримки на введення логіна.