ScreenshotNeo

BlogHow-to

How to Capture Playwright Screenshots in Azure Pipelines

Capture Playwright screenshots on failures, retain traces and reports, and publish every artifact reliably in Azure Pipelines.

By the ScreenshotNeo team1 October 20268 min read

To capture Playwright screenshots in Azure Pipelines, configure Playwright to save screenshots and traces, run the tests, then publish playwright-report/ and test-results/ with PublishPipelineArtifact@1. Set both publication tasks to condition: always() so a failed test does not erase the evidence you need.

This workflow gives you three kinds of evidence:

  • A PNG or JPEG showing what the page looked like at failure time.
  • A Playwright trace containing action order, DOM snapshots, network details, console output and a film-strip timeline.
  • An HTML report that lets you browse test status and open attached screenshots and traces.

1. Configure Playwright to retain screenshots and traces

In playwright.config.ts, retain screenshots on failures and traces for failed tests. The on-first-retry trace mode keeps normal runs smaller while recording the retry that usually contains the useful diagnostic context.

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

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  expect: {
    timeout: 5_000,
  },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html', { outputFolder: 'playwright-report', open: 'never' }],
    ['junit', { outputFile: 'test-results/results.xml' }],
  ],
  use: {
    baseURL: process.env.BASE_URL || 'http://127.0.0.1:3000',
    screenshot: 'only-on-failure',
    trace: 'on-first-retry',
    video: 'retain-on-failure',
    actionTimeout: 10_000,
    navigationTimeout: 30_000,
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

Playwright’s CI guidance covers browser installation and supported CI environments in its CI documentation. The HTML reporter writes to playwright-report/; failure screenshots, videos and traces are normally written below test-results/.

What each setting does

Setting Use Trade-off
screenshot: 'only-on-failure' Captures a screenshot when a test fails. Much smaller artifacts than capturing every step.
screenshot: 'on' Captures screenshots for every test. Useful for audits, but increases storage and upload time.
trace: 'on-first-retry' Records a trace on the first retry. Good diagnostic detail without tracing every successful run.
trace: 'retain-on-failure' Records traces and removes them for successful tests. Can create larger failed-run artifacts.
trace: 'on' Records every test. Best for intermittent failures; highest storage cost.
video: 'retain-on-failure' Retains a video only when a test fails. Helpful for visual timing issues, with larger files.

2. Write tests that produce useful failure evidence

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

test('checkout page shows the order summary', async ({ page }) => {
  await page.goto('/checkout');
  await page.getByRole('heading', { name: 'Order summary' }).waitFor();
  await expect(page.getByTestId('order-total')).toHaveText('$49.00');
});

For visual regression, use toHaveScreenshot() and commit the expected baseline images. Playwright compares the actual image with the configured snapshot and reports the difference. Keep the browser, viewport, fonts, data and color scheme stable so a genuine UI change is not hidden by environmental noise.

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

test('home page visual baseline', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('home-page.png', {
    fullPage: true,
    animations: 'disabled',
  });
});

Use deterministic seed data, wait for the important UI to settle, and avoid asserting on timestamps, random IDs or live third-party content. A failure screenshot answers “what did the page look like?” A trace answers “what happened before it looked that way?”

3. Add the Azure Pipelines job

The following complete pipeline installs dependencies, installs Playwright browsers, runs tests, publishes the HTML report and publishes test results. The artifact tasks run even when the test command exits with a failure.

trigger:
- main

pool:
  vmImage: ubuntu-latest

steps:
- checkout: self

- script: npm ci
  displayName: Install dependencies

- script: npx playwright install --with-deps
  displayName: Install Playwright browsers

- script: npx playwright test
  displayName: Run Playwright tests

'task: PublishPipelineArtifact@1'
  condition: always()
  inputs:
    targetPath: '$(System.DefaultWorkingDirectory)/playwright-report'
    artifact: 'playwright-report'
    publishLocation: 'pipeline'

