mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 17:22:38 +02:00
730 lines
22 KiB
Markdown
730 lines
22 KiB
Markdown
|
|
# Configuration
|
||
|
|
|
||
|
|
> **When to use**: Setting up a new Playwright project, adjusting timeouts, adding browser targets, configuring CI behavior, or connecting environment-specific settings.
|
||
|
|
|
||
|
|
## Quick Reference
|
||
|
|
|
||
|
|
```
|
||
|
|
npx playwright init # scaffold config + first test
|
||
|
|
npx playwright test --config=custom.config.ts # use non-default config
|
||
|
|
npx playwright test --project=chromium # run single project
|
||
|
|
npx playwright test --reporter=html # override reporter
|
||
|
|
npx playwright show-report # open last HTML report
|
||
|
|
DEBUG=pw:api npx playwright test # verbose Playwright logging
|
||
|
|
```
|
||
|
|
|
||
|
|
## Production-Ready Config (Copy-Paste Starter)
|
||
|
|
|
||
|
|
### TypeScript
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// playwright.config.ts
|
||
|
|
import { defineConfig, devices } from '@playwright/test';
|
||
|
|
import dotenv from 'dotenv';
|
||
|
|
import path from 'path';
|
||
|
|
|
||
|
|
// Load environment variables from .env file
|
||
|
|
dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||
|
|
|
||
|
|
export default defineConfig({
|
||
|
|
// ── Test discovery ──────────────────────────────────────────────
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.ts',
|
||
|
|
|
||
|
|
// ── Execution ───────────────────────────────────────────────────
|
||
|
|
fullyParallel: true,
|
||
|
|
forbidOnly: !!process.env.CI, // fail CI if test.only left in
|
||
|
|
retries: process.env.CI ? 2 : 0, // retry flakes in CI only
|
||
|
|
workers: process.env.CI ? '50%' : undefined, // half CPU in CI, auto locally
|
||
|
|
|
||
|
|
// ── Reporting ───────────────────────────────────────────────────
|
||
|
|
reporter: process.env.CI
|
||
|
|
? [['html', { open: 'never' }], ['github']]
|
||
|
|
: [['html', { open: 'on-failure' }]],
|
||
|
|
|
||
|
|
// ── Timeouts ────────────────────────────────────────────────────
|
||
|
|
timeout: 30_000, // per-test timeout
|
||
|
|
expect: {
|
||
|
|
timeout: 5_000, // per-assertion retry timeout
|
||
|
|
},
|
||
|
|
|
||
|
|
// ── Shared browser context options ──────────────────────────────
|
||
|
|
use: {
|
||
|
|
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
||
|
|
actionTimeout: 10_000, // click, fill, etc.
|
||
|
|
navigationTimeout: 15_000, // goto, waitForURL, etc.
|
||
|
|
|
||
|
|
// Artifact collection
|
||
|
|
trace: 'on-first-retry', // full trace on first retry only
|
||
|
|
screenshot: 'only-on-failure', // screenshot on failure
|
||
|
|
video: 'retain-on-failure', // video only kept for failures
|
||
|
|
|
||
|
|
// Sensible defaults
|
||
|
|
locale: 'en-US',
|
||
|
|
timezoneId: 'America/New_York',
|
||
|
|
extraHTTPHeaders: {
|
||
|
|
'x-test-automation': 'playwright',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
|
||
|
|
// ── Projects (browser targets) ─────────────────────────────────
|
||
|
|
projects: [
|
||
|
|
{
|
||
|
|
name: 'chromium',
|
||
|
|
use: { ...devices['Desktop Chrome'] },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'firefox',
|
||
|
|
use: { ...devices['Desktop Firefox'] },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'webkit',
|
||
|
|
use: { ...devices['Desktop Safari'] },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'mobile-chrome',
|
||
|
|
use: { ...devices['Pixel 7'] },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'mobile-safari',
|
||
|
|
use: { ...devices['iPhone 14'] },
|
||
|
|
},
|
||
|
|
],
|
||
|
|
|
||
|
|
// ── Dev server ──────────────────────────────────────────────────
|
||
|
|
webServer: {
|
||
|
|
command: 'npm run dev',
|
||
|
|
url: 'http://localhost:3000',
|
||
|
|
reuseExistingServer: !process.env.CI,
|
||
|
|
timeout: 120_000, // 2 min for cold builds
|
||
|
|
stdout: 'pipe',
|
||
|
|
stderr: 'pipe',
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### JavaScript
|
||
|
|
|
||
|
|
```js
|
||
|
|
// playwright.config.js
|
||
|
|
const { defineConfig, devices } = require('@playwright/test');
|
||
|
|
const dotenv = require('dotenv');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||
|
|
|
||
|
|
module.exports = defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.js',
|
||
|
|
|
||
|
|
fullyParallel: true,
|
||
|
|
forbidOnly: !!process.env.CI,
|
||
|
|
retries: process.env.CI ? 2 : 0,
|
||
|
|
workers: process.env.CI ? '50%' : undefined,
|
||
|
|
|
||
|
|
reporter: process.env.CI
|
||
|
|
? [['html', { open: 'never' }], ['github']]
|
||
|
|
: [['html', { open: 'on-failure' }]],
|
||
|
|
|
||
|
|
timeout: 30_000,
|
||
|
|
expect: {
|
||
|
|
timeout: 5_000,
|
||
|
|
},
|
||
|
|
|
||
|
|
use: {
|
||
|
|
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
||
|
|
actionTimeout: 10_000,
|
||
|
|
navigationTimeout: 15_000,
|
||
|
|
|
||
|
|
trace: 'on-first-retry',
|
||
|
|
screenshot: 'only-on-failure',
|
||
|
|
video: 'retain-on-failure',
|
||
|
|
|
||
|
|
locale: 'en-US',
|
||
|
|
timezoneId: 'America/New_York',
|
||
|
|
extraHTTPHeaders: {
|
||
|
|
'x-test-automation': 'playwright',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
|
||
|
|
projects: [
|
||
|
|
{
|
||
|
|
name: 'chromium',
|
||
|
|
use: { ...devices['Desktop Chrome'] },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'firefox',
|
||
|
|
use: { ...devices['Desktop Firefox'] },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'webkit',
|
||
|
|
use: { ...devices['Desktop Safari'] },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'mobile-chrome',
|
||
|
|
use: { ...devices['Pixel 7'] },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'mobile-safari',
|
||
|
|
use: { ...devices['iPhone 14'] },
|
||
|
|
},
|
||
|
|
],
|
||
|
|
|
||
|
|
webServer: {
|
||
|
|
command: 'npm run dev',
|
||
|
|
url: 'http://localhost:3000',
|
||
|
|
reuseExistingServer: !process.env.CI,
|
||
|
|
timeout: 120_000,
|
||
|
|
stdout: 'pipe',
|
||
|
|
stderr: 'pipe',
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Patterns
|
||
|
|
|
||
|
|
### Pattern 1: Environment-Specific Configuration
|
||
|
|
|
||
|
|
**Use when**: Tests run against dev, staging, and production environments.
|
||
|
|
**Avoid when**: Single-environment local-only projects.
|
||
|
|
|
||
|
|
#### TypeScript
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// playwright.config.ts
|
||
|
|
import { defineConfig } from '@playwright/test';
|
||
|
|
import dotenv from 'dotenv';
|
||
|
|
import path from 'path';
|
||
|
|
|
||
|
|
// Load environment-specific .env file: .env.staging, .env.production, etc.
|
||
|
|
const ENV = process.env.TEST_ENV || 'local';
|
||
|
|
dotenv.config({ path: path.resolve(__dirname, `.env.${ENV}`) });
|
||
|
|
|
||
|
|
const envConfig: Record<string, { baseURL: string; retries: number }> = {
|
||
|
|
local: { baseURL: 'http://localhost:3000', retries: 0 },
|
||
|
|
staging: { baseURL: 'https://staging.example.com', retries: 2 },
|
||
|
|
production: { baseURL: 'https://www.example.com', retries: 2 },
|
||
|
|
};
|
||
|
|
|
||
|
|
const env = envConfig[ENV];
|
||
|
|
|
||
|
|
export default defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.ts',
|
||
|
|
retries: env.retries,
|
||
|
|
use: {
|
||
|
|
baseURL: env.baseURL,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Run against staging
|
||
|
|
TEST_ENV=staging npx playwright test
|
||
|
|
|
||
|
|
# Run against production (subset of smoke tests)
|
||
|
|
TEST_ENV=production npx playwright test --grep @smoke
|
||
|
|
```
|
||
|
|
|
||
|
|
#### JavaScript
|
||
|
|
|
||
|
|
```js
|
||
|
|
// playwright.config.js
|
||
|
|
const { defineConfig } = require('@playwright/test');
|
||
|
|
const dotenv = require('dotenv');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
const ENV = process.env.TEST_ENV || 'local';
|
||
|
|
dotenv.config({ path: path.resolve(__dirname, `.env.${ENV}`) });
|
||
|
|
|
||
|
|
const envConfig = {
|
||
|
|
local: { baseURL: 'http://localhost:3000', retries: 0 },
|
||
|
|
staging: { baseURL: 'https://staging.example.com', retries: 2 },
|
||
|
|
production: { baseURL: 'https://www.example.com', retries: 2 },
|
||
|
|
};
|
||
|
|
|
||
|
|
const env = envConfig[ENV];
|
||
|
|
|
||
|
|
module.exports = defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.js',
|
||
|
|
retries: env.retries,
|
||
|
|
use: {
|
||
|
|
baseURL: env.baseURL,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Pattern 2: Multi-Project with Setup Dependencies
|
||
|
|
|
||
|
|
**Use when**: Tests need shared authentication state or database seeding before running.
|
||
|
|
**Avoid when**: Tests are fully independent with no shared setup phase.
|
||
|
|
|
||
|
|
#### TypeScript
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// playwright.config.ts
|
||
|
|
import { defineConfig, devices } from '@playwright/test';
|
||
|
|
|
||
|
|
export default defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.ts',
|
||
|
|
|
||
|
|
projects: [
|
||
|
|
// Setup project runs first, saves auth state
|
||
|
|
{
|
||
|
|
name: 'setup',
|
||
|
|
testMatch: /global\.setup\.ts/,
|
||
|
|
},
|
||
|
|
|
||
|
|
// Browser projects depend on setup
|
||
|
|
{
|
||
|
|
name: 'chromium',
|
||
|
|
use: {
|
||
|
|
...devices['Desktop Chrome'],
|
||
|
|
storageState: 'playwright/.auth/user.json',
|
||
|
|
},
|
||
|
|
dependencies: ['setup'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'firefox',
|
||
|
|
use: {
|
||
|
|
...devices['Desktop Firefox'],
|
||
|
|
storageState: 'playwright/.auth/user.json',
|
||
|
|
},
|
||
|
|
dependencies: ['setup'],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// tests/global.setup.ts
|
||
|
|
import { test as setup, expect } from '@playwright/test';
|
||
|
|
|
||
|
|
const authFile = 'playwright/.auth/user.json';
|
||
|
|
|
||
|
|
setup('authenticate', async ({ page }) => {
|
||
|
|
await page.goto('/login');
|
||
|
|
await page.getByLabel('Email').fill('user@example.com');
|
||
|
|
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
|
||
|
|
await page.getByRole('button', { name: 'Sign in' }).click();
|
||
|
|
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
||
|
|
await page.context().storageState({ path: authFile });
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
#### JavaScript
|
||
|
|
|
||
|
|
```js
|
||
|
|
// playwright.config.js
|
||
|
|
const { defineConfig, devices } = require('@playwright/test');
|
||
|
|
|
||
|
|
module.exports = defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.js',
|
||
|
|
|
||
|
|
projects: [
|
||
|
|
{
|
||
|
|
name: 'setup',
|
||
|
|
testMatch: /global\.setup\.js/,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'chromium',
|
||
|
|
use: {
|
||
|
|
...devices['Desktop Chrome'],
|
||
|
|
storageState: 'playwright/.auth/user.json',
|
||
|
|
},
|
||
|
|
dependencies: ['setup'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'firefox',
|
||
|
|
use: {
|
||
|
|
...devices['Desktop Firefox'],
|
||
|
|
storageState: 'playwright/.auth/user.json',
|
||
|
|
},
|
||
|
|
dependencies: ['setup'],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
```js
|
||
|
|
// tests/global.setup.js
|
||
|
|
const { test: setup, expect } = require('@playwright/test');
|
||
|
|
|
||
|
|
const authFile = 'playwright/.auth/user.json';
|
||
|
|
|
||
|
|
setup('authenticate', async ({ page }) => {
|
||
|
|
await page.goto('/login');
|
||
|
|
await page.getByLabel('Email').fill('user@example.com');
|
||
|
|
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD);
|
||
|
|
await page.getByRole('button', { name: 'Sign in' }).click();
|
||
|
|
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
||
|
|
await page.context().storageState({ path: authFile });
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Pattern 3: `webServer` with Build Step
|
||
|
|
|
||
|
|
**Use when**: Tests need a running application server. Let Playwright manage the server lifecycle.
|
||
|
|
**Avoid when**: Testing against an already-deployed environment (staging/prod).
|
||
|
|
|
||
|
|
#### TypeScript
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// playwright.config.ts
|
||
|
|
import { defineConfig } from '@playwright/test';
|
||
|
|
|
||
|
|
export default defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.ts',
|
||
|
|
use: {
|
||
|
|
baseURL: 'http://localhost:3000',
|
||
|
|
},
|
||
|
|
webServer: {
|
||
|
|
command: process.env.CI
|
||
|
|
? 'npm run build && npm run start' // production build in CI
|
||
|
|
: 'npm run dev', // dev server locally
|
||
|
|
url: 'http://localhost:3000',
|
||
|
|
reuseExistingServer: !process.env.CI,
|
||
|
|
timeout: 120_000,
|
||
|
|
stdout: 'pipe',
|
||
|
|
stderr: 'pipe',
|
||
|
|
env: {
|
||
|
|
NODE_ENV: 'test',
|
||
|
|
DATABASE_URL: process.env.DATABASE_URL || 'postgresql://localhost:5432/test',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
#### JavaScript
|
||
|
|
|
||
|
|
```js
|
||
|
|
// playwright.config.js
|
||
|
|
const { defineConfig } = require('@playwright/test');
|
||
|
|
|
||
|
|
module.exports = defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.js',
|
||
|
|
use: {
|
||
|
|
baseURL: 'http://localhost:3000',
|
||
|
|
},
|
||
|
|
webServer: {
|
||
|
|
command: process.env.CI
|
||
|
|
? 'npm run build && npm run start'
|
||
|
|
: 'npm run dev',
|
||
|
|
url: 'http://localhost:3000',
|
||
|
|
reuseExistingServer: !process.env.CI,
|
||
|
|
timeout: 120_000,
|
||
|
|
stdout: 'pipe',
|
||
|
|
stderr: 'pipe',
|
||
|
|
env: {
|
||
|
|
NODE_ENV: 'test',
|
||
|
|
DATABASE_URL: process.env.DATABASE_URL || 'postgresql://localhost:5432/test',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Pattern 4: `globalSetup` / `globalTeardown`
|
||
|
|
|
||
|
|
**Use when**: One-time non-browser work: seeding a database, starting a service, setting env vars. Runs once per `npx playwright test` invocation.
|
||
|
|
**Avoid when**: You need browser context (use a setup project instead) or per-test isolation (use fixtures).
|
||
|
|
|
||
|
|
#### TypeScript
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// playwright.config.ts
|
||
|
|
import { defineConfig } from '@playwright/test';
|
||
|
|
|
||
|
|
export default defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.ts',
|
||
|
|
globalSetup: './tests/global-setup.ts',
|
||
|
|
globalTeardown: './tests/global-teardown.ts',
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// tests/global-setup.ts
|
||
|
|
import { FullConfig } from '@playwright/test';
|
||
|
|
|
||
|
|
async function globalSetup(config: FullConfig) {
|
||
|
|
// Seed test database
|
||
|
|
const { execSync } = await import('child_process');
|
||
|
|
execSync('npx prisma db seed', { stdio: 'inherit' });
|
||
|
|
|
||
|
|
// Store data for tests to use via environment variables
|
||
|
|
process.env.TEST_RUN_ID = `run-${Date.now()}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default globalSetup;
|
||
|
|
```
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// tests/global-teardown.ts
|
||
|
|
import { FullConfig } from '@playwright/test';
|
||
|
|
|
||
|
|
async function globalTeardown(config: FullConfig) {
|
||
|
|
const { execSync } = await import('child_process');
|
||
|
|
execSync('npx prisma db push --force-reset', { stdio: 'inherit' });
|
||
|
|
}
|
||
|
|
|
||
|
|
export default globalTeardown;
|
||
|
|
```
|
||
|
|
|
||
|
|
#### JavaScript
|
||
|
|
|
||
|
|
```js
|
||
|
|
// playwright.config.js
|
||
|
|
const { defineConfig } = require('@playwright/test');
|
||
|
|
|
||
|
|
module.exports = defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.js',
|
||
|
|
globalSetup: './tests/global-setup.js',
|
||
|
|
globalTeardown: './tests/global-teardown.js',
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
```js
|
||
|
|
// tests/global-setup.js
|
||
|
|
const { execSync } = require('child_process');
|
||
|
|
|
||
|
|
async function globalSetup(config) {
|
||
|
|
execSync('npx prisma db seed', { stdio: 'inherit' });
|
||
|
|
process.env.TEST_RUN_ID = `run-${Date.now()}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = globalSetup;
|
||
|
|
```
|
||
|
|
|
||
|
|
```js
|
||
|
|
// tests/global-teardown.js
|
||
|
|
const { execSync } = require('child_process');
|
||
|
|
|
||
|
|
async function globalTeardown(config) {
|
||
|
|
execSync('npx prisma db push --force-reset', { stdio: 'inherit' });
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = globalTeardown;
|
||
|
|
```
|
||
|
|
|
||
|
|
### Pattern 5: `.env` File Setup
|
||
|
|
|
||
|
|
**Use when**: Managing secrets, URLs, or feature flags without hardcoding.
|
||
|
|
**Avoid when**: Never commit `.env` files with real secrets. Provide `.env.example` instead.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# .env.example (commit this)
|
||
|
|
BASE_URL=http://localhost:3000
|
||
|
|
TEST_PASSWORD=
|
||
|
|
API_KEY=
|
||
|
|
|
||
|
|
# .env.local (gitignored)
|
||
|
|
BASE_URL=http://localhost:3000
|
||
|
|
TEST_PASSWORD=s3cret
|
||
|
|
API_KEY=test-key-abc123
|
||
|
|
|
||
|
|
# .env.staging (gitignored)
|
||
|
|
BASE_URL=https://staging.example.com
|
||
|
|
TEST_PASSWORD=staging-password
|
||
|
|
API_KEY=staging-key-xyz789
|
||
|
|
```
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# .gitignore
|
||
|
|
.env
|
||
|
|
.env.local
|
||
|
|
.env.staging
|
||
|
|
.env.production
|
||
|
|
playwright/.auth/
|
||
|
|
```
|
||
|
|
|
||
|
|
Install dotenv:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npm install -D dotenv
|
||
|
|
```
|
||
|
|
|
||
|
|
### Pattern 6: Trace, Screenshot, and Video Settings
|
||
|
|
|
||
|
|
**Use when**: Deciding artifact collection strategy for local development vs CI.
|
||
|
|
|
||
|
|
| Setting | Local | CI | Why |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `trace` | `'off'` or `'on-first-retry'` | `'on-first-retry'` | Traces are large; only collect on failure |
|
||
|
|
| `screenshot` | `'off'` | `'only-on-failure'` | Useful for CI debugging only |
|
||
|
|
| `video` | `'off'` | `'retain-on-failure'` | Video is slow to record; keep only failures |
|
||
|
|
|
||
|
|
#### TypeScript
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// playwright.config.ts
|
||
|
|
import { defineConfig } from '@playwright/test';
|
||
|
|
|
||
|
|
export default defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.ts',
|
||
|
|
use: {
|
||
|
|
// CI: capture everything on failure; Local: minimal overhead
|
||
|
|
trace: process.env.CI ? 'on-first-retry' : 'off',
|
||
|
|
screenshot: process.env.CI ? 'only-on-failure' : 'off',
|
||
|
|
video: process.env.CI ? 'retain-on-failure' : 'off',
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
#### JavaScript
|
||
|
|
|
||
|
|
```js
|
||
|
|
// playwright.config.js
|
||
|
|
const { defineConfig } = require('@playwright/test');
|
||
|
|
|
||
|
|
module.exports = defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.js',
|
||
|
|
use: {
|
||
|
|
trace: process.env.CI ? 'on-first-retry' : 'off',
|
||
|
|
screenshot: process.env.CI ? 'only-on-failure' : 'off',
|
||
|
|
video: process.env.CI ? 'retain-on-failure' : 'off',
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Decision Guide
|
||
|
|
|
||
|
|
### Which Timeout to Adjust
|
||
|
|
|
||
|
|
| Symptom | Timeout to Change | Default | Recommended Range |
|
||
|
|
|---|---|---|---|
|
||
|
|
| Test takes too long overall | `timeout` | 30s | 30-60s (never above 120s) |
|
||
|
|
| Assertion `expect()` keeps retrying too long or not long enough | `expect.timeout` | 5s | 5-10s |
|
||
|
|
| `page.goto()` or `waitForURL()` times out | `navigationTimeout` | 30s | 10-30s |
|
||
|
|
| `click()`, `fill()`, `check()` time out | `actionTimeout` | 0 (no limit) | 10-15s |
|
||
|
|
| Dev server slow to start | `webServer.timeout` | 60s | 60-180s |
|
||
|
|
|
||
|
|
### Server Management
|
||
|
|
|
||
|
|
| Scenario | Approach | Why |
|
||
|
|
|---|---|---|
|
||
|
|
| Local dev + CI, app in same repo | `webServer` with `reuseExistingServer: !process.env.CI` | Playwright manages server; reuses yours locally |
|
||
|
|
| Separate frontend/backend repos | Manual start or Docker Compose | `webServer` can only run one command |
|
||
|
|
| Testing deployed staging/production | No `webServer`; set `baseURL` via env var | Server already running remotely |
|
||
|
|
| Multiple services needed (API + frontend) | Array of `webServer` entries | Each gets its own command and URL health check |
|
||
|
|
|
||
|
|
### Single vs Multi-Project Config
|
||
|
|
|
||
|
|
| Scenario | Approach | Why |
|
||
|
|
|---|---|---|
|
||
|
|
| Starting out, early development | Single project (chromium only) | Faster feedback, simpler config |
|
||
|
|
| Pre-release cross-browser validation | Multi-project: chromium + firefox + webkit | Catch rendering/API differences |
|
||
|
|
| Mobile-responsive app | Add mobile projects alongside desktop | Viewport + touch differences matter |
|
||
|
|
| Authenticated + unauthenticated tests | Setup project + dependent projects | Share auth state without re-login per test |
|
||
|
|
| CI pipeline with tight time budget | Chromium in PR checks; all browsers on merge to main | Balance speed vs coverage |
|
||
|
|
|
||
|
|
### globalSetup vs Setup Projects vs Fixtures
|
||
|
|
|
||
|
|
| Need | Use | Why |
|
||
|
|
|---|---|---|
|
||
|
|
| One-time DB seed or external service prep | `globalSetup` | Runs once, no browser needed |
|
||
|
|
| Shared browser auth (login once, reuse cookies) | Setup project with `dependencies` | Needs browser context; `globalSetup` has none |
|
||
|
|
| Per-test isolated state (unique user, fresh data) | Custom fixture via `test.extend()` | Each test gets its own instance with teardown |
|
||
|
|
| Cleanup after all tests | `globalTeardown` | Runs once at the end regardless of pass/fail |
|
||
|
|
|
||
|
|
## Anti-Patterns
|
||
|
|
|
||
|
|
| Don't Do This | Problem | Do This Instead |
|
||
|
|
|---|---|---|
|
||
|
|
| `timeout: 300_000` globally | Masks flaky tests; CI runs take forever | Fix the root cause; keep timeout at 30s; raise `navigationTimeout` only if warranted |
|
||
|
|
| Hardcoded URLs in tests: `page.goto('http://localhost:3000/login')` | Breaks in every non-local environment | Use `baseURL` in config, then `page.goto('/login')` |
|
||
|
|
| Running chromium + firefox + webkit on every PR | 3x CI time for marginal benefit on most PRs | Chromium on PRs; all browsers on main branch merges |
|
||
|
|
| `trace: 'on'` always in CI | Huge artifacts, slow uploads, disk full | `trace: 'on-first-retry'` -- only captures when a test fails and retries |
|
||
|
|
| `video: 'on'` always in CI | Massive CI storage; recording slows tests | `video: 'retain-on-failure'` -- records all but only keeps failures |
|
||
|
|
| Config values inline in test files: `test.use({ viewport: { width: 1280, height: 720 } })` in every file | Scattered, hard to maintain, inconsistent | Define once in project config; override per-file only when genuinely needed |
|
||
|
|
| `retries: 3` locally | Hides flakiness during development | `retries: 0` locally, `retries: 2` in CI |
|
||
|
|
| No `forbidOnly` in CI | Accidentally committed `test.only` runs a single test, everything else silently skipped | `forbidOnly: !!process.env.CI` |
|
||
|
|
| `globalSetup` for browser auth | No browser context available; complex workarounds needed | Use a setup project with `dependencies` |
|
||
|
|
| Committing `.env` files with real credentials | Security risk | Commit `.env.example` only; gitignore real `.env` files |
|
||
|
|
|
||
|
|
## Troubleshooting
|
||
|
|
|
||
|
|
### "baseURL" not working -- tests navigate to full URL
|
||
|
|
|
||
|
|
**Cause**: Using `page.goto('http://localhost:3000/path')` instead of `page.goto('/path')`. When `goto` receives an absolute URL, it ignores `baseURL`.
|
||
|
|
|
||
|
|
**Fix**: Always pass relative paths to `page.goto()`:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// Wrong -- ignores baseURL
|
||
|
|
await page.goto('http://localhost:3000/dashboard');
|
||
|
|
|
||
|
|
// Correct -- uses baseURL from config
|
||
|
|
await page.goto('/dashboard');
|
||
|
|
```
|
||
|
|
|
||
|
|
### webServer starts but tests still fail with connection refused
|
||
|
|
|
||
|
|
**Cause**: The `url` in `webServer` does not match what the server actually serves, or the health check endpoint returns non-200.
|
||
|
|
|
||
|
|
**Fix**: Ensure `webServer.url` matches the actual server address. Add a health check route if needed:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// playwright.config.ts
|
||
|
|
import { defineConfig } from '@playwright/test';
|
||
|
|
|
||
|
|
export default defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.ts',
|
||
|
|
webServer: {
|
||
|
|
command: 'npm run dev',
|
||
|
|
url: 'http://localhost:3000/api/health', // use a real endpoint
|
||
|
|
reuseExistingServer: !process.env.CI,
|
||
|
|
timeout: 120_000,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Tests pass locally but timeout in CI
|
||
|
|
|
||
|
|
**Cause**: CI machines are slower. Default timeouts too tight for CI hardware.
|
||
|
|
|
||
|
|
**Fix**: Increase `navigationTimeout` for CI, reduce `workers` to avoid resource contention:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// playwright.config.ts
|
||
|
|
import { defineConfig } from '@playwright/test';
|
||
|
|
|
||
|
|
export default defineConfig({
|
||
|
|
testDir: './tests',
|
||
|
|
testMatch: '**/*.spec.ts',
|
||
|
|
workers: process.env.CI ? '50%' : undefined,
|
||
|
|
use: {
|
||
|
|
navigationTimeout: process.env.CI ? 30_000 : 15_000,
|
||
|
|
actionTimeout: process.env.CI ? 15_000 : 10_000,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### "Error: page.goto: Target page, context or browser has been closed"
|
||
|
|
|
||
|
|
**Cause**: Test exceeded its `timeout` and Playwright tore down the browser while an action was still running.
|
||
|
|
|
||
|
|
**Fix**: Do not increase the global timeout. Instead, find the slow step using `--trace on` and fix it. Common causes: waiting for a slow API, unresolved network request, or missing `await`.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Record a trace for debugging
|
||
|
|
npx playwright test --trace on
|
||
|
|
npx playwright show-report
|
||
|
|
```
|
||
|
|
|
||
|
|
## Related
|
||
|
|
|
||
|
|
- [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- custom fixtures that replace `globalSetup` for per-test state
|
||
|
|
- [core/test-organization.md](test-organization.md) -- file structure, naming conventions, test grouping
|
||
|
|
- [core/authentication.md](authentication.md) -- setup projects for shared auth state
|
||
|
|
- [ci/ci-github-actions.md](../ci/ci-github-actions.md) -- CI-specific config and caching
|
||
|
|
- [ci/projects-and-dependencies.md](../ci/projects-and-dependencies.md) -- advanced multi-project patterns
|