Web

End-to-End Testing Web Apps with Playwright and Azure DevOps

Web · Published 2026-10-27

Playwright drives real Chromium, Firefox and WebKit browsers to test a web app the way a user would. To run it in Azure DevOps, install the browsers on the build agent with npx playwright install --with-deps, run npx playwright test with a JUnit reporter, and publish the XML with PublishTestResults@2 so results appear in the pipeline's Tests tab, alongside the HTML report and traces as artifacts.

The test and configuration below were run with Playwright Test 1.64 on Node.js 22 against a small local app; the pipeline YAML uses standard Azure Pipelines tasks.

Set up Playwright

npm init playwright@latest

The initializer adds @playwright/test, a playwright.config.ts, an example test and, optionally, a GitHub Actions workflow you can ignore. Locally, npx playwright test runs the suite, --ui opens the interactive runner, and npx playwright codegen http://localhost:4173 records clicks as test code, a useful starting point that you then tidy up.

A first test with resilient locators

The app here is a minimal to-do list: a labelled text field, an "Add" button and a list.

import { test, expect } from '@playwright/test';

test('adds a todo item', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveTitle(/Todo demo/);

  await page.getByLabel('New todo').fill('Write the pipeline');
  await page.getByRole('button', { name: 'Add' }).click();

  await expect(page.getByRole('listitem')).toHaveText(['Write the pipeline']);
  await expect(page.getByLabel('New todo')).toBeEmpty();
});

test('ignores empty input', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('button', { name: 'Add' }).click();
  await expect(page.getByRole('listitem')).toHaveCount(0);
});

Three habits make Playwright suites stable:

If you come from Selenium, the auto-waiting is the biggest difference; the comparison in Scrapy vs Selenium explains why browser automation needs it.

A configuration that behaves differently in CI

Azure Pipelines sets the TF_BUILD variable, and you can define CI yourself. The config reads it to switch reporters, retries and parallelism:

import { defineConfig, devices } from '@playwright/test';

const isCI = !!process.env.CI;

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: isCI,                 // fail if a test.only slipped in
  retries: isCI ? 2 : 0,
  workers: isCI ? 1 : undefined,
  reporter: isCI
    ? [['junit', { outputFile: 'test-results/junit.xml' }], ['html', { open: 'never' }]]
    : 'list',
  use: {
    baseURL: process.env.BASE_URL ?? 'http://localhost:4173',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
  webServer: process.env.BASE_URL
    ? undefined
    : { command: 'npm run preview -- --port 4173', url: 'http://localhost:4173', reuseExistingServer: !isCI },
});
Running 2 tests using 1 worker
··
  2 passed (3.1s)

Key choices:

The Azure DevOps pipeline

trigger:
  branches:
    include: [main]
pr:
  branches:
    include: [main]

pool:
  vmImage: ubuntu-latest

variables:
  CI: 'true'
  npm_config_cache: $(Pipeline.Workspace)/.npm

steps:
  - task: UseNode@1
    inputs:
      version: 22.x

  - task: Cache@2
    inputs:
      key: 'npm | "$(Agent.OS)" | package-lock.json'
      path: $(npm_config_cache)
    displayName: Cache npm

  - script: npm ci
    displayName: Install dependencies

  - script: npx playwright install --with-deps chromium
    displayName: Install Playwright browser

  - script: npx playwright test
    displayName: Run Playwright tests

  - task: PublishTestResults@2
    condition: succeededOrFailed()
    inputs:
      testResultsFormat: JUnit
      testResultsFiles: test-results/junit.xml
      testRunTitle: Playwright e2e
      failTaskOnFailedTests: true

  - task: PublishPipelineArtifact@1
    condition: succeededOrFailed()
    inputs:
      targetPath: playwright-report
      artifact: playwright-report-$(System.JobAttempt)
    displayName: Publish HTML report

  - task: PublishPipelineArtifact@1
    condition: failed()
    inputs:
      targetPath: test-results
      artifact: test-results-$(System.JobAttempt)
    displayName: Publish traces and screenshots

Notes on the pipeline:

As an alternative to installing browsers on every run, you can run the job in Microsoft's official Playwright Docker image (mcr.microsoft.com/playwright, tagged with the matching version) using a container job.

Testing a deployed environment

End-to-end tests are most valuable against the real deployment. Split the pipeline into stages: build and deploy to staging, then run the Playwright stage with BASE_URL set to the staging URL, and only promote to production if it passes. If the app is hosted on Azure Static Web Apps, its preview environments give each pull request its own URL; see deploying a static web app from an Azure DevOps pipeline. Keep credentials for test accounts in secret variables and map them explicitly into the test step with env:, because secret variables are not exposed to scripts automatically.

What to test end to end

Browser tests are slower than unit tests. Reserve them for critical journeys: sign-up and login, the main create-read-update-delete flow of your app (see what a CRUD application is), checkout or payment hand-off, and anything that broke in production before. Push edge cases down to unit and API tests.

Summary

Write Playwright tests with role and label locators and web-first assertions, make the config CI-aware (JUnit reporter, retries with traces, forbidOnly), install browsers with --with-deps on the agent, and publish the JUnit XML with PublishTestResults@2 and the HTML report and traces as pipeline artifacts, even when tests fail.

Sources

Read next

More in Web