Skip to main content
Big news! Interval has been acquired by Meter. Learn more →

Pixel perfect web screenshot comparison

When deploying sites and apps on the web, you'll often want to test that code updates don't break or unexpectedly change existing designs. In this example, we use Interval to build a tool that takes snapshots of any two webpages (e.g. production vs staging), ensures they look exactly the same, and highlights any differences between them.

Get started with this example
interval.com

Try it out

To create a fresh project based off this example, run:

npx create-interval-app --template=web-screenshot-comparison
The full source code for this tool can also be found in our examples repo.

How it works

This tool will compare two web page screenshots, so to start we'll request two urls using Interval's io.input.text I/O method. We'll nest these calls within an io.group to display the form on a single page along with an io.display.heading to provide context about the tool.

import { Action, io, ctx } from "@interval/sdk";

export default new Action(async () => {
const [_, baseUrl, compUrl] = await io.group([
io.display.heading(
"Select ground truth page and comparison page to compare screenshots"
),
io.input.text("Ground truth URL"),
io.input.text("Comparison URL"),
]);
});

We'll rely on a few libraries to implement most of the logic to implement the screenshot comparison. First we'll pass the urls provided to Puppeteer to load them in a headless browser and take full page snapshots for each. Since this takes a few seconds, we'll also add a loading spinner with context on what's happening.

import { Action, io, ctx } from "@interval/sdk";
import puppeteer from "puppeteer";

export default new Action(async () => {
const [_, baseUrl, compUrl] = await io.group([
io.display.heading(
"Select ground truth page and comparison page to compare screenshots"
),
io.input.text("Ground truth URL"),
io.input.text("Comparison URL"),
]);

ctx.loading.start("Taking screenshots...");

const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.goto(baseUrl);
const baseScreenshot = await page.screenshot({
encoding: "binary",
fullPage: true,
});
const baseBase64 = baseScreenshot.toString("base64");

await page.goto(compUrl);
const compScreenshot = await page.screenshot({
encoding: "binary",
fullPage: true,
});
const compBase64 = compScreenshot.toString("base64");
});

Now that we have the screenshots, we'll use pixelmatch and pngjs to do the detailed comparison and generate a new image showing any differences.

We'll wrap up the tool by returning the results to the user using io.display I/O methods. If the screenshots are identical, we'll return simply render the screenshot by leveraging Interval's io.display.markdown method, which can embed base64 images. If the screenshots are different, we'll render a table to allow viewing the screenshots side by side, with the newly generatated "diff" image in between.

import { Action, io, ctx } from "@interval/sdk";
import puppeteer from "puppeteer";
import { PNG } from "pngjs";
import pixelmatch from "pixelmatch";

export default new Action(async () => {
const [_, baseUrl, compUrl] = await io.group([
io.display.heading(
"Select ground truth page and comparison page to compare screenshots"
),
io.input.text("Ground truth URL"),
io.input.text("Comparison URL"),
]);

ctx.loading.start("Taking screenshots...");

const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.goto(baseUrl);
const baseScreenshot = await page.screenshot({
encoding: "binary",
fullPage: true,
});
const baseBase64 = baseScreenshot.toString("base64");

await page.goto(compUrl);
const compScreenshot = await page.screenshot({
encoding: "binary",
fullPage: true,
});
const compBase64 = compScreenshot.toString("base64");

ctx.loading.update("Comparing screenshots...");

const basePng = PNG.sync.read(baseScreenshot);
const compPng = PNG.sync.read(compScreenshot);
const diff = new PNG({ width: basePng.width, height: basePng.height });

const diffPixels = pixelmatch(
basePng.data,
compPng.data,
diff.data,
basePng.width,
basePng.height,
{ threshold: 0.1 }
);

if (diffPixels > 0) {
const diffBase64 = PNG.sync.write(diff).toString("base64");

await io.display.markdown(
`
## ⚠️ The pages are different

| | | |
| ---- | ---- | ---- |
| ![Screenshot](data:image/png;base64,${baseBase64}) | ![Screenshot](data:image/png;base64,${diffBase64}) | ![Screenshot](data:image/png;base64,${compBase64}) |
`
);
} else {
await io.display.markdown(`
## ✅ These pages are the same

![Screenshot](data:image/png;base64,${baseBase64})
`);
}

return "Done!";
});

API methods used

Did this section clearly explain what you wanted to learn?

548 Market St PMB 31323
San Francisco, CA 94104

© 2023

Join our mailing list

Get 1-2 emails per month with a roundup of product updates, SDK releases, and more.