Puppeteerでレガシーを乗り切る

# まずPuppeteerとは
https://github.com/GoogleChrome/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://qiita-image-store.s3.amazonaws.com/0/218423/76326de0-d48a-c65d-278d-e412f769f9c1.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
```
```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"
  }
}
```
```js: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に追加
```json:test.json
{
    "mypage": {
        "pageName": "mypage",
        "url": "https://k094vklyk7.codesandbox.io/"
    },
    "mypage2": {
        "pageName": "mypage2",
        "url": "[テスト対象のurl]"
    }
}
```
```json: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!」（リグレッションテスト）となりますが、
今回のような使用用途では、差分が出ること前提の運用になっています。
見やすいように表示はされますが、最終的にその差分が「いい差分かどうか」を判断するのは人の目になりますので、油断なきようにしましょう。