Todd Wolfson

Software Engineer

February 25, 2014

Visual regression tests/perceptual diffs are a way to test your website to verify the appearance stays consistent.

Example image of perceptual diff

For, I use a home-grown screenshot + image-diff script.

If you want something out of the box, there are a few existing solutions:

At the time I wrote the script, only dpxdt existed which was overkill for me.

Once you have your test suite set up, add in Travis CI. After the first test run, you should run into consistency issues (e.g. fonts are not the same). There are a few solutions for this:

  • Use near-identical isolated environment to test environment (e.g. Ubuntu Vagrant for Travis CI)
  • Use consistent isolated sub-environment in development and test environment (e.g Vagrant for both)
    • If your test environment is virtualized, this will not work (e.g. Travis CI)
  • Generate screenshots in remote environment (e.g. Sauce Labs, BrowserStack)

For this blog post, we will be walking through setting up near-identical environments.

To gain insight into what Travis CI's screenshots look like I have written a script that uploads my actual and diff screenshots to

# Install underscore-cli for hacking
if ! which underscore &> /dev/null; then
  npm install -g underscore-cli

# Navigate to pereceptual-test directory
cd test/perceptual-tests

# Prepare location to collect delete commands
if test "$TRAVIS_BUILD_NUMBER" = ""; then

# curl from via
# Documentation:

for filepath in $(find {actual,diff}_screenshots -name '*.png'); do
  result="$(curl -H "Expect: " -F "key=$api_key" -F "image=@$filepath" )"
  # result='{"rsp":{"stat":"ok","image":{"image_hash":"dKZ0YK9","delete_hash":"r0MsZp11K9vawLf","original_image":"http:\/\/\/dKZ0YK9.png","large_thumbnail":"http:\/\/\/dKZ0YK9l.jpg","small_thumbnail":"http:\/\/\/dKZ0YK9s.jpg","imgur_page":"http:\/\/\/dKZ0YK9","delete_page":"http:\/\/\/delete\/r0MsZp11K9vawLf"}}}'
  if test "$(echo "$result" | underscore extract 'rsp.stat')" != '"ok"'; then
    echo "There was a problem uploading \"$filepath\"" 1>&2
    echo "$result" 1>&2
    download_cmds="${download_cmds}wget -O \"$output_dir/$filepath\" $(echo "$result" | underscore extract 'rsp.image.original_image')\n"
    delete_cmds="${delete_cmds}curl$(echo "$result" | underscore extract 'rsp.image.delete_hash' --outfmt text).json;"

echo "All uploads complete!"
echo ""
echo "Download via:"
echo "    mkdir -p $output_dir/{actual,diff}_screenshots"
# DEV: `echo -e` processes line feeds
echo -e "    $download_cmds"
echo "Delete via:"
echo -e "    $delete_cmds"

Inside my .travis.yml, I run the upload script when a failure occurs:

  - 'npm run test-perceptual || (./test/perceptual-tests/ && exit 1)'

The result is something like which presents me with a long list of wget commands to download my diffs and screenshots for local browsing.

Download via:
    mkdir -p tmp/travis/104/{actual,diff}_screenshots
    wget -O "tmp/travis/104/actual_screenshots/%2F500.png" ""
    wget -O "tmp/travis/104/actual_screenshots/%2F2013-07-24-abandoned-project%3A-kaleidoscope.png" ""

Here is an example set of images:

Example set of downloaded images

With the set of images, I can see the discrapencies and aim to resolve them. In my case, it is fonts that give me the most trouble. To remedy this, I use a Vagrantfile that mimics Travis CI as close as possible.

Travis CI runs an Ubuntu 12.04 LTS image. More info can be found here:

Vagrant.configure("2") do |config| = "precise64"
  config.vm.box_url = ""

  # Install test dependencies
  # Then, use `vagrant ssh` to run your screenshot commands

Then, I rely on Vagrant to generate a local set of expected screenshots and deliver them to Travis CI.

If the stars align, then the expected images will match and your tests will pass. passing.png

Top articles

Lessons of a startup engineer

Lessons from being a 3x first engineer, former Uber engineer, and working at even more startups

Develop faster

Removing the tedium from creating, developing, and publishing repos.

Sexy bash prompt

A bash prompt with colors, git statuses, and git branches.