ScreenshotNeo

BlogHow-to

How to Integrate Visual Tests with GitHub and Visual Studio Team Services

Run one Playwright visual test suite in GitHub Actions and Azure Pipelines, publish results, and keep screenshot evidence useful and consistent.

By the ScreenshotNeo team4 October 202610 min read

Use one browser test suite in both GitHub Actions and Azure Pipelines (the current name for the service formerly called Visual Studio Team Services, or VSTS). Playwright supports CI examples for both platforms. A practical pipeline checks out the same repository, installs a pinned Node.js runtime, project dependencies and matching browser binaries, runs the visual tests, publishes machine-readable test results, and preserves screenshots or traces when a test fails. Playwright’s CI guide documents both integrations.

The key to useful visual comparisons is repeatability: keep the browser version, operating system, viewport, fonts, test data, and application state controlled. The CI job can tell you that two renders differ; it cannot determine whether the difference is an intended design change. Review changed baselines deliberately.

1. Put the visual suite in the repository

Store the test code, runner configuration, and any approved baseline images with the application or in a repository that the pipeline can check out. Azure Pipelines can use Azure Repos or a connected GitHub repository. Keeping the test definition versioned with the code makes pull request runs reproducible and reviewable.

This walkthrough uses Playwright Test with Node.js and its built-in screenshot assertions. The sample checks a page against a committed baseline. On the first run, create and review the baseline using the update command; do not automatically accept every new image in CI.

// tests/home.visual.spec.ts
import { test, expect } from '@playwright/test';

test('home page matches its visual baseline', async ({ page }) => {
  await page.setViewportSize({ width: 1280, height: 800 });
  await page.goto(process.env.BASE_URL ?? 'http://127.0.0.1:3000', {
    waitUntil: 'networkidle',
  });
  await expect(page).toHaveScreenshot('home.png', {
    fullPage: true,
    animations: 'disabled',
  });
});

Install Playwright Test as a development dependency and commit the lockfile:

npm install --save-dev @playwright/test
npx playwright install

For a baseline update on a developer machine, run npx playwright test --update-snapshots, inspect the generated diff, and commit only intentional changes. Avoid using a different browser or operating system for baseline generation than the CI environment that will compare against it.

2. Run the suite with GitHub Actions

Create .github/workflows/visual-tests.yml. This workflow runs on pushes and pull requests, installs the locked dependencies and the Chromium browser required by the tests, runs Playwright, and uploads the HTML report even after a failure. The action major versions and browser setup should be reviewed periodically and kept aligned with the project’s Playwright version.

name: Visual tests

on:
  push:
  pull_request:

jobs:
  visual:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test
      - name: Upload Playwright report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          if-no-files-found: ignore
          retention-days: 14

The sample expects the app to be available at the configured base URL. Add a prior step to start the application or use Playwright’s webServer configuration. A representative configuration is:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  reporter: [['list'], ['html', { open: 'never' }]],
  use: {
    baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
    browserName: 'chromium',
    viewport: { width: 1280, height: 800 },
    screenshot: 'only-on-failure',
    trace: 'retain-on-failure',
  },
  webServer: {
    command: 'npm run start -- --host 127.0.0.1',
    url: 'http://127.0.0.1:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
});

Use the project’s actual start command and readiness URL. If the app is deployed to a preview environment, set BASE_URL to that URL instead. Do not make the workflow depend on a developer’s local server.

3. Run the same suite with Azure Pipelines

Save the following as azure-pipelines.yml at the repository root. It installs Node.js, installs exact lockfile dependencies and browser dependencies, runs tests while producing JUnit XML, publishes test results even when tests fail, and uploads the HTML report as a pipeline artifact.

trigger:
  - main

pr:
  - main

pool:
  vmImage: ubuntu-latest

