惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
Forbes - Security
Forbes - Security
Blog — PlanetScale
Blog — PlanetScale
P
Proofpoint News Feed
Apple Machine Learning Research
Apple Machine Learning Research
Google DeepMind News
Google DeepMind News
MyScale Blog
MyScale Blog
GbyAI
GbyAI
B
Blog RSS Feed
S
SegmentFault 最新的问题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
小众软件
小众软件
The Last Watchdog
The Last Watchdog
W
WeLiveSecurity
Webroot Blog
Webroot Blog
S
Security @ Cisco Blogs
Hugging Face - Blog
Hugging Face - Blog
Microsoft Security Blog
Microsoft Security Blog
月光博客
月光博客
博客园 - 聂微东
F
Fortinet All Blogs
H
Hacker News: Front Page
A
About on SuperTechFans
Application and Cybersecurity Blog
Application and Cybersecurity Blog
C
Check Point Blog
V
V2EX
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Y
Y Combinator Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
N
News | PayPal Newsroom
Cisco Talos Blog
Cisco Talos Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Cloudflare Blog
P
Privacy & Cybersecurity Law Blog
N
Netflix TechBlog - Medium
C
CXSECURITY Database RSS Feed - CXSecurity.com
Recorded Future
Recorded Future
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Google DeepMind News
Google DeepMind News
大猫的无限游戏
大猫的无限游戏
美团技术团队
Security Latest
Security Latest
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
I
InfoQ
Recent Announcements
Recent Announcements
The GitHub Blog
The GitHub Blog
Google Online Security Blog
Google Online Security Blog
T
The Exploit Database - CXSecurity.com

Matthias Ott

Hello Again, World This, Still Not for Everyone The Shape of Friction WeissKlang L1 – Punching Above Its Weight Continvoucly Morged Value Webspace Invaders To Affinity and Beyond The Mystery of Storytelling Amateurs! Echoes of Connection Linear() Is Not (That) Linear View Transitions: The Smooth Parts Adding AVIF and WebP Support to My Craft CMS Site Challenge Acoustic Room Treatment and Building Sound Panels, Part 1: Planning Play On Overshoot The HTML Output Element Listening Closely Compressed Fluid Typography The Lifeblood of the Web What Could Go Wrong? That’s My Rank Making Space CSS :is() :where() the Magic Happens Jane Goodall’s Famous Last Words European Tech Alternatives 🇪🇺 Independent Type Foundry Advent Calendar – Day 24: NaN Independent Type Foundry Advent Calendar – Day 23: Typotheque Independent Type Foundry Advent Calendar – Day 22: 205TF Independent Type Foundry Advent Calendar – Day 21: HvD Fonts Independent Type Foundry Advent Calendar – Day 20: Frere-Jones Type Independent Type Foundry Advent Calendar – Day 19: Fontwerk Independent Type Foundry Advent Calendar – Day 18: Vectro Independent Type Foundry Advent Calendar – Day 17: Studio René Bieder Independent Type Foundry Advent Calendar – Day 16: R-Typography Independent Type Foundry Advent Calendar – Day 15: David Jonathan Ross Independent Type Foundry Advent Calendar – Day 14: Interval Type Independent Type Foundry Advent Calendar – Day 13: Newglyph Independent Type Foundry Advent Calendar – Day 12: Swiss Typefaces Independent Type Foundry Advent Calendar – Day 11: Sharp Type Independent Type Foundry Advent Calendar – Day 10: Colophon Foundry Independent Type Foundry Advent Calendar – Day 9: Commercial Type Independent Type Foundry Advent Calendar – Day 8: Letters from Sweden Independent Type Foundry Advent Calendar – Day 7: Lineto Independent Type Foundry Advent Calendar – Day 6: Ohno Type Company Independent Type Foundry Advent Calendar – Day 5: Milieu Grotesque Independent Type Foundry Advent Calendar – Day 4: TypeMates Independent Type Foundry Advent Calendar – Day 3: Klim Type Foundry Independent Type Foundry Advent Calendar – Day 2: Dinamo Independent Type Foundry Advent Calendar – Day 1: Grilli Type The Independent Type Foundry Advent Calendar 2022 A Conversation With ChatGPT ChatGPT, please explain websites in the words of William Shakespeare Transient Frameworks Leaving Twitter Behind Converting Your Twitter Archive to Markdown The Wrong Question It Wasn’t Written Syndicating Posts from Your Personal Website to Twitter and Mastodon Suspension None of Your Business Shitty Code Prototypes Doing Our Part Patch That Package Brain Dump Generating Accessibility Test Results for a Whole Website With Evaluatory The CSS Cascade, a Deep Dive Updates About Updates How to Delete Your Commit History in Git Unblocking Your Writing Blocks, Part 2: I’m Not an Expert nor a “Thought Leader” Connections No Wrong Notes Better Options Design Debt Finite and Infinite Games Don’t Assume, Validate. Necessity Is the Ultimate Teacher One Egg Go Deep There Is No Secret Code Balancing Risk Blue Eyes, Brown Eyes The Shortcut Boomerang My RSS Feed Collection of Personal Websites Frequency The Illusion of Control The Decisions Journey Write It Down Nownownow Into the Personal-Website-Verse Considering the Opposite What is it for? Unlimited Bowling. Never done. We Are Team Internet. We Need to Save #NetNeutrality. Progressive Search Data loss (also) by JavaScript Books I Will Definitely Maybe Read in 2017 Starting to Write Notes
Visual Regression Testing for External URLs With Playwright
Matthias Ott · 2025-10-07 · via Matthias Ott

