Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversHalloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix Now×
FREEDOM251

Freedom251 › Blog › Continuous integration

How to Run Visual Regression Screenshots for Vite Apps in GitLab CI

By Freedom251 Team · · 13 min read

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

Run the visual suite in a pinned Playwright container, not on an arbitrary GitLab runner: build the Vite app, let Playwright start vite preview on a fixed URL, assert each page with toHaveScreenshot, and always upload the report, actual image, expected image, diff, and trace as GitLab artifacts. Pinning the container and every rendering input (browser, fonts, viewport, locale, timezone, data, and motion) is what turns a pixel comparison into a useful regression test instead of a flaky job.

The workflow that works reliably

A Vite visual-regression job has four distinct responsibilities:

As an Amazon Associate I earn from qualifying purchases.

  1. Produce the same application: install the lockfile dependencies with npm ci and run npm run build.
  2. Serve it at a known address: use Vite’s preview server on a fixed port and host.
  3. Render and compare: run Playwright in its official, version-pinned container and compare screenshots with expect(page).toHaveScreenshot().
  4. Preserve evidence: publish JUnit output and the images and traces from every run, including failed jobs, as GitLab artifacts.

Keep the first baseline narrow: one browser project, one viewport, and one container image. Add more browser projects only when cross-browser behavior is an explicit requirement. Every baseline image should be reviewed as a code change; a failed assertion must not silently rewrite the expected image.

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

Repository prerequisites

Package scripts and lockfile

Commit package-lock.json (or the lockfile used by your package manager) and provide scripts equivalent to these:

#1 Best Overall
{
  "scripts": {
    "build": "vite build",
    "preview": "vite preview",
    "test:visual": "playwright test"
  }
}

Install Playwright and its test runner as development dependencies. The CI image supplies the browser binaries and operating-system libraries, so the job should use the same Playwright version as the project. Do not mix a locally generated baseline from one Playwright or browser build with a different image in CI.

A stable preview address

Vite’s development server is useful for interactive work, but a visual job should test the production build. The usual sequence is npm run build followed by npm run preview -- --host 0.0.0.0. Playwright can launch that command itself and wait until http://127.0.0.1:4173 responds, so no hand-written sleep is needed.

Configure Playwright for deterministic screenshots

Create playwright.config.ts at the repository root. This configuration fixes the URL, viewport, browser project, reporters, and server lifecycle while allowing local runs to reuse an already running server.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  expect: {
    timeout: 10_000,
  },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 1 : 0,
  reporter: [
    ['list'],
    ['junit', { outputFile: 'test-results/junit.xml' }],
    ['html', { outputFolder: 'playwright-report', open: 'never' }],
  ],
  use: {
    baseURL: 'http://127.0.0.1:4173',
    viewport: { width: 1280, height: 720 },
    deviceScaleFactor: 1,
    locale: 'en-US',
    timezoneId: 'UTC',
    colorScheme: 'light',
    reducedMotion: 'reduce',
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
    video: 'off',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
  webServer: {
    command: 'npm run preview -- --host 0.0.0.0',
    url: 'http://127.0.0.1:4173',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
});

The webServer.url check is both a readiness wait and a useful failure signal: if the preview process exits or never binds, Playwright reports that before it attempts a page assertion. Use a health endpoint instead if your app needs a separate readiness check, but keep the URL stable between local and CI runs.

Write a visual test that waits for real content

Put tests in tests/. The first run creates an expected image only when you explicitly approve it; subsequent runs compare the rendered page with that committed file.

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

test('home page has the approved visual', async ({ page }) => {
  await page.goto('/', { waitUntil: 'networkidle' });
  await page.evaluate(() => document.fonts.ready);

  await expect(page).toHaveScreenshot('home.png', {
    animations: 'disabled',
    caret: 'hide',
    fullPage: true,
  });
});

animations: 'disabled' stops CSS transitions and animations during the assertion. Waiting for document.fonts.ready prevents a comparison between fallback and web fonts. If your application loads data after the network becomes idle, wait for a semantic locator instead:

