Parallel & Cross-Browser Testing

Run tests fast across multiple browsers

8 min readNode.js

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:

EngineBrowsersNotes
ChromiumChrome, EdgeDefault, fastest
FirefoxFirefoxGecko engine
WebKitSafariiOS/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: true parallelizes 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!