Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To show a Cypress failure screenshot on the matching Azure Pipelines test result, keep the generated file on the build agent, add an Azure JUnit attachment marker to that test case’s system-out, and publish the XML with PublishTestResults@2. Cypress writes failure images to cypress/screenshots by default; Azure reads markers in the form [[ATTACHMENT|filePath]]. If your deployment is Azure DevOps Server 2022.1 or older, individual JUnit attachments are not supported, so publish the screenshot directory as a pipeline artifact instead.

How the attachment chain works

There are three separate operations:

  1. Cypress captures an image. During cypress run, Cypress automatically takes a screenshot when a test fails unless failure capture is disabled. The default directory is cypress/screenshots. See the Cypress screenshots guide and configuration reference.
  2. A JUnit result identifies the test. Run Cypress with its JUnit reporter so each test is represented in XML.
  3. Azure resolves the file marker. In the relevant JUnit testcase, put [[ATTACHMENT|path-to-image]] in system-out. PublishTestResults@2 reads that marker and associates the existing file with the result. The path must still resolve on the agent when the task runs. The syntax and task inputs are documented by Microsoft’s PublishTestResults@2 reference.

Cypress does not automatically add Azure’s marker to its JUnit output. Your reporter version may provide an attachment option; otherwise, add the marker during result generation or with an XML post-processing step.

Prepare Cypress to preserve useful screenshots

Use cypress run, not the interactive runner

Automatic failure screenshots are taken during cypress run. A test opened with cypress open does not receive the same automatic failure capture. For a deliberate image at any point in a test, use cy.screenshot(); the screenshot API documents viewport, full-page and runner capture options.

Check the output directory and cleanup behavior

The default directory is cypress/screenshots. Cypress clears screenshot and video assets before a run by default, so do not put input files there before starting the job. If retaining that directory across runs is intentional, configure trashAssetsBeforeRuns: false as described in the screenshots guide. In most CI jobs, a clean directory is safer because old images cannot be mistaken for the current run.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Optional explicit capture

describe('checkout', () => {
  it('shows a useful failure state', () => {
    cy.visit('/checkout');
    cy.screenshot('checkout-before-submit', { capture: 'fullPage' });
    cy.get('[data-cy=submit-order]').click();
  });
});

Keep the resulting files until test publication has completed. If you customize screenshotsFolder, use that exact location in the later attachment and artifact steps.

Generate JUnit XML from Cypress

Cypress documents the JUnit reporter invocation below. The directory is placed under Azure’s common test-results location so the publishing task can find it predictably.

npx cypress run 
  --reporter junit 
  --reporter-options "mochaFile=$(Common.TestResultsDirectory)/junit/test-results.xml,toConsole=true"

The reporter creates the test records, but the general Cypress reporter documentation does not define an Azure-specific screenshot-marker recipe. Review the XML produced by your installed Cypress and reporter versions. A result that contains only failure text, without [[ATTACHMENT|...]] in system-out, will publish successfully but will not show an image on the individual test row.

Add the Azure attachment marker

What the XML must contain

For each image you want on a particular result, the corresponding JUnit testcase needs a system-out element such as:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<testcase classname="spec.cy.js" name="checkout submits" time="1.42">
  <failure>Timed out waiting for submit</failure>
  <system-out>[[ATTACHMENT|/agent/_work/1/s/cypress/screenshots/spec.cy.js/checkout submits (failed).png]]</system-out>
</testcase>

Use an absolute path or a path rooted where the task runs, and verify that the file exists on that agent. Do not point to a developer laptop, a container path that is not mounted in the publishing job, or a directory deleted by a cleanup step.

Reporter option versus post-processing

If your JUnit reporter supports an attachment setting, configure it to emit the marker in the matching testcase. If it does not, post-process the XML after Cypress exits and before PublishTestResults@2. The post-processor must know how your reporter names tests and screenshots; Cypress naming can include spec, suite and test names, so inspect a real XML file and directory listing before making the mapping rule. A safe implementation should:

  • parse XML rather than performing string replacement;
  • match a testcase to its own screenshot, not every image in the directory;
  • escape the path as XML text;
  • leave existing system-out content intact; and
  • fail clearly when a referenced image is missing.

Because Cypress and reporter versions differ, treat the following pipeline as an illustrative pattern, not a tested universal configuration. Confirm the produced XML and paths on your selected agent.

Publish the results in Azure Pipelines

steps:
- script: |
    npx cypress run 
      --reporter junit 
      --reporter-options "mochaFile=$(Common.TestResultsDirectory)/junit/test-results.xml,toConsole=true"
  displayName: Run Cypress and write JUnit results

# If your reporter or post-processor adds [[ATTACHMENT|...]], run it here.
# - script: python ci/add_junit_attachments.py
#   displayName: Add screenshot markers to JUnit

- task: PublishTestResults@2
  condition: always()
  inputs:
    testResultsFormat: JUnit
    testResultsFiles: '$(Common.TestResultsDirectory)/junit/*.xml'
    failTaskOnFailedTests: true
    publishRunAttachments: true
  displayName: Publish Cypress test results