await page.goto('/dashboard');
await page.getByRole('heading', { name: 'Dashboard' }).waitFor();
await page.locator('[data-testid="orders-table"]').waitFor();
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot('dashboard.png', {
  animations: 'disabled',
  fullPage: true,
});

Prefer selectors that express user-visible state over arbitrary delays. A fixed delay can mask a slow request on one runner and still capture a spinner on another.

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.

Freeze volatile inputs in the test

Mock values that are expected to change without representing a UI change. Dates, random IDs, advertisements, analytics responses, and third-party APIs are common sources of unexplained diffs. Seed fixtures or intercept requests before navigation:

test.beforeEach(async ({ page }) => {
  await page.addInitScript(() => {
    Date.now = () => 1_735_689_600_000;
  });

  await page.route('**/api/orders', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        orders: [
          { id: 'order-1', status: 'Ready', total: '$42.00' },
        ],
      }),
    });
  });
});

Use a test-only fixture or deterministic API response rather than changing production behavior. If a visual change is intentional, update the baseline in a separate, reviewable commit and describe why it changed.

Create and review baseline files

  1. Run the test locally in the same Playwright container tag you will use in CI.
  2. Generate a baseline explicitly with npx playwright test --update-snapshots after checking that the page is fully loaded and deterministic.
  3. Inspect every generated PNG. Commit the expected images in the snapshot directory created beside the test file (for example, tests/home.spec.ts-snapshots/).
  4. Run the suite without --update-snapshots. A changed pixel must fail the job and produce actual, expected, and diff files.

Do not add --update-snapshots to the CI command. Automatic updates would convert a regression into a new baseline before anyone reviews it.

GitLab CI job

Playwright’s documented GitLab pattern uses its public Docker image. Pin the tag so browser binaries, system libraries, and fonts remain stable:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
stages: [visual]

visual_regression:
  stage: visual
  image: mcr.microsoft.com/playwright:v1.64.0-noble
  variables:
    CI: "true"
  script:
    - npm ci
    - npm run build
    - npx playwright test
  artifacts:
    when: always
    expire_in: 14 days
    paths:
      - playwright-report/
      - test-results/
      - tests/**/*.png
    reports:
      junit: test-results/junit.xml

when: always is essential. A visual assertion normally makes the job fail, and without that setting GitLab can discard the very files needed to diagnose it. The example retains the HTML report, JUnit XML, traces, actual and expected images, and diffs for 14 days. Set expire_in to your team’s review window; longer retention consumes more artifact storage.

What each artifact is for

Path Use
playwright-report/ Interactive HTML report with test steps and links to attachments.
test-results/ JUnit XML, traces retained on failure, and Playwright’s per-test output.
tests/**/*.png Committed baselines plus generated actual and diff images, depending on your Playwright output layout.

Open the job’s Artifacts panel to browse these paths. The failed test details in the HTML report are usually the quickest way to compare the three images; the trace lets you inspect navigation, network, and page state at the moment of failure.

JUnit results and inline screenshot attachments

GitLab’s test summary needs a file, not a directory. Configure the JUnit reporter with an output file such as test-results/junit.xml, then reference that exact file under artifacts:reports:junit.

GitLab can show an image directly in failed-test details when the JUnit testcase contains a system-out attachment entry. The path is relative to $CI_PROJECT_DIR, and the same file must be included in artifacts:paths:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<system-out>[[ATTACHMENT|test-results/home-failure.png]]</system-out>

If your reporting layer adds this entry, use a path that exists in the job workspace. A directory name such as test-results/ is not a valid JUnit report path, and an attachment that is not uploaded cannot be opened from the GitLab dialog.

Expired report artifacts can leave the Test summary empty even though the pipeline ran successfully. Keep report and image expiration aligned with the period in which reviewers need to inspect a change.

Make rendering deterministic