steps:
  - task: NodeTool@0
    inputs:
      versionSpec: '20.x'
    displayName: Use Node.js

  - script: npm ci
    displayName: Install project dependencies

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

  - script: npx playwright test --reporter=list,junit
    displayName: Run visual tests
    env:
      CI: 'true'
      PLAYWRIGHT_JUNIT_OUTPUT_NAME: results.xml

  - task: PublishTestResults@2
    condition: succeededOrFailed()
    inputs:
      testResultsFormat: JUnit
      testResultsFiles: results.xml
      mergeTestResults: true
      failTaskOnFailedTests: true
      testRunTitle: Playwright visual tests

  - task: PublishPipelineArtifact@1
    condition: succeededOrFailed()
    inputs:
      targetPath: playwright-report
      artifact: playwright-report
      publishLocation: pipeline

For Playwright’s JUnit reporter, configure the output path if your installed version does not honor the environment variable shown above. One portable approach is to declare the reporter in playwright.config.ts, using an environment-dependent output file, or use the documented reporter options for the version pinned by your lockfile. The JUnit file must exist at results.xml for the publishing task to find it.

The example triggers on the main branch and pull requests targeting it. Adjust branch filters, path filters, and trigger policy to fit your repository. Azure Pipelines supports YAML and Classic pipelines; YAML keeps the definition alongside the suite and makes changes reviewable.

4. Keep rendering conditions stable

Visual tests are sensitive to differences that ordinary functional assertions often ignore. Make these inputs explicit:

  • Browser and framework versions: commit the package lockfile and install the browser version associated with that Playwright release.
  • Operating system and fonts: use the same CI image for baseline generation and comparison. A container job can help teams use a consistent screenshot environment across operating systems; select a Playwright container tag that matches the installed Playwright version.
  • Viewport and scale: set viewport dimensions and device scale factor deliberately. If responsive behavior matters, test a small defined set of viewports rather than a changing set.
  • Application data: seed deterministic records, freeze dates where relevant, and avoid content that changes on each request.
  • Asynchronous UI: wait for a meaningful locator or application-ready condition. Network idle alone can be unsuitable for pages with analytics, polling, or persistent connections.
  • Motion and transient UI: disable animations where appropriate and control carousels, timestamps, rotating content, and consent state.
  • Secrets and access: provide test credentials through the CI secret store and avoid printing them in logs or artifacts.

Playwright’s container guidance is particularly relevant when screenshot comparisons need a repeatable Linux environment. Containers reduce some environment variation; they do not make application data, fonts, external services, or timing deterministic by themselves.

5. Publish results and preserve failure evidence

Test status and diagnostic evidence are separate outputs. JUnit lets Azure Pipelines display pass/fail results in the test tab. Screenshots, HTML reports, and traces help explain what failed. Keep the artifact upload step running after a failure, and choose a retention period that matches how long your team investigates regressions.

Playwright can attach screenshots and traces to its HTML report or generate screenshots only on failure. Azure DevOps attachment behavior depends on the test result format and task. Microsoft documents VSTest/TRX and NUnit 3.0 attachment support for the Visual Studio Test task; other formats may require publishing separate build artifacts or using APIs. Do not assume that a screenshot file on the agent automatically appears as an attachment on a test result.

For test-case traceability, Azure Test Plans can associate automated test methods with test case work items. Microsoft describes this as a way to connect results to requirements and enable on-demand execution. This is optional for a screenshot comparison pipeline; it is useful when the team already manages test cases and requirements there.

6. Scale the workflow only when needed

Start with one job and a small, stable set of high-value pages. If the suite becomes too slow, Playwright supports sharding across jobs. Keep shard count and browser versions fixed enough that reproducing a failure remains practical. More parallel jobs can increase CI resource use and make shared test data or rate-limited preview environments a bottleneck.

Microsoft’s Azure Playwright Workspaces is an optional hosted execution route documented for GitHub Actions and Azure Pipelines. It requires a workspace, endpoint, and CI authentication setup; the Azure Pipelines route also calls for an Azure Resource Manager service connection. Use it when managed parallel execution fits your environment, not as a prerequisite for visual testing.

Consider platform and execution choices against repository permissions, supported test framework, browser and operating system coverage, screenshot consistency, artifact access, Test Plans traceability needs, and the operational cost of an added hosted service. The cited platform documentation does not establish a comparative performance benchmark between providers.

