How to Run Visual Regression Tests for a Vue Website with Playwright
Build reliable visual regression tests for Vue with Playwright: set baselines, stabilize screenshots, review diffs, and handle common failures.
Use Playwright Test’s built-in screenshot assertion, await expect(page).toHaveScreenshot('home.png'), to compare a Vue page against a committed baseline. The reliable workflow is to serve the app in a controlled test environment, navigate to a predictable state, capture and review the initial baseline, then inspect every diff before updating snapshots. Pin the browser and operating system used for baseline generation and CI because rendering can vary across environments.
1. Install Playwright and configure the Vue app
Install Playwright Test and its browser binaries in your Vue project:
npm init playwright@latest
npx playwright install
If Playwright is already configured, keep the existing setup and add the visual test. Configure Playwright to start the Vue development server before tests and reuse it locally. In CI, start a fresh server so the test run has a known app instance.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
reporter: 'html',
use: {
baseURL: 'http://127.0.0.1:4173',
trace: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'npm run dev -- --host 127.0.0.1 --port 4173',
url: 'http://127.0.0.1:4173',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
Use a server command and port appropriate to your project. If your production build is the target, build it first and serve the build output instead of using the development server. Keep the browser version, operating system, viewport, and relevant browser settings aligned between baseline generation and CI.
2. Add a page-level visual regression test
Create a test for a route and state whose appearance matters. Control the test data and avoid relying on changing third-party services. Wait for the page state you intend to protect, then assert on the screenshot.
// tests/home.visual.spec.ts
import { test, expect } from '@playwright/test';
test('home page matches its visual baseline', async ({ page }) => {
await page.goto('/');
await page.getByRole('heading', { name: 'Welcome' }).waitFor();
await expect(page).toHaveScreenshot('home.png', {
fullPage: true,
});
});
Replace the heading locator with a stable signal from your app, such as a page title or a test-specific ready marker. A full-page screenshot covers the route from top to bottom; omit fullPage to capture the viewport. Playwright waits for two consecutive screenshots to be identical before comparing, and screenshot assertions disable animations by default.
3. Generate and commit the baseline
- Run
npx playwright test tests/home.visual.spec.ts. - On the first run, Playwright creates the expected screenshot snapshot. Find it in the snapshot directory associated with the test and browser project.
- Open and review the image at its actual size. Confirm it represents the intended page state.
- Commit the test and its expected screenshot together.
On later runs, Playwright compares the current rendering with that committed reference. A failed assertion produces actual and diff output for review. Do not update a baseline just to make a failing test pass: first decide whether the change is an intended design update or a regression. For an intentional change, regenerate snapshots in the same pinned environment and review the image changes in the code review.
# Update snapshots only after reviewing the intended UI change
npx playwright test --update-snapshots
4. Make screenshot output deterministic
Visual tests are only useful when unrelated variation is controlled. Stabilize the inputs that affect rendering before adjusting image comparison tolerance.
- Use the same environment: generate and compare baselines with the same operating system and browser version. Rendering can also vary with browser settings, hardware, power source, and headless mode.
- Control data: use fixtures or seeded test data for content, dates, randomized values, and user-specific state. Avoid external services whose responses can change independently of your app.
- Wait for meaningful readiness: wait for a visible app state or a selector, rather than adding arbitrary delays. If the UI depends on async data, provide a controlled response or fixture.
- Handle known motion and dynamic regions deliberately: Playwright disables animations for screenshot assertions. For known volatile elements, use screenshot options such as masks or a screenshot stylesheet via
stylePathto hide or adjust just those elements. - Keep the viewport explicit: choose a fixed viewport for each baseline. Add separate browser projects or viewport-specific tests only for environments your product supports and needs to protect.
- Start with strict comparison: the matcher supports pixel-difference and threshold controls. Relax them only for a documented rendering variation. Too much tolerance can allow real layout changes through.
Consult the Playwright visual comparisons guide and the PageAssertions API for the complete screenshot assertion options. Playwright also recommends controlling dependencies and data in its best practices.
5. Choose page or component coverage
| Test scope | What it protects | When to use it |
|---|---|---|
| Page screenshot | The integrated route, layout, and visible content | Important user-facing pages and key workflows |
| Component screenshot | A focused component state, with less unrelated page content | Reusable components and states that are easier to inspect in isolation |
Playwright component testing runs components in a real browser and supports Vue. Mount the component in the component testing setup, then assert on the returned root locator so the screenshot excludes unrelated gallery content.
// Illustrative Vue component test; use the mount helper from your configured setup
import { test, expect } from '@playwright/experimental-ct-vue';
import PrimaryButton from './PrimaryButton.vue';
test('primary button state matches its baseline', async ({ mount }) => {
const component = await mount(PrimaryButton, {
props: { label: 'Continue' },
});
await expect(component).toHaveScreenshot('primary.png');
});
Component testing requires the Playwright component-testing package and project setup for your Vue version; follow the official component testing guide for the current configuration.
6. Review a visual failure
- Open the failed test output and compare the expected, actual, and diff images.
- Check whether the difference is a true app change, a changed test input, or an environment mismatch.
- Inspect the trace for the action timeline, DOM snapshots, and network requests that explain how the page reached its captured state.
- Fix the app or test setup for regressions and instability. If the UI change is intended, update the baseline and review the new snapshot files.
Playwright Trace Viewer can show screenshot comparisons alongside DOM and network details. UI Mode can help step through and inspect test runs.
7. Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Snapshots fail only in CI | Different OS, browser version, headless mode, or rendering environment | Generate and compare baselines in a consistent pinned environment. Avoid sharing snapshots across incompatible environments. |
| Diff changes between identical commits | Uncontrolled time, random content, async data, or third-party responses | Seed or fixture the data, control dates and network dependencies, and wait for a stable app marker. |
| Screenshot is blank or captures a loading state | The test navigated before the required UI was ready | Wait for a meaningful visible locator or provide the async dependency deterministically. Avoid relying only on a fixed sleep. |
| Full-page image differs below the fold | Lazy-loaded content may not have settled, or the page has dynamic lower sections | Ensure the page reaches the intended state before capture, control changing content, and inspect the actual screenshot before changing the baseline. |
| Small text or antialiasing differences fail | Platform or browser rendering differs | Align the rendering environment first. If variation is unavoidable and understood, adjust the matcher threshold narrowly and document why. |
| Every test run proposes a baseline update | The snapshot path or project environment changed, or snapshots were not committed | Check the test name, browser project, snapshot directory, and version-control status; commit the intended expected images. |
| A component snapshot includes surrounding content | The assertion targets the page or gallery instead of the mounted component root | Call toHaveScreenshot() on the locator returned by the component mount helper. |
8. Performance, reliability, and maintenance
Visual assertions add browser rendering and image comparison work to the test run. Keep the suite focused on important routes and representative states, and use component tests for isolated states that do not need a full route. Parallel tests can reduce elapsed time, but each test should have independent, controlled inputs so scheduling does not change its output.
Reliability comes primarily from stable inputs and a consistent rendering environment. Keep expected snapshots under version control, review image diffs alongside code, and update only the references affected by an intentional UI change. Choose viewport and browser coverage based on the environments you support; each additional environment needs its own consistent baseline.
Or skip the browser setup
For a one-off capture of a deployed Vue page, ScreenshotNeo can return a screenshot from one GET request. It is a website screenshot API and MCP server for developers. The API also supports options such as full-page capture, viewport and device presets, custom CSS and JavaScript, wait conditions, and image formats. See the ScreenshotNeo API documentation for parameters.
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}`);
ScreenshotNeo accepts cookie and 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, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers say the page verdict and whether the capture was billed. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. See ScreenshotNeo for the product and plans.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Does the first Playwright run create the expected screenshot?
Yes. The first run creates the baseline. Review and commit it so later runs can compare against that reference.
Should I update snapshots whenever CI fails?
No. First inspect the diff and determine whether the change is intended. Update snapshots only for reviewed visual changes.
Can I use this approach for isolated Vue components?
Yes. Playwright component testing supports Vue; assert on the mounted component root locator to keep the capture focused.
Why does the same page produce different screenshots on another machine?
Browser and operating system rendering can differ, as can app data and browser settings. Keep the environment and inputs controlled between baseline creation and comparison.