condition: always() matters: a failed Cypress command must not prevent the publishing step from running. Microsoft marks PublishTestResults@1 deprecated in favor of version 2; use PublishTestResults@2.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Verify an individual result

  1. Open the pipeline run and its Tests tab.
  2. Open a failed test that should have an image.
  3. Check that the attachment is shown in the result details, not merely in the run’s general attachments.
  4. If it is absent, download or print the XML from the agent and search for [[ATTACHMENT|; then check the exact path with a file-existence command immediately before publication.

Fallback for Azure DevOps Server 2022.1 and older

JUnit attachment support in the publishing task is unavailable on Azure DevOps Server 2022.1 and lower. Check the exact server deployment, not just the task version. You can still publish the JUnit results and make the screenshots downloadable as a run-level build artifact:

steps:
- script: |
    npx cypress run 
      --reporter junit 
      --reporter-options "mochaFile=$(Common.TestResultsDirectory)/junit/test-results.xml,toConsole=true"
  displayName: Run Cypress

- task: PublishTestResults@2
  condition: always()
  inputs:
    testResultsFormat: JUnit
    testResultsFiles: '$(Common.TestResultsDirectory)/junit/*.xml'
    failTaskOnFailedTests: true
  displayName: Publish JUnit results

- task: CopyFiles@2
  condition: always()
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)/cypress/screenshots'
    Contents: '**'
    TargetFolder: '$(Build.ArtifactStagingDirectory)/cypress-screenshots'
  displayName: Stage Cypress screenshots

- task: PublishBuildArtifacts@1
  condition: always()
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)/cypress-screenshots'
    ArtifactName: cypress-screenshots
    publishLocation: Container
  displayName: Publish screenshot artifact

This gives you a complete screenshot directory to download from the run, but it does not attach each file to a specific test row. Microsoft’s guidance on UI testing and test runs explains the distinction between run-level files and test-result attachments: UI testing considerations and Manage test runs.

Choose individual attachments or an artifact

Approach Where the image appears Requirements Best use
JUnit attachment marker On the associated test result Supported Azure DevOps deployment, valid marker, existing file, and reporter or XML customization Fast diagnosis of a particular failed test
Build artifact Downloadable directory on the pipeline run Copy and publish steps; no JUnit attachment support required Older Server versions, bulk review, or retaining every screenshot

You can use both when supported: individual markers for the failures people investigate first, and an artifact as a complete record.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting checklist

No screenshot exists

  • Confirm the job ran cypress run, not only cypress open.
  • Check that screenshotOnRunFailure was not disabled.
  • Check the configured screenshotsFolder and list it immediately after Cypress exits.
  • Remember that the folder is cleared before a run by default; do not expect files from a previous job.

Results publish, but no image is visible

  • Open the XML and search for the literal [[ATTACHMENT| marker inside the failed testcase’s system-out.
  • Check spelling, capitalization and XML escaping of the path.
  • Confirm the screenshot still exists at publication time and that the publishing task runs on the same agent or has access to the same workspace.
  • Ensure the marker is inside the intended testcase, not in a suite-level or unrelated output element.

The XML file is not found

  • Use the same $(Common.TestResultsDirectory) path in the Cypress reporter option and the task’s testResultsFiles glob.
  • Check whether a parallel job writes a different filename or directory.
  • Keep the glob narrow enough to avoid publishing stale XML from another run.

The task fails the job unexpectedly

failTaskOnFailedTests: true intentionally marks the task failed when Cypress has failed tests. Keep it if test failures should fail CI; set the input according to your release policy, but do not hide a publishing problem by removing the task or its logs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Attachments work in Azure DevOps Services but not on Server

Check the Server version first. Azure DevOps Server 2022.1 and lower do not support this JUnit attachment mechanism. Use the artifact fallback rather than repeatedly changing the XML.

Performance, reliability and retention

  • Write screenshots and JUnit XML to the agent’s local workspace, then publish once; copying the same large image through several intermediate directories adds avoidable I/O.
  • Use deterministic screenshot names and clean directories so retries cannot associate an old image with a new testcase.
  • When tests run in parallel, give each worker a separate results and screenshot directory or a collision-resistant filename, then merge or publish all XML files with the task glob.
  • Keep the screenshot files until both the test-results and artifact tasks finish. A cleanup step placed earlier in the job makes valid markers unusable.
  • Large full-page images increase artifact size and review time. Use cy.screenshot() with a focused capture when the failure can be diagnosed without the whole page; use full-page capture when layout outside the viewport is material.

Or skip the browser setup

If you need a clean website image outside a Cypress browser test—for example, a documentation page or deployment smoke-check—ScreenshotNeo provides a single HTTP request. It removes cookie-consent banners, newsletter popups and chat widgets before capture; bot checks, blank pages, failed loads and timeouts are not billed, and an MCP server lets AI agents take screenshots.

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)
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}`);

See the ScreenshotNeo documentation for request options. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account to try it.

The Bottom Line

Use Cypress to generate the image, add [[ATTACHMENT|...]] to the matching JUnit testcase, and publish with PublishTestResults@2. On Azure DevOps Server 2022.1 or older, publish cypress/screenshots as a build artifact instead.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.