Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →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.
- Produce the same application: install the lockfile dependencies with
npm ciand runnpm run build. - Serve it at a known address: use Vite’s preview server on a fixed port and host.
- Render and compare: run Playwright in its official, version-pinned container and compare screenshots with
expect(page).toHaveScreenshot(). - 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.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →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.
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.
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:
Rank #2
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
- Run the test locally in the same Playwright container tag you will use in CI.
- Generate a baseline explicitly with
npx playwright test --update-snapshotsafter checking that the page is fully loaded and deterministic. - Inspect every generated PNG. Commit the expected images in the snapshot directory created beside the test file (for example,
tests/home.spec.ts-snapshots/). - 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:
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:
<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.
Rank #3
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.
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.
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:
Rank #4
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.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsThe 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.
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.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.
Recommended Free Tools
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.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallIts 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.
Quick Recap
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.