- task: PublishPipelineArtifact@1
  condition: always()
  inputs:
    targetPath: '$(System.DefaultWorkingDirectory)/test-results'
    artifact: 'playwright-test-results'
    publishLocation: 'pipeline'

- task: PublishTestResults@2
  condition: always()
  inputs:
    testResultsFormat: 'JUnit'
    testResultsFiles: '$(System.DefaultWorkingDirectory)/test-results/results.xml'
    mergeTestResults: true
    failTaskOnFailedTests: false
    testRunTitle: 'Playwright tests'

Replace the first artifact task’s accidental string key with the normal YAML task syntax shown below; this is the version to use in your file:

- task: PublishPipelineArtifact@1
  condition: always()
  inputs:
    targetPath: '$(System.DefaultWorkingDirectory)/playwright-report'
    artifact: 'playwright-report'
    publishLocation: 'pipeline'

The PublishPipelineArtifact@1 task uploads a directory as a pipeline artifact. $(System.DefaultWorkingDirectory) is safe when the repository is checked out into the standard agent workspace. If your job changes directory or uses a custom checkout path, point targetPath at the actual absolute directory.

Why condition: always() matters

Without an explicit condition, a later task is commonly skipped after npx playwright test fails. That leaves the pipeline with a red status but no screenshot, trace or report. Apply always() to every diagnostic publication task, including JUnit results.

4. Install browsers on different Azure agents

  • Windows and macOS: install Playwright and run the tests. Playwright’s CI documentation states that no additional configuration is required for these agents beyond installing Playwright.
  • Linux: use npx playwright install --with-deps, or run inside an official Playwright container with the required browser libraries.
  • Self-hosted agents: make sure the agent user can launch the browser and that the required system libraries, fonts and sandbox permissions are available.

A container-based job can make Linux dependencies reproducible:

pool:
  vmImage: ubuntu-latest

container: mcr.microsoft.com/playwright:v1.52.0-noble

steps:
- script: npm ci
  displayName: Install dependencies
- script: npx playwright test
  displayName: Run Playwright tests
- task: PublishPipelineArtifact@1
  condition: always()
  inputs:
    targetPath: '$(System.DefaultWorkingDirectory)/playwright-report'
    artifact: 'playwright-report'
    publishLocation: 'pipeline'
- task: PublishPipelineArtifact@1
  condition: always()
  inputs:
    targetPath: '$(System.DefaultWorkingDirectory)/test-results'
    artifact: 'playwright-test-results'
    publishLocation: 'pipeline'

Pin the container tag to a Playwright version compatible with your project and verify the current tag in Playwright’s CI documentation before changing it.

5. Open the screenshots, trace and HTML report

  1. Open the completed pipeline run in Azure DevOps.
  2. Open the Artifacts section.
  3. Download playwright-report and playwright-test-results.
  4. Serve the downloaded HTML report locally, or open it according to your team’s artifact policy.
  5. Open a .zip trace with Playwright Trace Viewer:
npx playwright show-trace path/to/trace.zip

The report is best for browsing test status and attachments. The screenshot is best for a quick visual check. The trace is best when you need the action sequence, DOM state, network activity, console messages and film-strip timeline.

6. Publish results from parallel or sharded jobs

Parallel jobs can write to separate directories or publish distinct artifact names. Avoid two jobs uploading the same directory at the same time.

- task: PublishPipelineArtifact@1
  condition: always()
  inputs:
    targetPath: '$(System.DefaultWorkingDirectory)/test-results'
    artifact: 'playwright-results-$(System.JobAttempt)'
    publishLocation: 'pipeline'

If you need one combined report, merge results in a follow-up job after all shards finish. Keep each shard’s screenshots and traces identifiable by browser, shard index and job name.

7. Add JUnit reporting to Azure DevOps Test reporting

The JUnit reporter makes test cases visible in Azure DevOps test reporting. The configuration above writes test-results/results.xml, and PublishTestResults@2 imports it after the run. Screenshot, recording and trace attachment behavior is version-sensitive; verify the current Playwright and Azure task behavior before depending on direct attachment links. Microsoft’s guidance describes associating failure artifacts with JUnit results for supported Playwright versions.

