Почти каждый содержательный сценарий требует авторизованного пользователя, и наивный путь - логиниться через форму в начале каждого теста - убивает набор дважды. Он медленный: сотня тестов проходит форму входа сотню раз. И хрупкий: любая нестабильность страницы логина роняет все сценарии разом, хотя вход к их сути отношения не имеет. Профессиональный подход разделяет вход и использование: авторизуются один раз, а состояние переиспользуют многократно.
Механизм строится вокруг storageState - снимка состояния авторизации. После успешного входа Playwright умеет сохранить cookies и localStorage контекста в файл, а потом стартовать новый контекст сразу с этим состоянием, минуя форму. Это тот же способ, каким браузер помнит вас между сессиями: не повторный логин, а восстановление уже полученного состояния. Один раз вошли - дальше все тесты стартуют уже авторизованными.
Логин выносят в отдельный setup-тест. Файл auth.setup.ts проходит форму входа один раз - как обычный сценарий, с getByLabel и getByRole, - дожидается признака успешного входа и вызывает context().storageState({ path }), сохраняя состояние в файл. Учётные данные при этом берут из переменных окружения, а не хардкодят: секретам в коде теста не место. Это единственное место, где вход проходит по-настоящему.
Связывают setup и основные тесты через projects с зависимостью. Отдельный project с именем setup ловит по testMatch файлы вида *.setup.ts, а основной project браузера объявляет dependencies на него и подхватывает готовый storageState через use. Playwright сам гарантирует порядок: setup отрабатывает раньше, кладёт файл состояния, и только потом стартуют основные сценарии - уже авторизованными, без единого повторного логина.
Полезно один раз увидеть обе части рядом - setup-тест, который сохраняет состояние, и конфигурацию projects, которая его подхватывает. Ниже - файл авторизации и связка проектов с dependencies; к этой паре возвращаются при заведении нового окружения или при добавлении второй роли, которой нужен собственный сохранённый вход.
Сохранённое состояние - чувствительный файл. В storageState лежат живые cookies сессии, которыми можно войти под пользователем; относиться к нему надо как к секрету. Каталог с этими файлами обязательно добавляют в .gitignore, чтобы состояние авторизации не утекло в репозиторий и историю. Это не формальность: закоммиченный storageState - это, по сути, выложенные в git ключи от аккаунта.
Роли и общий аккаунт требуют отдельного внимания. Разным ролям - админ, обычный пользователь, гость - заводят отдельные storageState и отдельные setup-проекты, чтобы сценарий сразу стартовал в нужной роли. А если тесты изменяют серверное состояние аккаунта - меняют профиль, настройки, баланс, - общий аккаунт на всех приведёт к гонкам; тогда создают отдельный аккаунт на worker или сценарий, чтобы изменения не пересекались.
Типичные провалы авторизации предсказуемы. Логин через UI в каждом тесте - медленный набор, где падение формы входа рушит всё сразу. Хардкод пароля прямо в тесте - секрет в коде и в истории git. Закоммиченный каталог .auth - утёкшие cookies сессии. И один общий аккаунт под тестами, которые его изменяют, - гонки на server-side состоянии. Авторизуйтесь один раз в setup, храните состояние вне git и разводите роли и изменяемые аккаунты.
// auth.setup.ts - вход один раз, состояние в файл
import { test as setup, expect } from '@playwright/test'
import path from 'node:path'
const authFile = path.join(import.meta.dirname, '.auth/user.json')
setup('authenticate', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill(process.env.E2E_EMAIL!)
await page.getByLabel('Пароль').fill(process.env.E2E_PASSWORD!)
await page.getByRole('button', { name: 'Войти' }).click()
await expect(page).toHaveURL('/dashboard')
await page.context().storageState({ path: authFile })
})// playwright.config.ts - setup идёт раньше, основной project берёт состояние
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
]