Most visual flakes are environment differences, not random Playwright failures. Control the inputs that affect pixels:

  • Browser and operating system: use the same pinned Playwright image for baseline generation and CI. Do not compare a local macOS baseline with a Linux container baseline unless you intentionally accept platform-specific rendering.
  • Fonts: install or bundle the exact fonts used by the app, wait for document.fonts.ready, and investigate missing glyphs before accepting a diff.
  • Viewport and scale: set width, height, and device scale factor explicitly. A different viewport can change responsive breakpoints and line wrapping.
  • Locale and timezone: fix both so date, number, and translated strings do not vary by runner.
  • Color scheme and motion: choose light or dark mode deliberately, set reduced motion, and disable CSS transitions and animations before capture.
  • Network data: mock external APIs and use seeded fixtures. Do not let ads, rotating content, random identifiers, or current timestamps enter a baseline.
  • Lazy content: for full-page screenshots, scroll or wait for the application to finish loading images and sections that appear below the fold.

When a difference is intentional because of a browser or font update, regenerate all affected baselines in the pinned environment and review the resulting diff as a normal code change. Do not hide broad changes with a large pixel threshold; a threshold can be useful for unavoidable anti-aliasing, but it can also conceal a real layout regression.

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

Full-page, element, and multi-state coverage

Full-page pages

Use fullPage: true when the page’s vertical layout matters. Ensure lazy-loaded images are triggered before the assertion; otherwise the screenshot can contain placeholders even though the page eventually displays content.

One component or region

Capture a stable element when a full-page image would create unnecessary churn:

await expect(page.locator('[data-testid="pricing-card"]')).toHaveScreenshot(
  'pricing-card.png',
  { animations: 'disabled' }
);

Element screenshots are useful for a component with a controlled fixture, while a page screenshot verifies integration, spacing, and responsive composition. Keep names tied to the state being tested, such as checkout-error.png rather than a generic screen.png.

Dark mode and responsive states

Create explicit projects or tests for states that are contractual. For example, a second project can set colorScheme: 'dark', and a mobile project can use a fixed device preset. Do not add every browser and viewport by default: each combination multiplies baseline files, runtime, and review work.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

Scaling the suite in GitLab

When a single job becomes too slow, split the same deterministic suite across GitLab runners. Playwright accepts a shard in the form --shard=INDEX/TOTAL. GitLab exposes the corresponding values as CI_NODE_INDEX and CI_NODE_TOTAL when parallel is used:

visual_regression:
  stage: visual
  image: mcr.microsoft.com/playwright:v1.64.0-noble
  parallel: 4
  script:
    - npm ci
    - npm run build
    - npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
  artifacts:
    when: always
    expire_in: 14 days
    paths:
      - playwright-report/
      - test-results/
      - tests/**/*.png
    reports:
      junit: test-results/junit.xml

Sharding reduces wall-clock time but creates four artifact sets and can make a failure harder to reproduce if tests share state. Keep tests isolated, use deterministic fixtures, and make each shard’s JUnit output unique if your GitLab configuration merges reports. For larger matrices, combine browser projects and shards with GitLab’s matrix jobs, but add a browser only when its rendering behavior is part of the product contract.

Troubleshooting failed or flaky jobs

“The webServer was not able to start”

Cause: the build failed, the preview command is missing, port 4173 is occupied, or the server is bound only to localhost inside the container. Fix: inspect the build log, verify the preview script, pass --host 0.0.0.0, and keep the URL in webServer identical to the address Playwright uses.

Every screenshot differs by text wrapping or glyph shape

Cause: a different font package, browser build, device scale factor, or viewport. Fix: regenerate the baseline in mcr.microsoft.com/playwright:v1.64.0-noble, ensure the app’s fonts are available in that image, and pin viewport and scale. Do not approve the diff until you know which input changed.

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

The screenshot contains a spinner, blank image, or old API data

Cause: the assertion ran before the UI’s readiness condition or a volatile request completed. Fix: wait for a meaningful locator, wait for fonts, mock the request with seeded data, and use a bounded timeout rather than an arbitrary long sleep.

Only animated regions fail intermittently

Cause: a transition, cursor, video, or canvas is captured at a different frame. Fix: pass animations: 'disabled', hide the caret, inject a test stylesheet for transitions that the option cannot control, and replace video or canvas animation with a deterministic poster or fixture.

