Playwright + Azure DevOps CI/CD Integration
A real-world example of integrating a Playwright test framework with Azure DevOps CI/CD. Covers project setup, playwright.config.ts for CI, parallel.
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
TYPESCRIPT1import { defineConfig, devices } from '@playwright/test' 2 3const isCI = !!process.env.CI 4 5export default defineConfig({ 6 testDir: './tests/e2e', 7 fullyParallel: true, 8 forbidOnly: isCI, 9 retries: isCI ? 2 : 0, 10 workers: isCI ? 6 : 4, 11 timeout: isCI ? 45_000 : 30_000, 12 expect: { 13 timeout: isCI ? 10_000 : 5_000, 14 }, 15 globalSetup: './tests/setup/global-setup.ts', 16 reporter: [ 17 ['list'], 18 ['junit', { outputFile: 'test-results/results.xml' }], 19 ['html', { outputFolder: 'playwright-report', open: 'never' }], 20 isCI ? ['github'] : ['dot'], // GitHub annotations in CI 21 ], 22 use: { 23 baseURL: process.env.BASE_URL || 'http://localhost:3000', 24 storageState: 'tests/setup/.auth/user.json', 25 trace: 'on-first-retry', 26 screenshot: 'only-on-failure', 27 video: 'on-first-retry', 28 actionTimeout: isCI ? 15_000 : 8_000, 29 navigationTimeout: isCI ? 30_000 : 15_000, 30 }, 31 projects: [ 32 // Setup project — creates auth state 33 { name: 'setup', testMatch: /global-setup\.ts/ }, 34 // Chrome (main) 35 { 36 name: 'chromium', 37 use: { ...devices['Desktop Chrome'] }, 38 dependencies: ['setup'], 39 }, 40 // Mobile Chrome 41 { 42 name: 'mobile', 43 use: { ...devices['Pixel 7'] }, 44 dependencies: ['setup'], 45 testMatch: '**/mobile/**/*.spec.ts', 46 }, 47 ], 48})
Global setup for authentication
TYPESCRIPT1// tests/setup/global-setup.ts 2import { chromium, FullConfig } from '@playwright/test' 3import * as path from 'path' 4import * as fs from 'fs' 5 6async function globalSetup(config: FullConfig) { 7 const authDir = path.join(__dirname, '.auth') 8 if (!fs.existsSync(authDir)) fs.mkdirSync(authDir, { recursive: true }) 9 10 const browser = await chromium.launch() 11 const page = await browser.newPage() 12 13 await page.goto(`${config.projects[0].use.baseURL}/login`) 14 await page.fill('[data-testid="email"]', process.env.TEST_EMAIL!) 15 await page.fill('[data-testid="password"]', process.env.TEST_PASSWORD!) 16 await page.click('[data-testid="sign-in"]') 17 await page.waitForURL('**/dashboard', { timeout: 30_000 }) 18 19 await page.context().storageState({ path: path.join(authDir, 'user.json') }) 20 await browser.close() 21} 22 23export default globalSetup
Multi-environment pipeline
YAML1trigger: 2 branches: 3 include: [main] 4 5pr: 6 branches: 7 include: [main] 8 9pool: 10 vmImage: ubuntu-latest 11 12variables: 13 - group: playwright-secrets 14 15stages: 16 # ── PR validation: smoke tests only ────────────────────────────────────── 17 - stage: SmokePR 18 displayName: Smoke Tests (PR) 19 condition: eq(variables['Build.Reason'], 'PullRequest') 20 jobs: 21 - job: Smoke 22 steps: 23 - template: .azure/playwright-job.yml 24 parameters: 25 grepTag: '@smoke' 26 runTitle: Smoke — PR $(System.PullRequest.PullRequestNumber) 27 environment: staging 28 29 # ── Main branch: full regression ───────────────────────────────────────── 30 - stage: Regression 31 displayName: Full Regression 32 condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') 33 jobs: 34 - job: Shard1 35 steps: 36 - template: .azure/playwright-job.yml 37 parameters: 38 shard: '1/4' 39 runTitle: Regression Shard 1/4 40 environment: staging 41 - job: Shard2 42 steps: 43 - template: .azure/playwright-job.yml 44 parameters: 45 shard: '2/4' 46 runTitle: Regression Shard 2/4 47 environment: staging 48 - job: Shard3 49 steps: 50 - template: .azure/playwright-job.yml 51 parameters: 52 shard: '3/4' 53 runTitle: Regression Shard 3/4 54 environment: staging 55 - job: Shard4 56 steps: 57 - template: .azure/playwright-job.yml 58 parameters: 59 shard: '4/4' 60 runTitle: Regression Shard 4/4 61 environment: staging 62 63 # ── Nightly: UAT environment ────────────────────────────────────────────── 64 - stage: NightlyUAT 65 displayName: Nightly UAT Regression 66 condition: eq(variables['Build.Reason'], 'Schedule') 67 jobs: 68 - job: UATFull 69 steps: 70 - template: .azure/playwright-job.yml 71 parameters: 72 runTitle: Nightly UAT — $(Build.BuildNumber) 73 environment: uat
Template .azure/playwright-job.yml:
YAML1parameters: 2 - name: grepTag 3 default: '' 4 - name: shard 5 default: '' 6 - name: runTitle 7 default: 'Playwright' 8 - name: environment 9 default: 'staging' 10 11steps: 12 - task: NodeTool@0 13 inputs: 14 versionSpec: '20.x' 15 - script: npm ci 16 - script: npx playwright install --with-deps chromium 17 - script: | 18 ARGS="" 19 if [ -n "${{ parameters.grepTag }}" ]; then 20 ARGS="$ARGS --grep ${{ parameters.grepTag }}" 21 fi 22 if [ -n "${{ parameters.shard }}" ]; then 23 ARGS="$ARGS --shard=${{ parameters.shard }}" 24 fi 25 npx playwright test $ARGS 26 env: 27 BASE_URL: $(${{ parameters.environment }}_URL) 28 TEST_EMAIL: $(TEST_EMAIL) 29 TEST_PASSWORD: $(TEST_PASSWORD) 30 CI: true 31 - task: PublishTestResults@2 32 inputs: 33 testResultsFormat: JUnit 34 testResultsFiles: test-results/results.xml 35 testRunTitle: ${{ parameters.runTitle }} 36 condition: always() 37 - task: PublishPipelineArtifact@1 38 inputs: 39 targetPath: playwright-report 40 artifact: playwright-report-$(System.JobName) 41 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.
Share this article
Follow for more
Follow me on social media for more developer tips, tricks, and tutorials. Let's connect and build something great together!