Playwright + Azure DevOps CI/CD Integration: Real Example
A real-world example of integrating a Playwright test framework with Azure DevOps CI/CD. Covers project setup, playwright.config.ts for CI, parallel execution, trace collection, Azure Test Plans integration, and multi-environment testing.
This article demonstrates a production Playwright + Azure DevOps integration used by a real QA team — a 4-person QA team running 280 Playwright tests across 3 environments with a 12-minute pipeline runtime.
Project structure
tests/
├── e2e/
│ ├── auth/
│ │ ├── login.spec.ts
│ │ └── logout.spec.ts
│ ├── checkout/
│ │ ├── cart.spec.ts
│ │ └── payment.spec.ts
│ └── shared/
│ ├── smoke.spec.ts # @smoke tagged tests
│ └── regression.spec.ts # @regression tagged
├── fixtures/
│ ├── auth.ts # Authentication fixtures
│ └── test-data.ts
├── setup/
│ └── global-setup.ts # Auth state creation
├── playwright.config.ts
└── azure-pipelines.yml
playwright.config.ts optimised for CI
import { defineConfig, devices } from '@playwright/test'
const isCI = !!process.env.CI
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: isCI,
retries: isCI ? 2 : 0,
workers: isCI ? 6 : 4,
timeout: isCI ? 45_000 : 30_000,
expect: {
timeout: isCI ? 10_000 : 5_000,
},
globalSetup: './tests/setup/global-setup.ts',
reporter: [
['list'],
['junit', { outputFile: 'test-results/results.xml' }],
['html', { outputFolder: 'playwright-report', open: 'never' }],
isCI ? ['github'] : ['dot'], // GitHub annotations in CI
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
storageState: 'tests/setup/.auth/user.json',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
actionTimeout: isCI ? 15_000 : 8_000,
navigationTimeout: isCI ? 30_000 : 15_000,
},
projects: [
// Setup project — creates auth state
{ name: 'setup', testMatch: /global-setup\.ts/ },
// Chrome (main)
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
// Mobile Chrome
{
name: 'mobile',
use: { ...devices['Pixel 7'] },
dependencies: ['setup'],
testMatch: '**/mobile/**/*.spec.ts',
},
],
})Global setup for authentication
// tests/setup/global-setup.ts
import { chromium, FullConfig } from '@playwright/test'
import * as path from 'path'
import * as fs from 'fs'
async function globalSetup(config: FullConfig) {
const authDir = path.join(__dirname, '.auth')
if (!fs.existsSync(authDir)) fs.mkdirSync(authDir, { recursive: true })
const browser = await chromium.launch()
const page = await browser.newPage()
await page.goto(`${config.projects[0].use.baseURL}/login`)
await page.fill('[data-testid="email"]', process.env.TEST_EMAIL!)
await page.fill('[data-testid="password"]', process.env.TEST_PASSWORD!)
await page.click('[data-testid="sign-in"]')
await page.waitForURL('**/dashboard', { timeout: 30_000 })
await page.context().storageState({ path: path.join(authDir, 'user.json') })
await browser.close()
}
export default globalSetupMulti-environment pipeline
trigger:
branches:
include: [main]
pr:
branches:
include: [main]
pool:
vmImage: ubuntu-latest
variables:
- group: playwright-secrets
stages:
# ── PR validation: smoke tests only ──────────────────────────────────────
- stage: SmokePR
displayName: Smoke Tests (PR)
condition: eq(variables['Build.Reason'], 'PullRequest')
jobs:
- job: Smoke
steps:
- template: .azure/playwright-job.yml
parameters:
grepTag: '@smoke'
runTitle: Smoke — PR $(System.PullRequest.PullRequestNumber)
environment: staging
# ── Main branch: full regression ─────────────────────────────────────────
- stage: Regression
displayName: Full Regression
condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
jobs:
- job: Shard1
steps:
- template: .azure/playwright-job.yml
parameters:
shard: '1/4'
runTitle: Regression Shard 1/4
environment: staging
- job: Shard2
steps:
- template: .azure/playwright-job.yml
parameters:
shard: '2/4'
runTitle: Regression Shard 2/4
environment: staging
- job: Shard3
steps:
- template: .azure/playwright-job.yml
parameters:
shard: '3/4'
runTitle: Regression Shard 3/4
environment: staging
- job: Shard4
steps:
- template: .azure/playwright-job.yml
parameters:
shard: '4/4'
runTitle: Regression Shard 4/4
environment: staging
# ── Nightly: UAT environment ──────────────────────────────────────────────
- stage: NightlyUAT
displayName: Nightly UAT Regression
condition: eq(variables['Build.Reason'], 'Schedule')
jobs:
- job: UATFull
steps:
- template: .azure/playwright-job.yml
parameters:
runTitle: Nightly UAT — $(Build.BuildNumber)
environment: uatTemplate .azure/playwright-job.yml:
parameters:
- name: grepTag
default: ''
- name: shard
default: ''
- name: runTitle
default: 'Playwright'
- name: environment
default: 'staging'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
- script: npm ci
- script: npx playwright install --with-deps chromium
- script: |
ARGS=""
if [ -n "${{ parameters.grepTag }}" ]; then
ARGS="$ARGS --grep ${{ parameters.grepTag }}"
fi
if [ -n "${{ parameters.shard }}" ]; then
ARGS="$ARGS --shard=${{ parameters.shard }}"
fi
npx playwright test $ARGS
env:
BASE_URL: $(${{ parameters.environment }}_URL)
TEST_EMAIL: $(TEST_EMAIL)
TEST_PASSWORD: $(TEST_PASSWORD)
CI: true
- task: PublishTestResults@2
inputs:
testResultsFormat: JUnit
testResultsFiles: test-results/results.xml
testRunTitle: ${{ parameters.runTitle }}
condition: always()
- task: PublishPipelineArtifact@1
inputs:
targetPath: playwright-report
artifact: playwright-report-$(System.JobName)
condition: always()Common errors and fixes
Error: Global setup times out creating auth state
Fix: The login page may be slow on the first load in CI. Increase the waitForURL timeout to 60 seconds. Also check that TEST_EMAIL and TEST_PASSWORD environment variables are correctly set.
Error: Tests pass individually but fail when run in parallel Fix: Tests share auth state but not browser contexts. Each test gets its own page with the shared storage state. If tests are writing data that other tests read, add unique identifiers (UUIDs) to created records.
Error: playwright.config.ts import error in pipeline
Fix: Ensure @playwright/test is in devDependencies and npm ci runs before npx playwright test. The node_modules directory must exist before Playwright config is loaded.
Stay ahead in AI-driven QA
Get practical tutorials on test automation, AI testing, and quality engineering — straight to your inbox. No spam, unsubscribe any time.
Discussion
Sign in with GitHub to comment · powered by Giscus