Конфиг Playwright задаёт правила для всего набора, и его легко превратить в свалку скопированных из интернета опций, смысла которых никто в команде не помнит. Профессиональный playwright.config.ts читается как набор осознанных решений: где лежат тесты, как ведёт себя прогон локально и на CI, что записывается при падении и в каких браузерах всё проверяется. Каждая опция должна отвечать на конкретный вопрос эксплуатации, а не стоять "на всякий случай".
Начинается конфиг с базовых правил прогона. testDir указывает каталог тестов, fullyParallel включает параллельность на уровне файлов, а forbidOnly, привязанный к переменной CI, роняет сборку, если в код просочился забытый test.only - иначе на CI молча выполнится один тест вместо всех. Это не косметика, а страховка: одна забытая пометка способна тихо отключить весь набор в пайплайне.
Поведение на CI и локально сознательно разное. retries на CI ставят в 2, чтобы редкая инфраструктурная нестабильность не роняла пайплайн, но локально держат в нуле, чтобы флак был виден сразу. workers на CI фиксируют под ресурсы машины. reporter тоже разветвляют: на CI - html без автооткрытия плюс github для аннотаций прямо в PR, локально - html плюс list для живого вывода в терминал.
Секция use задаёт общий контекст для всех тестов. baseURL позволяет писать page.goto('/login') вместо полного адреса и менять окружение одной строкой. Три опции артефактов работают в паре с падением: trace: 'on-first-retry' записывает подробный trace только при повторной попытке, screenshot: 'only-on-failure' и video: 'retain-on-failure' сохраняют картинку и видео лишь у упавших. Так диагностика есть там, где нужна, но не раздувает каждый успешный прогон.
Отдельный блок - webServer: Playwright сам поднимает приложение перед тестами. command разветвляют - на CI собранный start, локально dev, - url задаёт, чего дождаться перед стартом сценариев, reuseExistingServer вне CI переиспользует уже запущенный сервер, а timeout даёт приложению время подняться. Это убирает целый класс гонок "тесты стартовали раньше, чем сервер".
Массив projects описывает браузеры, в которых идёт прогон: chromium, firefox и webkit через готовые devices. Каждый project - именованная конфигурация среды, и позже сюда добавятся устройства, локали и зависимости вроде setup-проекта авторизации. Полезно один раз собрать весь конфиг целиком, чтобы видеть, как эти части складываются в одно объяснимое поведение прогона.
Важный принцип - timeout не лекарство. Соблазн вылечить один медленный или нестабильный сценарий, подняв глобальный timeout, почти всегда вреден: он маскирует настоящую причину и замедляет весь набор, растягивая ожидание для всех тестов сразу. Правильный путь - сначала выяснить, чего именно ждёт сценарий, и, если операция объективно долгая, задать локальный лимит только ей, а не всему прогону.
Типичные провалы конфига растут из бездумного копирования. Глобально задранный timeout, прячущий флак вместо его починки; одинаковые retries локально и на CI, из-за которых нестабильность либо не видна, либо роняет пайплайн; отсутствие forbidOnly, позволяющее забытому only отключить набор. Держите конфиг минимальным и объяснимым: каждая опция - ответ на реальный вопрос эксплуатации, а не строчка из чужого примера.
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: process.env.CI
? [['html', { open: 'never' }], ['github']]
: [['html', { open: 'never' }], ['list']],
use: {
baseURL: 'http://127.0.0.1:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
webServer: {
command: process.env.CI ? 'npm run start' : 'npm run dev',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
})