静的サイトにpuppeteerとresemblejsでスクリーンショットテスト環境を作る

June 22, 2019

静的サイトを作っていて結構思うことなのですが、こういうこと↓が結構あります。

  • 「CSS変更したら全然違うページが崩れちゃった」
  • 「コンポーネント変更したら別のページでも使われているの知らなくて崩れちゃった」

これらを回避するために、デプロイする前に全ページ確認する作業をやらなくてはいけません。これはかなり面倒です。サイト全体のスクショを開発前と開発後で差分を抽出して、意図しない見た目の変更をキャッチできるようにすれば確認する手間が減ります。

そんな手間を省くためにサイト全体のスクリーンショットの差分を取得できる環境を作ってみました。

流れとしてはこんな感じ

  • 現在のサイトのスクリーンショットを保存する
  • 開発中のスクリーンショットと比較する
  • 差分があれば差分画像を保存する

pupeteerのインストール

npm i -D puppeteer

GitHub - GoogleChrome/puppeteer: Headless Chrome Node API

pupeteerをtypescriptで使う

npm i -D @types/puppeteer

Using Puppeteer in TypeScript |

TypeScriptでnode.jsを動かす

npm install -D ts-node

Node.js QuickStart - TypeScript Deep Dive 日本語版

現在のサイトのスクリーンショットを保存する

puppeteerでスクリーンショットは下記の流れで撮ります。

  • const browser = await puppeteer.launch()でブラウザを立ち上げる
  • const page = await browser.newPage()でページを開く(Chromeで言うタブを開く)
  • await page.goto(url)でページにアクセスする
  • await page.screenshot()でスクリーンショットを保存する
  • browser.close()でブラウザを終了する

今回はスクリーンショットを撮るためだけのこういうクラスを作りました。browser.newPageではなくcontext.newPageを使っています。これはブラウザキャッシュを効かせないようにするためにシークレットモードを使用するためにbrowser.createIncognitoBrowserContext()を使っているからです。

import puppeteer from "puppeteer";

export class ScreenShotSaver {
  private browser: puppeteer.Browser;
  private context: puppeteer.BrowserContext;

  async init() {
    this.browser = await puppeteer.launch({ headless: true });
    this.context = await this.browser.createIncognitoBrowserContext();
  }

  async close() {
    await this.browser.close();
  }

  async saveScreenshot(url: string, dist: string) {
    console.log("start saving screenshot", url);
    const page = await this.context.newPage();
    await page.goto(url, {
      waitUntil: "networkidle2",
      timeout: 60000
    });
    await page.screenshot({ path: dist, fullPage: true });
    console.log("saved screenshot", url, "as", dist);
    return dist;
  }
}

保存する

今回はローカルに起動したサイトのトップページを保存します。./regression-test/screenshots配下に保存されます。

import { ScreenShotSaver } from "./utils/screenshot";

const save = async () => {
  const ssSaver = new ScreenShotSaver();
  await ssSaver.init();
  const ssDir = `./regression-test/screenshots`;
  const url = `http://localhost:8000`;
  const newSs = await ssSaver.saveScreenshot(url, `${ssDir}/index.png`);
  console.log(newSs, "saved");
  ssSaver.close();
};

save();

実行はts-nodeを使います。

./node_modules/.bin/ts-node ./regression-test/save-screenshots.ts

試しにブログトップを保存

このブログのトップを試しに保存してみました。

縦に長いのでここに置いてあります。

画像の差分を取る

resemblejsという画像を比較してくれるライブラリがあるのでそれを使います。

npm i -D resemblejs

画像の差分を取得するスクリプト

差分情報のとり方はresemble(image1).compareTo(image2)CompareResultが非同期で渡ってきます。

比較する画像のパスと差分画像のファイル名を引数に、非同期で差分情報を返す関数を作成しました。

const resemble = require("resemblejs");
import fs from "fs";

interface CompareResult {
  isSameDimensions: boolean;
  dimensionDifference: { width: number; height: number };
  rawMisMatchPercentage: number;
  misMatchPercentage: string;
  diffBounds: { top: number; left: number; bottom: number; right: number };
  analysisTime: number;
  getImageDataUrl: () => any;
  getBuffer: () => any;
}

export const compareImages = (
  path1: string,
  path2: string,
  diffFileName: string
) => {
  const image1 = fs.readFileSync(path2);
  const image2 = fs.readFileSync(path1);

  return new Promise<CompareResult>(res => {
    resemble(image1)
      .compareTo(image2)
      .onComplete((data: CompareResult) => {
        fs.writeFileSync(diffFileName, data.getBuffer());
        res(data);
      });
  });
};

差分を取る

今回はトップページの差分を取ります。差分がなければ新しい画像と差分画像は削除されるようにしました。

import fs from "fs";
import path from "path";
import { ScreenShotSaver, compareImages } from "./utils/screenshot";

const diffScreenshot = async () => {
  let ssSaver: ScreenShotSaver;
  ssSaver = new ScreenShotSaver();
  await ssSaver.init();
  const url = "http://localhost:8000";
  const ssDir = "./regression-test/screenshots";
  const newSs = await ssSaver.saveScreenshot(
    url,
    path.join(ssDir, `index.new.png`)
  );
  const diff = path.join(ssDir, `index.diff.png`);
  const result = await compareImages(
    newSs,
    path.join(ssDir, `index.png`),
    diff
  );
  console.log(
    "result rawMisMatchPercentage",
    "index",
    result.rawMisMatchPercentage
  );
  if (result.rawMisMatchPercentage > 0.1) {
    throw new Error("rawMisMatchPercentage > 0.1");
  }
  fs.unlinkSync(newSs);
  fs.unlinkSync(diff);
  await ssSaver.close();
};

diffScreenshot();

実行はts-nodeを使用します。

./node_modules/.bin/ts-node ./regression-test/diff-screenshots.ts

このブログで試してみた

試しにこの記事のデプロイ前後の差分をとってみました。差分があるところはピンクになります。いい感じにできたのではないでしょうか。CIと組み合わせてプルリクエストごとにテストが走るともっと良いですね。

差分画像

          2019 06 23 5 27 37

ソースコード

ここまでのソースコードはここにあります。

blog/regression-test at c6edd241f810aa61ff771fc366d9aed2ad7f3542 · SatoshiKawabata/blog · GitHub

参考

PuppeteerとResemble.jsを使ったスクショ比較によるピクセル差分テスト - Qiita