The job fails but there is no image to inspect

Cause: artifacts were uploaded only on success, the PNG path does not match the actual output directory, or the files expired. Fix: set artifacts:when: always, include both the report directories and tests/**/*.png, verify the paths in the job workspace, and choose an expiration period that covers review.

GitLab shows no tests in the Test summary

Cause: artifacts:reports:junit points to a directory or a file that was never created. Fix: configure Playwright’s JUnit reporter with an explicit output file and use that exact relative path in the YAML. Keep the JUnit file even when tests fail.

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

A baseline update creates hundreds of unrelated diffs

Cause: a browser, operating-system library, font, locale, or shared fixture changed. Fix: compare the container tag and dependency lockfile with the previous run, restore the old environment if the change was accidental, or make one deliberate environment update and review all resulting baselines together.

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

Performance, reliability, and storage decisions

Visual testing costs more than a DOM assertion because it renders a real browser and stores binary evidence. Keep CI time predictable by using one browser project initially, avoiding unnecessary full-page captures, mocking slow external services, and sharding only after tests are isolated. A page-level screenshot catches composition regressions; an element-level assertion can cover a reusable component with less image data.

Artifact retention is an operational trade-off. Retaining 14 days is enough for many review cycles, while a long retention period increases storage usage. Keep baselines in Git because they are reviewed alongside code; keep generated actual, diff, trace, and HTML files as short-lived CI artifacts. If a merge request needs a longer audit trail, extend expiration for that pipeline rather than permanently retaining every failure.

Reliability comes from reproducibility, not from making assertions permissive. A stable image, fixed inputs, explicit readiness checks, and reviewable baseline changes provide a clear signal when pixels change for a real reason.

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

Or skip the browser setup

If you need a clean screenshot of a deployed Vite page rather than a repository-bound Playwright diff, ScreenshotNeo provides a website screenshot API and MCP server. It accepts a URL and returns PNG, JPEG, WebP, or PDF. Before capture it accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers.

One request is enough to capture a public preview or staging URL (replace the URL with your own):

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

See the complete parameter list and authentication details in the ScreenshotNeo API documentation. The same request from Python is:

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)

And in 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 fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo has 63 options, including full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets plus arbitrary viewports, retina scale, PDF paper size and page ranges, HTML/CSS-to-image, custom JavaScript and CSS, pre-capture clicks, hidden selectors, selector or network-idle waits, ad and tracker blocking, custom headers, cookies, user agents and Authorization, timezone and geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture of 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify a migration.

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

Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients, so an AI agent can inspect a page without you wiring a browser runner. Every feature is available on every plan:

Plan Included screenshots Price
Free 1,000 per month $0, no card
Starter 3,000 $5
Growth 15,000 $15
Pro 60,000 $39
Scale 250,000 $99
Business 1,000,000 $249

Yearly billing gives two months free. This API does not replace committed Playwright baselines and GitLab’s diff review when you need a pass/fail regression gate, but it is a practical way to obtain clean reference images, capture pages outside your CI network, or let an agent inspect a deployment. Sign up for 1,000 free screenshots a month with no card.

A practical decision checklist

  • Is the Vite production build created with the lockfile and served by a fixed preview URL?
  • Does CI use the same pinned Playwright image used to create approved baselines?
  • Are viewport, scale, browser, locale, timezone, color scheme, fonts, and motion fixed?
  • Do tests wait for fonts and meaningful application state instead of sleeping for an arbitrary duration?
  • Are dates, random IDs, ads, and external API responses seeded or mocked?
  • Are baseline updates explicit code-review changes rather than an automatic CI side effect?
  • Does GitLab always retain JUnit XML, HTML reports, traces, expected images, actual images, and diffs?
  • Does the JUnit path point to a file, and do any attachment paths point to files uploaded as artifacts?
  • Would one browser and one viewport provide enough coverage before you add matrix or shard complexity?

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.

Continuous integrationfrontend testingGitLab CIPlaywrightvisual regression testingVite

Leave a Reply

Your email address will not be published. Required fields are marked *

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

Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.