Or skip the browser setup

If your job is to capture reference images rather than exercise browser interactions, a screenshot API can remove the browser installation and rendering setup from that part of the pipeline. ScreenshotNeo is a website screenshot API and MCP server by Yorker Media. A single GET request returns an image or PDF, and its API accepts the parameter names used by other screenshot APIs. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
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 request failed: ${res.status}`);
await Bun.write('shot.webp', res);

Replace the example target URL with your own page and keep the API key in your CI secret store. ScreenshotNeo accepts cookie consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server exposes screenshot, page-info, and PDF tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up free for 1,000 screenshots a month, with no card.

Troubleshooting

Browser executable is missing

Cause: the job installed Node dependencies but not the browser binary, or the installed Playwright package and browser cache do not match. Fix: run npx playwright install --with-deps chromium in the job, and keep the package lockfile and any container image aligned with the Playwright version.

Tests pass locally but fail in CI with visual diffs

Cause: different operating system, fonts, browser build, viewport, device scale, locale, time zone, data, or animation timing. Fix: compare those inputs, use the same container or CI image for baseline creation and comparison, and make page state deterministic before updating snapshots.

The page is captured before it is ready

Cause: a fixed delay is too short, or networkidle never occurs because the app keeps connections open. Fix: wait for a page-specific locator or readiness signal and set a bounded timeout. Check the trace to see which request or condition delayed the page.

Azure Pipelines shows no test results

Cause: the test runner did not write JUnit XML where PublishTestResults@2 expects it, or the step was skipped after failure. Fix: configure the JUnit output path explicitly, verify it exists in the job workspace, and use condition: succeededOrFailed() on result and artifact publication.

Screenshots are missing from the test report

Cause: screenshots were written to disk but not attached in a supported result format or uploaded as artifacts. Fix: publish the Playwright report or screenshot directory as a pipeline artifact. If using Visual Studio Test attachments, confirm the documented format support for that task.

A GitHub workflow cannot access the preview environment

Cause: the preview URL is unavailable to the runner, requires credentials, or has not finished deploying. Fix: make the deployment dependency explicit, pass the preview URL through an environment variable, configure authentication via secrets, and add a readiness check before running the suite.

The first run fails because no snapshots exist

Cause: snapshot baselines have not been generated for the current project and platform. Fix: run the snapshot update command in the chosen baseline environment, inspect the images, and commit the approved baseline files.

Performance, reliability, and cost notes

  • Runtime: browser installation adds setup time, so cache package downloads where supported and install only the browser engines the suite uses. Avoid caching browser binaries across incompatible Playwright versions.
  • Reliability: bounded waits and deterministic app data reduce flaky captures. Capture traces and failure screenshots so a retry is not the only way to investigate.
  • Parallelism: sharding can shorten elapsed test time, but uses more runners and can overload a shared preview backend. Measure your own pipeline before increasing concurrency; no comparative speed figure is established by the sources here.
  • CI cost: GitHub and Azure runner use, artifact retention, and optional hosted execution are governed by the account and service configuration. Review those current terms in the relevant provider documentation.
  • API capture cost: ScreenshotNeo bills only clean shots; its page-verdict and billed response headers make outcomes visible to the calling job. The free tier and published paid tiers are described above.

FAQ

Do I need Azure Test Plans to run visual tests?

No. A pipeline can execute Playwright and publish results without Test Plans. Test Plans adds test-case and requirement traceability when the team needs it.

Can one Playwright suite run on both platforms?

Yes. Keep the suite and configuration in the repository and provide platform-specific pipeline files for installation, triggers, result publishing, and artifacts.

Can Microsoft-hosted Azure agents run visible UI automation?

Microsoft documents hosted agents for headless web UI testing. Visible UI scenarios may require a properly configured self-hosted Windows agent.

Does an API screenshot replace browser interaction tests?

No. An API is useful for rendering a URL into an image or PDF. Tests that click controls, validate behavior, or inspect application state still need a browser automation suite.

Sources