Free tools Windows power users keep installed
One-click scans. No signup required.
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:
- Cypress captures an image. During
cypress run, Cypress automatically takes a screenshot when a test fails unless failure capture is disabled. The default directory iscypress/screenshots. See the Cypress screenshots guide and configuration reference. - A JUnit result identifies the test. Run Cypress with its JUnit reporter so each test is represented in XML.
- Azure resolves the file marker. In the relevant JUnit
testcase, put[[ATTACHMENT|path-to-image]]insystem-out.PublishTestResults@2reads 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.
#1 Best Overall
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.
Rank #2
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:
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →<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:
Rank #3
- 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-outcontent 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.
Recommended Free Tools
Verify an individual result
- Open the pipeline run and its Tests tab.
- Open a failed test that should have an image.
- Check that the attachment is shown in the result details, not merely in the run’s general attachments.
- 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:
Rank #4
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.Troubleshooting checklist
No screenshot exists
- Confirm the job ran
cypress run, not onlycypress open. - Check that
screenshotOnRunFailurewas not disabled. - Check the configured
screenshotsFolderand 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’ssystem-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’stestResultsFilesglob. - 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.
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.
Quick Recap
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.

