IKivan-kozenko -aqa
Try yourself as a QA tester
All topics·Advanced·7 / 24

Continuous Integration

The pattern that works for me in every CI provider: install deps, install Playwright with --with-deps (that flag is the one everyone forgets), run tests with workers: 1 for stability, upload the report with if: !cancelled() so you actually get the artifact when tests fail. The rest is boilerplate.

The three steps that actually matter

--with-deps installs OS-level browser libraries; without it the browser binary fails to launch with cryptic errors

Every CI setup for Playwright boils down to the same three things. The --with-deps flag on step 2 is what trips people up most — it installs the OS-level browser dependencies (libglib, libnss, etc.) that the browser binary needs to actually launch. Without it you get 'Failed to launch browser' errors that look like a Playwright bug.

bash
# Крок 1: встанови NPM-пакети
npm ci

# Крок 2: встанови браузери Playwright + системні залежності ОС
npx playwright install --with-deps

# Крок 3: запусти тести
npx playwright test

Workers on CI — use 1, not the default

By default Playwright uses all available CPU cores as workers. On a shared CI runner those cores are often virtual and shared with other jobs — running many workers in parallel leads to flaky tests from resource contention. I set workers: 1 on CI. If you need speed, use sharding across multiple machines rather than workers on one.

ts
// playwright.config.ts
export default defineConfig({
  workers: process.env.CI ? 1 : undefined,
})

GitHub Actions — the config I actually use

The if: ${{ !cancelled() }} on the artifact upload is critical. When a test fails, the job is marked as failed — and by default any subsequent steps are skipped. Without this condition, you never get the HTML report when you need it most (when tests fail).

On deployment trigger: I use github.event.deployment_status.state == 'success' when testing against a preview URL. Vercel and similar platforms fire the deployment_status event and put the URL in deployment_status.target_url — I pass that as PLAYWRIGHT_TEST_BASE_URL.

yaml
name: Playwright Tests
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v5
    - uses: actions/setup-node@v5
      with:
        node-version: lts/*
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
    - name: Run Playwright tests
      run: npx playwright test
    - uses: actions/upload-artifact@v4
      if: ${{ !cancelled() }}
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30
yaml
# Тести після деплою на Vercel/Netlify/etc
name: Playwright Tests
on:
  deployment_status:
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    if: github.event.deployment_status.state == 'success'
    steps:
    - uses: actions/checkout@v5
    - uses: actions/setup-node@v5
      with:
        node-version: lts/*
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright
      run: npx playwright install --with-deps
    - name: Run Playwright tests
      run: npx playwright test
      env:
        PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}

Faster feedback on PRs — --only-changed

--only-changed runs only the test files affected by the current changeset. Playwright analyzes the dependency graph to figure out which test files import or depend on the changed source files. On a large project this can cut CI time from 15 minutes to 2 minutes for a small PR.

Important: this is a heuristic, not a guarantee. It can miss tests if the dependency analysis doesn't catch all relationships. I always follow it with a full test run after the PR merges to main.

yaml
# В GitHub Actions — тільки для PR
- name: Run changed Playwright tests
  run: npx playwright test --only-changed=origin/$GITHUB_BASE_REF
  if: github.event_name == 'pull_request'
- name: Run all Playwright tests
  run: npx playwright test

When the browser won't launch on CI

Error: Failed to launch browser on CI is almost always a missing system dependency. First check: did you run --with-deps? If yes, try DEBUG=pw:browser to see exactly what the browser binary says when it fails to start.

Don't cache browser binaries between CI runs. The time to restore from cache is similar to re-downloading, and on Linux the OS dependencies aren't cacheable anyway. Just always reinstall.

bash
# Дебаг запуску браузера — виводить детальний лог
DEBUG=pw:browser npx playwright test

Other CI providers

For all other providers the approach is the same — the only difference is the YAML syntax. Most use the official Playwright Docker image (mcr.microsoft.com/playwright:v1.x-noble) to skip the browser installation step entirely. The image already has all browsers and system dependencies installed.

yaml
# Azure Pipelines
trigger:
- main
pool:
  vmImage: ubuntu-latest
steps:
- task: UseNode@1
  inputs:
    version: '22'
- script: npm ci
- script: npx playwright install --with-deps
- script: npx playwright test
  env:
    CI: 'true'
- task: PublishPipelineArtifact@1
  inputs:
    targetPath: playwright-report
    artifact: playwright-report
  condition: succeededOrFailed()
yaml
# GitLab CI
stages:
  - test
tests:
  stage: test
  image: mcr.microsoft.com/playwright:v1.50.0-noble
  script:
    - npm ci
    - npx playwright test
groovy
// Jenkins Pipeline
pipeline {
  agent { docker { image 'mcr.microsoft.com/playwright:v1.50.0-noble' } }
  stages {
    stage('e2e-tests') {
      steps {
        sh 'npm ci'
        sh 'npx playwright test'
      }
    }
  }
}