This article is also available on medium.
Take a better picture with puppeteer.
Puppeteer is awesome. It allows me to capture screenshots of web a lot easier.
But sometimes I can't get intended result because of animations within pages.
For example, I'll write these kind of code when I'm trying to capture SFMOMA.
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://www.sfmoma.org/", {
waitUntil: "networkidle0",
});
await page.screenshot({ path: "example.png" });
This will results in this image.
This capture is taken before animation have finished.
Expected result is this.
How should I avoid this?
Plan 1: Sleep fixed time.
Most easiest way is just sleep specific time. This is useful when I know duration of animations.
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
await page.goto("https://www.sfmoma.org/", {
waitUntil: "networkidle0",
});
sleep(1000);
await page.screenshot({ path: "example.png" });
But usually I have to specify longer time than actual animation for safe. This will problematic when I'm using puppeteer on CI. I don't want to waste CI's build time.
Plan 2: Speed up!!
Another way is to speed up CSS animations. I can fast forward them by using DevTools Protocol.
await page._client.send('Animation.setPlaybackRate', { playbackRate: 2 });
Virtually fast forward to take a screenshot after animations #453
Plan 3: Find the page movement
Finally I'll find page movement by capturing multiple screenshots.
I use blink-diff to find differences between images.
Then I wrote this small function.
import * as BlinkDiff from "blink-diff";
import { Page } from "puppeteer";
export async function waitTillPageStoped(
page: Page,
interval: number = 200,
timeout: number = 3000,
fullPage: boolean = true,
): Promise<boolean> {
const t0 = new Date().getTime();
let previousBuffer: Buffer;
while (new Date().getTime() - t0 < timeout) {
await sleep(interval);
const currentBuffer: Buffer = Buffer.from(await page.screenshot({
encoding: "base64",
fullPage,
}), "base64");
if (previousBuffer == null) {
previousBuffer = currentBuffer;
continue;
}
const diff = new BlinkDiff({ imageA: previousBuffer, imageB: currentBuffer });
const result = await diff.runWithPromise();
if (result.differences === 0) {
return true;
}
previousBuffer = currentBuffer;
}
throw new Error("Timeouted!!");
}
This captures a screenshot in each interval specified till timed out. If no pixel differences found, simply stop waiting.
I can use this function like this.
await page.goto("https://www.sfmoma.org/", {
waitUntil: "networkidle0",
});
await waitTillPageStoped(page);
await page.screenshot({ path: "example.png" });
And I've got an expected result!!
Top comments (1)
I like this idea a lot of taking screenshots until nothing changed. Is there a package out there that does this?