8. Troubleshooting

Symptom Likely cause Fix
No screenshot files Screenshot policy is disabled, or the test passed. Set screenshot: 'only-on-failure' or 'on'; inspect test-results/ before publishing.
Artifacts missing after a failure Publish tasks were skipped by the default condition. Add condition: always() to every report, result and artifact task.
“Path does not exist” from PublishPipelineArtifact The test process wrote files elsewhere, or the path is relative to another directory. List the directories before publication and use $(System.DefaultWorkingDirectory) plus the real path.
Browser executable not found Playwright browsers were not installed on the agent. Run npx playwright install --with-deps on Linux or use the official Playwright container.
Browser fails to launch on Linux Missing system libraries, fonts or sandbox permissions. Use --with-deps, a supported Playwright container, or install the self-hosted agent dependencies.
Blank or inconsistent screenshots The UI was still loading, data was nondeterministic, or the viewport differed. Wait for a selector or network idle, seed test data, disable animations and standardize browser and viewport settings.
Trace is unavailable Trace mode did not cover that test, or the trace artifact was not downloaded. Use retain-on-failure or on-first-retry, publish test-results/, then open the downloaded ZIP with Trace Viewer.
Visual baseline mismatch Browser, viewport, fonts, OS rendering or page data changed. Reproduce with the same environment, inspect expected and actual images, and update baselines deliberately.
JUnit report is empty The output path differs from the publication path, or the reporter was not enabled. Confirm the junit reporter and check that results.xml exists before PublishTestResults@2.

9. Performance, reliability and cost considerations

  • Artifact size: failure-only screenshots and traces keep uploads smaller. Videos and full traces can grow quickly on suites with many failures.
  • Runtime: browser installation is repeated on fresh hosted agents unless you use a supported container or caching strategy. Keep browser and Playwright versions aligned.
  • Reproducibility: pin Node, Playwright, browser, viewport, timezone, locale and test data where possible.
  • Reliability: use retries for transient CI failures, but keep the original failure evidence. A retry can pass while the first attempt’s screenshot and trace explain the problem.
  • Retention: configure Azure DevOps artifact retention to match your debugging window. Delete or restrict old artifacts when they contain sensitive page data.
  • Security: screenshots, traces and reports may include tokens rendered in the UI, customer information or internal URLs. Upload them only to trusted artifact stores and apply access controls or encryption.

Or skip the browser setup

If you need a clean screenshot of a deployed URL rather than a test trace, ScreenshotNeo provides a single HTTP request. Its capture service removes cookie and consent banners, newsletter popups and chat widgets before the shot. Bot checks, blank pages, failed loads and cache hits are not billed, and the response reports the verdict and billing status in headers. An MCP server also lets Claude, Cursor and other MCP clients call screenshot tools directly.

See the ScreenshotNeo API documentation for all options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', data));

Every plan includes the same features: full-page and element capture, device presets and custom viewports, dark mode, retina scale, custom CSS and JavaScript, waits, request blocking, headers, cookies, user agents, geolocation, PDF output, caching, signed links, asynchronous jobs, bulk capture and usage reporting. You get 1,000 screenshots each month free with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

FAQ

Where do Playwright screenshots go in Azure Pipelines?

Failure screenshots normally appear under test-results/. The HTML report is written to playwright-report/. Publish both directories as pipeline artifacts.

Should I publish screenshots or traces?

Publish both for failed CI runs. Screenshots are faster to inspect; traces explain the sequence of actions and page state that led to the failure.

Can Azure Pipelines show Playwright tests in its Test tab?

Yes. Enable Playwright’s JUnit reporter and import the XML with PublishTestResults@2.

Why use always() instead of succeededOrFailed()?

always() also runs when the job is canceled or reaches another terminal state, making it the safer choice for collecting diagnostics.

Are screenshots safe to publish publicly?

Assume they are sensitive. They can contain rendered secrets, customer data and internal URLs, so restrict artifact access and set an appropriate retention period.