7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

NIJIBOXAdvent Calendar 2018

Day 23

Puppeteerでレガシーを乗り切る

Last updated at Posted at 2018-12-22

まずPuppeteerとは

Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

devToolsのプロトコルを使い(ヘッドレス)Chromeを動かすためのnode.jsのライブラリ、とのことですね。
これを使うことで任意のページのスクリーンショットを撮ったり、パフォーマンスの計測をしたりが自動化できる、というわけです。

色々できるPuppeteerですが、今回は「スクリーンショットを撮る」機能にフォーカスを当てます。

レガシーを乗り切る?

フロントエンド界隈では開発環境を日々いい感じにするムーブが強めなわけですが、はて全てのwebサイトが全てwebpackで構成されているのか、テストを考慮した設計になっているのか、というとそうでもないですよね。
様々な事情があり、いわゆる「レガシー」な環境で作業する機会はまだまだ存在する、というのが実情ではないでしょうか?
例えばそう、よく似た30枚の静的なhtmlを直さねばならない、とかね、、、

よく似た30枚の静的なhtmlを直す

※以下はイメージです。

①正規表現で対象の文言を置換したりする
③30枚を頑張って確認
③「できました!」
④「すまんがここのビジュアル差し替えお願いします」
⑤「はい。。。」30枚を頑張って(略)
⑥「ここのリンク文言変更になったので」
⑦「はい。。。。。。」30枚を(ry

何が起こるか?

※以下はイメージです。

  • 目へのダメージ
  • 集中力へのダメージ
  • 上記から来る確認漏れ
  • 確認漏れから来る修正対応
  • 修正対応から来る精神へのダメージ...

辛いですね。

(半)自動化しましょう

こんな感じで。
mypage.png
https://6z2zmkp69w.codesandbox.io/
https://k094vklyk7.codesandbox.io/
の画面の差分が赤く表示されています。

ファイル構成

※Puppeteerだけでなく、差分画像の生成用にpixelmatchも併せてインストールします。

screen_diff
├── node_modules
│   └── 略
├── package-lock.json
├── package.json
├── screenshot
│   ├── diff
│   │   └── mypage.png
│   ├── reference
│   │   └── mypage.png
│   └── test
│       └── mypage.png
├── screenshot.js
└── test
    ├── reference.json
    └── test.json
package.json
{
  "name": "screen_diff",
  "version": "1.0.0",
  "license": "MIT",
  "scripts": {
    "start": "node screenshot"
  },
  "dependencies": {
    "pingjs": "^0.9.1", // 差分画像の生成に使用
    "pixelmatch": "^4.0.2", // 差分画像の生成に使用
    "puppeteer": "^1.9.0"
  }
}
screenshot.js
const puppeteer = require('puppeteer');
const pixelmatch = require('pixelmatch');
const PNG = require('pngjs').PNG;
const fs = require('fs');
const TESTS_JSON = './test/test.json';
const REFERENCE_JSON = './test/reference.json';
const SCREENSHOT_FILE_PATH = './screenshot/';
const tests = JSON.parse(fs.readFileSync(TESTS_JSON, 'utf8'));
const refs = JSON.parse(fs.readFileSync(REFERENCE_JSON, 'utf8'));
const PC_SIZE = {
    width: 1200,
    height: 800
};

const action = {
    'hover': async (options) => {
        const page = options.page;

        await page.hover(options.trigger);
    }
};

const compareScreenshots = (test) => {
    const defaultCompare = doneReading({
        img1: fs.createReadStream(`screenshot/test/${test.pageName}.png`).pipe(new PNG()).on('parsed', () => {defaultCompare.next()}),
        img2: fs.createReadStream(`screenshot/reference/${test.pageName}.png`).pipe(new PNG()).on('parsed', () => {defaultCompare.next()}),
        filename: test.pageName
    });

    if (test.pattern) {
        for (let pattern of test.pattern) {
            const patternCompare = doneReading({
                img1: fs.createReadStream(`screenshot/test/${test.pageName}_${pattern.type}.png`).pipe(new PNG()).on('parsed', () => {patternCompare.next()}),
                img2: fs.createReadStream(`screenshot/reference/${test.pageName}_${pattern.type}.png`).pipe(new PNG()).on('parsed', () => {patternCompare.next()}),
                filename: `${test.pageName}_${pattern.type}`
            });
        }
    }
}

function* doneReading(options) {
    const img1 = options.img1;
    const img2 = options.img2;
    let readed = 0;
    yield readed++;

    const diff = new PNG({
        width: img1.width,
        height: img1.height
    });

    pixelmatch(
        img1.data,
        img2.data,
        diff.data,
        img1.width,
        img1.height,
        {
            threshold: 0.01
        }
    );
    console.log('diff');
    return diff.pack().pipe(fs.createWriteStream(`screenshot/diff/${options.filename}.png`));
}

const takeScreenshot = async (options) => {
    const page = options.page;
    const test = options.test;

    await page.goto(options.url);
    await page.screenshot({
        path: options.dist + test.pageName + '.png',
        fullPage: true
    })
}

(async () => {
    const browser = await puppeteer.launch({
        headless: true,
        ignoreHTTPSErrors: true,
    });
    const page = await browser.newPage();

    page.setViewport(PC_SIZE)

    for (let key in tests) {
        await takeScreenshot({
            page: page,
            url: refs[key].url,
            test: tests[key],
            dist: SCREENSHOT_FILE_PATH + 'reference/',
        });
        await takeScreenshot({
            page: page,
            url: tests[key].url,
            test: tests[key],
            dist: SCREENSHOT_FILE_PATH + 'test/',
        });

        // 画像差分を出す
        compareScreenshots(tests[key]);

        console.log("save screenshot: " + key)
    }
    await browser.close()
})();

使い方

何は無くとも

$ npm i

比較したいurlをjsonに追加

test.json
{
    "mypage": {
        "pageName": "mypage",
        "url": "https://k094vklyk7.codesandbox.io/"
    },
    "mypage2": {
        "pageName": "mypage2",
        "url": "[テスト対象のurl]"
    }
}
reference.json
{
    "mypage": {
        "pageName": "mypage",
        "url": "https://6z2zmkp69w.codesandbox.io/"
    },
    "mypage2": {
        "pageName": "mypage2",
        "url": "[比較元のurl]"
    }
}

実行

$ npm run start

> screen_diff@1.0.0 start
> node screenshot

save screenshot: mypage
diff
save screenshot: mypage2
diff

差分の確認

screenshot/diff/

に差分画像が出ているので確認。
お疲れ様でした。

まとめ

削られた精神でも見逃さないよう、良き感じにdiffってくださるPuppeteerさんは天使。

諸注意

リファクタリングであれば、「差分出てない、OK!」(リグレッションテスト)となりますが、
今回のような使用用途では、差分が出ること前提の運用になっています。
見やすいように表示はされますが、最終的にその差分が「いい差分かどうか」を判断するのは人の目になりますので、油断なきようにしましょう。

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?