DevOps 4 min read

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.

I
InnovateBits
InnovateBits

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

TYPESCRIPT
1import { 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

TYPESCRIPT
1// 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

YAML
1trigger: 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:

YAML
1parameters: 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.

Tags
#playwright#azure-devops#ci-cd#playwright-config#e2e-testing#test-automation#azure-test-plans

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!