Сохранение сессии, cookies, localStorage, обход login-формы, multi-factor auth
Большинство веб-приложений требуют авторизации. Повторный логин в каждом тесте замедляет прогон в 5-10 раз. Playwright предоставляет эффективные механизмы управления сессиями.
Простейший подход — заполнить форму логина:
def test_dashboard(logged_in_page):
logged_in_page.goto('/dashboard')
assert logged_in_page.get_by_role('heading', name='Добро пожаловать').is_visible()
@pytest.fixture
def logged_in_page(page):
page.goto('/login')
page.get_by_label('Email').fill('user@example.com')
page.get_by_label('Пароль').fill('secret123')
page.get_by_role('button', name='Войти').click()
page.wait_for_url('**/dashboard')
yield pageПроблема: каждый тест загружает страницу, заполняет поля, ждёт ответа сервера. Это медленно.
Решение: залогиниться один раз, сохранить cookies и localStorage, переиспользовать в тестах.
# conftest.py
import pytest
from pathlib import Path
AUTH_FILE = Path('auth/user.json')
@pytest.fixture(scope='session')
def saved_auth(browser):
"""Логин один раз на сессию и сохранение состояния."""
context = browser.new_context()
page = context.new_page()
page.goto('/login')
page.get_by_label('Email').fill('user@example.com')
page.get_by_label('Пароль').fill('secret123')
page.get_by_role('button', name='Войти').click()
page.wait_for_url('**/dashboard')
# Сохраняем cookies + localStorage + sessionStorage
context.storage_state(path=AUTH_FILE)
context.close()
yield
@pytest.fixture
def user_page(page):
"""Каждый тест получает уже авторизованную страницу."""
# Пересоздаём контекст с сохранённой сессией
page.context.storage_state(path=AUTH_FILE)
yield pageТеперь тесты мгновенно получают авторизованную сессию:
def test_settings(user_page):
user_page.goto('/settings')
# Уже авторизован — редиректа на /login нет
assert user_page.url.endswith('/settings')Файл auth.json выглядит так:
{
"cookies": [
{
"name": "session_id",
"value": "abc123",
"domain": ".example.com",
"path": "/",
"expires": 1735689600
}
],
"origins": [
{
"origin": "https://example.com",
"localStorage": [
{"name": "auth_token", "value": "jwt-token-here"}
]
}
]
}Сохраняются: cookies (включая httpOnly), localStorage, sessionStorage. Не сохраняется: состояние IndexedDB, кэш браузера.
Если у приложения есть login API — используйте его. Это ещё быстрее:
@pytest.fixture(scope='session')
def api_auth(browser, base_url):
"""Авторизация через API, без загрузки страницы логина."""
context = browser.new_context()
page = context.new_page()
# Вызываем login API напрямую
response = page.request.post(f'{base_url}/api/auth/login', data={
'email': 'user@example.com',
'password': 'secret123',
})
assert response.status == 200
# Сохраняем сессию (cookies уже установлены)
context.storage_state(path=AUTH_FILE)
context.close()
yieldpage.request — встроенный APIRequestContext Playwright. Он работает в контексте страницы, поэтому cookies автоматически сохраняются.
Если приложение использует JWT в localStorage:
@pytest.fixture
def jwt_page(page):
"""Установка JWT-токена напрямую в localStorage."""
page.goto('/') # Нужно зайти на домен, чтобы localStorage был доступен
page.evaluate('''
localStorage.setItem('auth_token', 'eyJhbGciOiJIUzI1NiIs...')
''')
page.reload() # Перезагрузка для применения токена
yield pageВажно:
localStorageдоступен только после навигации на домен. Сначалаpage.goto('/'), потомevaluate().
Создайте auth-файл для каждой роли:
# conftest.py
AUTH_FILES = {
'admin': Path('auth/admin.json'),
'manager': Path('auth/manager.json'),
'viewer': Path('auth/viewer.json'),
}
CREDENTIALS = {
'admin': ('admin@example.com', 'admin_pass'),
'manager': ('manager@example.com', 'manager_pass'),
'viewer': ('viewer@example.com', 'viewer_pass'),
}
@pytest.fixture
def page_as(browser, base_url, request):
"""Фикстура с параметризованной ролью."""
role = request.param
email, password = CREDENTIALS[role]
auth_file = AUTH_FILES[role]
context = browser.new_context()
page = context.new_page()
page.goto(f'{base_url}/login')
page.get_by_label('Email').fill(email)
page.get_by_label('Пароль').fill(password)
page.get_by_role('button', name='Войти').click()
page.wait_for_url('**/dashboard')
context.storage_state(path=auth_file)
context.close()
# Новый контекст с auth
auth_context = browser.new_context(storage_state=auth_file)
auth_page = auth_context.new_page()
yield auth_page
auth_context.close()
@pytest.mark.parametrize('page_as', ['admin', 'manager', 'viewer'], indirect=True)
def test_dashboard_for_roles(page_as):
page_as.goto('/dashboard')
assert page_as.get_by_role('heading').is_visible()Для тестирования обработки expired token:
import json
import time
from pathlib import Path
def expire_auth_file(auth_path: Path):
"""Устанавливает время истечения cookies в прошлое."""
data = json.loads(auth_path.read_text())
for cookie in data['cookies']:
cookie['expires'] = int(time.time()) - 1000 # Истёк час назад
auth_path.write_text(json.dumps(data))
def test_expired_token(page, base_url):
expire_auth_file(Path('auth/user.json'))
page.context.storage_state(path=Path('auth/user.json'))
page.goto(base_url + '/dashboard')
# Приложение должно показать форму логина
assert '/login' in page.urlОбход 2FA через сохранение сессии:
storage_stateЕсли сессия сбрасывается — используйте API для генерации TOTP-кодов:
import pyotp
def get_totp_code(secret: str) -> str:
"""Генерирует текущий TOTP-код из секрета."""
totp = pyotp.TOTP(secret)
return totp.now()
# В тесте
page.get_by_label('Код подтверждения').fill(get_totp_code('JBSWY3DPEHPK3PXP'))# Очистить cookies
await context.clear_cookies()
# Очистить localStorage
await page.evaluate('localStorage.clear()')
# Полная очистка — закрыть контекст
await context.close()❌ Использование одного auth.json для разных доменов. Cookies привязаны к домену. auth.json от staging.example.com не сработает на prod.example.com.
❌ Хранение auth.json в Git. Токены могут попасть в репозиторий. Добавьте auth/ в .gitignore и генерируйте файлы в CI.
❌ Попытка установить localStorage до навигации. page.evaluate('localStorage...') до page.goto() вызовет ошибку — нет origin.
✅ Правильный подход: storage_state для скорости, API-login для ещё большей скорости, отдельные файлы для каждой роли, очистка в teardown.
auth.json.page.request.post() к login API.expires в auth.json и проверьте редирект на /login.Изучите визуальное тестирование, чтобы научиться сравнивать скриншоты с эталонами.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.