В большинстве 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' }) |
+-----------------------------------------------------------------------------+
│
▼ (Запись артефактов на диск)
+-----------------------------------------------------------------------------+
| .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 |
+------------------------------------+ +------------------------------------+Регистрация проекта setup в playwright.config.ts с файлом инициализации авторизации.
Выполнение HTTP-запроса на /api/auth/login или разовое прохождение формы авторизации в браузере.
Вызов page.context().storageState({ path: '.auth/role.json' }) для сохранения кук и localStorage.
Добавление флага dependencies: ['setup'] и опции storageState в конфигурации рабочих проектов.
Сотни тестов мгновенно стартуют в авторизованном состоянии без единой задержки на ввод логина.
Почему разделение авторизации на отдельный Setup Project с механизмом dependencies: ['setup'] эффективнее ручного логина в хуках beforeEach каждого теста?
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';Почему файлы .auth/*.json категорически запрещено коммитить в Git, и какой риск возникает при параллельном запуске тестов под одной учетной записью?
Файлы авторизационных состояний содержат живые 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.