This is already a whole lot better: because now we are comparing real images. Instead of asserting that a component has an error classname, we are asserting that when we pass an error state to that component, that it should have red text, and a red border.

It also had some drawbacks as well 😢.

Problems with our approach

We had to store images in the codebase. Screenshots would depend on the machine they were taken on (so we would have had to use docker to ensure consistency). Having to stop animations with CSS before taking screenshots to avoid false negatives. No easy way for designers to review visual differences. Test performance — each test would take 1–2 seconds.

Percy and Puppeteer to the rescue!

While we could have spent time developing solutions to the problems with our homemade approach, we found an easier way. We decided to use Percy.

With Percy, we can take visual snapshots of our components, upload them to their service, and have their service analyze visual differences. We can integrate Percy into our build pipeline, and add a check that doesn’t allow pull requests to be merged if Percy detects unapproved visual regressions.

Percy is a visual testing platform that makes it easy to get started with visual testing. It captures DOM assets to render full page snapshots. You don’t have to worry about uploading assets or about updating to the latest browser. To avoid false positives and stabilize snapshots, it handle things like freezing animations and video, managing font rendering, anti-aliasing, and more.

Percy warning us about visual changes that will be introduced by a PR

Percy offers two SDKs relevant to React users: percy-storybook and percy-puppeteer. We decided to use percy-puppeteer because we wanted to take full control of our visual testing setup. We wanted to be able to interact with our components before taking snapshots, and we wanted to use the production rollup bundle of our library, something that isn’t possible with storybook.

Visual testing with our final bundle gives us confidence that our visual baselines are done with the same code that is consumed by our consumers.

Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

We use puppeteer to interact with our UI components before snapshotting them with Percy. Let’s take a look at how we integrated visual testing with Percy into our React component library that implements our design system.

Creating our setup using Percy and Puppeteer

First we installed the required development dependencies. We already use jest to run our integration tests, so it made sense to use jest and jest-puppeteer to run our visual tests.

yarn add -D @percy/puppeteer jest-puppeteer puppeteer

First up — rendering components for visual testing

In order to take snapshots of our components using percy-puppeteer, we needed a web server that renders our components in their different states. Our idea was that for each component, we would create a webpage that shows that component in all of it’s possible states. So for TextInput , we would want a page that shows a TextInput with a warning state, an error state, with a placeholder, without a placeholder, etc.

To do this, we created a new React app called visual-testing-app . It’s pretty basic — just a simple React app and an express server. You can check out the code here.

The idea was that for each UI component that we wanted to test visually, we would create two files: .visualspec.js and .visualroute.js .

Our visual-testing-app renders every file in our codebase ending in . visualroute.js as a route. These files need to expose two named exports: component and routePath .

This way, we can setup our visual test environments in these files, and access those routes in our . visualspec.js files.

For our TextInput component, we created text-input.visualspec.js and text-input.visualroute.js . The former would make assertion on the HTML created by the latter.