Parallel & Cross-Browser Testing
Run tests fast across multiple browsers
Parallel & Cross-Browser Testing
In the previous tutorial, we learned API testing. Now let's make our test suite fast.
A test suite that takes 30 minutes to run gets ignored. One that takes 3 minutes gets run on every commit. Playwright was built for speed — parallel execution, multiple browsers, all out of the box. Let's learn how to run tests fast at scale.
Parallel Execution
How It Works
Playwright runs tests in parallel using workers. Each worker is a separate process that runs tests independently.
Worker 1: test-A.spec.ts → test-D.spec.ts → test-G.spec.ts
Worker 2: test-B.spec.ts → test-E.spec.ts → test-H.spec.ts
Worker 3: test-C.spec.ts → test-F.spec.ts → test-I.spec.ts
By default, Playwright uses half your CPU cores as workers. On an 8-core machine, that's 4 parallel workers.
Configuring Workers
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Number of parallel workers
workers: 4,
// Or use a percentage of CPUs
workers: '50%',
// In CI, you might want more (or fewer) workers
workers: process.env.CI ? 2 : undefined, // undefined = auto
});
Run with a specific number of workers:
# Use 4 workers
npx playwright test --workers=4
# Run sequentially (debugging)
npx playwright test --workers=1
Fully Parallel Mode
By default, tests within a single file run sequentially. Enable fullyParallel to parallelize everything:
// playwright.config.ts
export default defineConfig({
fullyParallel: true, // Parallelize tests within files too
workers: 4,
});
Or per-file:
// my-tests.spec.ts
import { test } from '@playwright/test';
test.describe.configure({ mode: 'parallel' });
test('test 1', async ({ page }) => { /* ... */ });
test('test 2', async ({ page }) => { /* ... */ });
// Both tests can run at the same time
Serial Tests
"What if my tests MUST run in order?"
Sometimes tests must run in order (e.g., create → read → update → delete):
test.describe.configure({ mode: 'serial' });
test.describe('CRUD flow', () => {
test('create item', async ({ page }) => { /* ... */ });
test('read item', async ({ page }) => { /* ... */ });
test('update item', async ({ page }) => { /* ... */ });
test('delete item', async ({ page }) => { /* ... */ });
});
If any test in a serial group fails, the rest are skipped.
Cross-Browser Testing
Available Browsers
Playwright supports three browser engines:
| Engine | Browsers | Notes |
|---|---|---|
| Chromium | Chrome, Edge | Default, fastest |
| Firefox | Firefox | Gecko engine |
| WebKit | Safari | iOS/macOS |
All three are installed with:
npx playwright install
Configuring Projects
Use projects to test across browsers:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
Now npx playwright test runs all tests on all three browsers.
Running Specific Browsers
# Run only on Chromium
npx playwright test --project=chromium
# Run on Chromium and Firefox
npx playwright test --project=chromium --project=firefox
# Skip WebKit
npx playwright test --project=chromium --project=firefox
Browser-Specific Tests
Skip or modify tests for specific browsers:
import { test, expect } from '@playwright/test';
test('works in all browsers', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading')).toBeVisible();
});
test('chromium only feature', async ({ page, browserName }) => {
test.skip(browserName !== 'chromium', 'This feature is Chromium-only');
await page.goto('/chrome-only-feature');
// ...
});
test('skip flaky webkit test', async ({ page, browserName }) => {
test.fixme(browserName === 'webkit', 'Fix Safari scroll bug');
await page.goto('/scroll-heavy-page');
// ...
});
Different Expectations Per Browser
test('browser-specific behavior', async ({ page, browserName }) => {
await page.goto('/');
if (browserName === 'webkit') {
// Safari renders differently
await expect(page.locator('.menu')).toHaveCSS('padding', '10px');
} else {
await expect(page.locator('.menu')).toHaveCSS('padding', '12px');
}
});
Mobile Testing
"Can I test mobile devices?"
Absolutely! Playwright can emulate mobile devices:
Device Emulation
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Desktop
{
name: 'Desktop Chrome',
use: { ...devices['Desktop Chrome'] },
},
// Mobile
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 13'] },
},
// Tablet
{
name: 'iPad',
use: { ...devices['iPad Pro 11'] },
},
],
});
Available Devices
Playwright has dozens of device profiles. List them all:
npx playwright show-devices
Popular ones:
'iPhone 13','iPhone 13 Pro Max','iPhone 14''Pixel 5','Pixel 7''Galaxy S9+','Galaxy Tab S4''iPad Pro 11','iPad Mini'
Custom Viewports
Create your own viewport:
export default defineConfig({
projects: [
{
name: 'custom-mobile',
use: {
viewport: { width: 390, height: 844 },
isMobile: true,
hasTouch: true,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)...',
},
},
],
});
Testing Responsive Layouts
test('responsive menu', async ({ page, isMobile }) => {
await page.goto('/');
if (isMobile) {
// Mobile: hamburger menu
await expect(page.getByLabel('Menu')).toBeVisible();
await expect(page.getByRole('navigation')).not.toBeVisible();
await page.getByLabel('Menu').click();
await expect(page.getByRole('navigation')).toBeVisible();
} else {
// Desktop: visible nav
await expect(page.getByRole('navigation')).toBeVisible();
await expect(page.getByLabel('Menu')).not.toBeVisible();
}
});
Speed Optimization
Sharding for CI
"How do I make CI even faster?"
Split tests across multiple CI machines:
# Machine 1
npx playwright test --shard=1/3
# Machine 2
npx playwright test --shard=2/3
# Machine 3
npx playwright test --shard=3/3
Each machine runs a different subset of tests. Results merge automatically in CI. A 20-minute suite becomes 5 minutes. Boom!
Test Dependencies with Projects
Run setup once, then tests in parallel:
export default defineConfig({
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: '.auth/user.json',
},
dependencies: ['setup'], // Runs after setup
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
storageState: '.auth/user.json',
},
dependencies: ['setup'],
},
],
});
chromium and firefox don't depend on each other, so they run in parallel after setup completes.
Fail Fast
Stop on first failure to get faster feedback:
npx playwright test --max-failures=1
Or in config:
export default defineConfig({
maxFailures: process.env.CI ? 10 : 1,
});
Retries
Flaky tests? Retry them:
export default defineConfig({
retries: process.env.CI ? 2 : 0, // Retry twice in CI
});
Retries run in the same worker, so they're fast. Use trace: 'on-first-retry' to capture what went wrong.
Test Timeout
Don't let slow tests block everything:
export default defineConfig({
timeout: 30_000, // 30 seconds per test
expect: {
timeout: 5_000, // 5 seconds for assertions
},
});
Full Configuration Example
Here's a production-ready config:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// Parallelization
fullyParallel: true,
workers: process.env.CI ? 2 : undefined,
// Reliability
retries: process.env.CI ? 2 : 0,
maxFailures: process.env.CI ? 10 : undefined,
// Timeouts
timeout: 30_000,
expect: { timeout: 5_000 },
// Artifacts
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
// Projects
projects: [
// Auth setup
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// Desktop browsers
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'], storageState: '.auth/user.json' },
dependencies: ['setup'],
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'], storageState: '.auth/user.json' },
dependencies: ['setup'],
},
// Mobile
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'], storageState: '.auth/user.json' },
dependencies: ['setup'],
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 13'], storageState: '.auth/user.json' },
dependencies: ['setup'],
},
],
// Reporters
reporter: [
['list'],
['html', { open: 'never' }],
...(process.env.CI ? [['github'] as const] : []),
],
// Dev server
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Reporting
Built-in Reporters
export default defineConfig({
reporter: [
// Console output
['list'], // Concise list
['line'], // Single line per test
['dot'], // Minimal dots
// Files
['html'], // Interactive HTML report
['json', { outputFile: 'results.json' }],
['junit', { outputFile: 'results.xml' }],
// CI integrations
['github'], // GitHub Actions annotations
],
});
Multiple Reporters
Use several at once:
reporter: [
['list'],
['html', { open: 'never' }],
['json', { outputFile: 'test-results.json' }],
],
Viewing HTML Report
npx playwright show-report
Opens an interactive report with:
- Pass/fail summary
- Test duration
- Screenshots, videos, traces
- Filter by status, browser, file
Quick Commands Reference
# Run all tests
npx playwright test
# Specific browser
npx playwright test --project=chromium
# Specific file
npx playwright test tests/login.spec.ts
# Specific test by name
npx playwright test -g "login flow"
# Parallel workers
npx playwright test --workers=4
# Sequential (for debugging)
npx playwright test --workers=1
# Headed mode
npx playwright test --headed
# UI mode
npx playwright test --ui
# Debug mode
npx playwright test --debug
# Sharding
npx playwright test --shard=1/4
# Fail fast
npx playwright test --max-failures=1
# Generate report
npx playwright show-report
What's Next?
Your test suite is now blazing fast:
- Workers parallelize test execution across CPU cores
fullyParallel: trueparallelizes tests within files- Projects enable cross-browser and mobile testing
- Device emulation for responsive testing
- Sharding splits tests across CI machines
- Retries handle flaky tests
- Multiple reporters for different needs
You've got fast, cross-browser tests. Now let's run them automatically on every commit with CI/CD integration. Let's go!