We’ve all been there: You write a bit of CSS, check whether everything looks right. You deploy. Then someone sends you a screenshot: the mobile navigation is broken. And why is the size of those headings just a bit off? And where has that button gone?

Especially when you are working on a larger codebase together or you are refactoring your CSS or consolidating redundant styles, seemingly small changes in one corner of your CSS (or JavaScript) can have repercussions in a seemingly unrelated component. If you ever changed your base typography styles, you know what I mean. The Cascade and inheritance are powerful features, but as your codebase grows, tracking possible interdependencies can become a challenge.

This is when visual regression testing can be useful. Instead of manually checking every page after each CSS change, you capture screenshots automatically and compare them against a baseline – the gold standard. When something shifts, breaks, disappears, or turns lightgoldenrodyellow, you’ll know immediately.

In a recent client project, we had that exact challenge: we needed to check a large amount of pages – more than 1500 – for how well they digested a rework of the HTML structure and the CSS of all headings in all frontend components. And we needed to compare the changes deployed to a staging system to the production site.

But How? #

A first search surfaced a great article by Monica Dinculescu from 2017 in which she explained how to setup visual diffing with Puppeteer. At the same time, I found another interesting solution: the testing framework Vitest can now run native visual regression testing out of the box.

While this seems to be a fantastic solution for testing components or pages within your own local (or cloud) test environment, it is designed for the experimental browser mode feature in the Vitest API, which means it can’t navigate to external URLs. So if, like in our case, you want to test a staging environment against a production site, you need a different approach.

At first, I thought of combining Vitest and Playwright, but looking into the docs, I discovered that Playwright Test includes the ability to produce and visually compare screenshots using expect(page).toHaveScreenshot(). And because Playwright is built for browser testing, it can visit external URLs – and even interact with pages, if needed. 🎉

Setup #

The initial setup turned out to be easier than expected. All you basically need is a new project and install Playwright Test. So you could start a new project with

npm init

using playwright test as the test command in the project setup dialog and then install Playwright Test with

npm install -D @playwright/test

Alternatively, you can also scaffold a new Playwright project with

npm init playwright@latest

This will give you a package.jsonfile similar to this one – I added a few more useful test commands already:

{
  "name": "playwright-visual-regression",
  "version": "1.0.0",
  "type": "module",
  "description": "Little demo project for visual diff testing with Playwright",
  "scripts": {
    "test": "playwright test",
    "test:update": "playwright test --update-snapshots",
    "test:debug": "playwright test --debug"
  },
  "author": "Matthias Ott",
  "license": "MIT",
  "devDependencies": {
    "@playwright/test": "^1.56.0"
  }
}

The Script #

