Here is a rewritten version of the article, preserving the original intent and technical depth while improving clarity and flow:

Automated Visual Regression Testing with Playwright

Visual comparison testing can be a powerful tool for catching regressions, especially during UI overhauls. However, it’s often fragile and slow. Fortunately, Playwright makes visual testing more accessible for web applications — though getting it just right still requires a bit of setup and customization.

Recently, I found myself with a rare stretch of downtime — the perfect excuse to tackle a long-standing issue: the CSS for a website I help maintain had become increasingly messy as we experimented with new features. Now that our requirements are clearer, it was time for some much-needed internal CSS refactoring. This would help reduce technical debt and allow us to take advantage of newer CSS features like nesting, while also laying the groundwork for a long-overdue dark mode implementation.

But I was hesitant to make large-scale changes without a safety net. I needed a way to catch visual regressions during the refactor. Snapshot testing — comparing screenshots before and after changes — seemed like the right tool. The problem? It’s notoriously brittle and slow.

Snapshot Testing: What It Is and Why It’s Tricky

In this context, snapshot testing involves capturing screenshots of web pages as a baseline, then comparing future renders against them. But many factors can affect these screenshots: timing issues, hardware differences, and even randomized content. Plus, you have to manage and store these image files, which adds complexity to the test setup.

Still, I decided to give it a try — just a small experiment to help during the refactor. This wouldn’t be part of our main test suite, just a temporary utility.

Getting Started with Playwright

I remembered that Playwright includes built-in support for visual comparisons. Even better, it doesn’t depend on many external packages — a plus for someone who tries to keep dependencies minimal.

While Playwright offers a handy setup command (npm init playwright@latest), I opted for a manual setup in a dedicated test/visual folder. This gave me better control and helped me understand how everything works.

Here’s a minimal package.json to get started:

{
“scripts”: {
“test”: “playwright test”,
“report”: “playwright show-report”,
“update”: “playwright test –update-snapshots”,
“reset”: “rm -r ./playwright-report ./test-results ./viz.test.js-snapshots || true”
},
“devDependencies”: {
“@playwright/test”: “^1.49.1”
}
}

After running npm install and npx playwright install (to download browser binaries), I created a basic playwright.config.js:

import { defineConfig, devices } from “@playwright/test”;

export let BROWSERS = [“Desktop Firefox”, “Desktop Chrome”, “Desktop Safari”];
export let BASE_URL = “http://localhost:8000”;
let SERVER = “cd ../../dist && python3 -m http.server”;
let IS_CI = !!process.env.CI;

export default defineConfig({
testDir: “./”,
fullyParallel: true,
forbidOnly: IS_CI,
retries: 2,
workers: IS_CI ? 1 : undefined,
reporter: “html”,
webServer: {
command: SERVER,
url: BASE_URL,
reuseExistingServer: !IS_CI
},
use: {
baseURL: BASE_URL,
trace: “on-first-retry”
},
projects: BROWSERS.map(ua => ({
name: ua.toLowerCase().replaceAll(” “, “-“),
use: { …devices[ua] }
}))
});

This configuration assumes your static site is built into a dist folder and served locally. I included multiple browsers for completeness, but you could reduce the list to speed up testing.

Writing a Simple Visual Test

Now for the actual test. Here’s a minimal sample.test.js:

import { test, expect } from “@playwright/test”;

test(“home page”, async ({ page }) => {
await page.goto(“/”);
await expect(page).toHaveScreenshot();
});

Running npm test will initially fail — because there’s no baseline screenshot yet. Run it again, and Playwright will compare the current render to the saved image.

If you change the site (e.g., by modifying dist assets), the test should fail, displaying a visual diff.

Playwright stores screenshots in a folder named after the test file (e.g., sample.test.js-snapshots), with filenames based on the test name and browser.

Generating Tests for All Pages

To test every page without writing repetitive code, I built a simple web crawler that discovers internal URLs and generates tests dynamically.

Playwright’s globalSetup feature lets us run code before test discovery. We’ll use it to crawl the site and save a list of URLs to a

Similar Posts