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.
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!";
});