Now the juicy part, the test script. Which also turned out to be a lot shorter than expected. I saved it as ./tests/visual-regression.test.js. Playwright automatically executes all files in a tests (or test) folder that end in *.spec.ts/*.spec.js or *.test.ts/*.test.js.

We start by importing Playwright’s test runner and defining a config object for a few basic settings:

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

/**
 * Configuration:
 * - baseUrl: The base URL of the site to test.
 * - routes: The routes to test (relative to baseUrl).
 * - viewports: Different viewport sizes to test (width, height, icon).
 */
const CONFIG = {
	
  baseUrl: 'https://matthiasott.com',
  routes: [
    '', 
    'notes', 
    'articles', 
    'workshops', 
    'links', 
    'about'
  ],
  viewports: {
    wide: { width: 1280, height: 800, icon: '🖥️' },
    narrow: { width: 375, height: 667, icon: '📱' },
  },
};

And now off to the actual tests – read the comments to see what each section does:

/**
 * Tests
 */
 
// We loop through each viewport configuration
Object.entries(CONFIG.viewports).forEach(([viewport, { width, height, icon }]) => {
  
  // Create a test group for this viewport
  // test.describe() groups related tests 
  // together in the console output
  test.describe(`${icon} ${viewport}`, () => {
    
    // Configure Playwright to use this viewport size
    test.use({ viewport: { width, height } });

    // Now we loop through each route we want to test …
    CONFIG.routes.forEach(route => {
      
      // … and define an individual test for this route. 
      // With the `page` object, Playwright controls the browser
      test(`📸 ${route || ''}`, async ({ page }) => {
        
        // Build the complete URL        
        const url = `${CONFIG.baseUrl}/${route}`;
        
        // Navigate to the URL
        // (waitUntil: 'networkidle' waits for the page to finish loading)
        await page.goto(url, { waitUntil: 'networkidle' });
        
        // Now we can take a screenshot and compare it to the baseline
        await expect(page).toHaveScreenshot({
          // Only capture the viewport, not entire scrollable page
          fullPage: false,
          // How much difference do we allow between the screenshots?
          maxDiffPixels: 100,   
        });
      });
    });
  });
});

And that’s it! The brilliant thing about using Playwright is that we don’t have to generate diff images that show the difference between the baseline and a new screenshot separately. Playwright does this automatically for us and puts the respective diff images into the image folder alongside the baseline and the screenshots to compare against.

The last step is to run the test:

npm test

On the first run, Playwright will create the baseline screenshots. On subsequent runs, it will take new screenshots and compare them against the original gold standard versions.

Screenshot Playwright test results

There are a few Playwright commands that can make this script even more useful, for example:

# Update baseline screenshots, for example after updating your design
npm run test:update

# Step-by-step debugging of failing tests with Playwright Inspector
npm run test:debug

A really useful last adjustment can be to generate an HTML report of the test results. To do this, we need to slightly adjust the test script section in our package.json and add the --reporter=html flag to the playwright test command, so that a “reporter” is specified. Then, we can add another script command to show the results ("test:report": "playwright show-report"):

"scripts": {
    "test": "playwright test --reporter=html",
    "test:update": "playwright test --update-snapshots",
    "test:debug": "playwright test --debug"
    "test:report": "playwright show-report",
  },

Now Playwright will serve an HTML report if there are any errors. To show the results of our test we can also use this command:

# View failed test results with visual diff viewer
npm run test:report
Playwright test results report overview
Playwright test results detail view

Of course, this is just the first version of this script which could be improved further. You could add a Playwright config file that changes the output directory or move the config options from the top of the script into the config file to make it easier to reuse the testing script in different projects, for example. Playwright also allows you to install different browser engines, like chromium, firefox, or webkit – all default engines can be installed with the command npx playwright install. So you could even use a specific browser to make your screenshots. You could also add authentication, in case your staging environment is password-protected, integrate the visual regression script into a CI workflow, and, and, and …

Now I’m curious: are you using any type of visual regression testing – or any testing at all? Let me know, for example on Mastodon, Bluesky, or by email

This is post 6 of Blogtober 2025.

~

20 Webmentions

5 Reposts

14 Likes

ⓘ Webmentions are a way to notify other websites when you link to them, and to receive notifications when others link to you. Learn more about Webmentions.