nightwatch+resemblejsでデザインのデグレチェックをしてみた

#前書き
社内でnightwatchをWinMergeを使用してデグレチェックしたよーって話があったので、
WinMergeの部分も自動化してみました。
ツールを作ったのなら、こっちにも展開してくれよーと思うのだがなー。。。

#やること
- nightwatchでサイトのスクリーンショットを撮影(master/作業ブランチそれぞれで)
- 撮影したスクリーンショットの差異をresemblejs出力する

#事前準備
###Seleniumを使用するのでJDKのインストール
公式からインストールしてください
https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html

```
# バージョンがでればOK
$ java -version
```

###必要なモジュールのインストール

```
$ npm init -y
$ npm install nightwatch
$ npm install webdriver-manager
$ npm install resemblejs

# babelも使用したので以下も入れておく
$ babel-cli
$ npm install babel-register
$ npm install babel-preset-es2015
```
###WebDriverのダウンロード
webdriver-managerを利用してWebDriverダウンロードする

```
node_modules/.bin/webdriver-manager update
node_modules/.bin/webdriver-manager update --chrome
```
ダウンロードされていることを確認
パスは後で使用するのでメモっておく

```
$ find node_modules/webdriver-manager/selenium/ -type f
node_modules/webdriver-manager/selenium/chromedriver_2.43.exe
node_modules/webdriver-manager/selenium/geckodriver-v0.23.0.exe
node_modules/webdriver-manager/selenium/selenium-server-standalone-3.141.0.jar
...
```

#Nightwatch
###nightwatchのconfigファイルを書く
事前にnightwatchに必要なディレクトリを作成しておきます。
configファイルで設定するので場所はどこでもOK!

```
$ mkdir tests
$ mkdir reports
```
公式ページの「Configuration」部分をコピーしてnightwatch.jsonに記述します。
http://nightwatchjs.org/gettingstarted

一部調整が必要なので書き換えてください。
・src_folders,output_folderに作成したディレクトリを設定
・server_pathにseleniumのパスを設定
・webdriver.chrome.driverにはwebdriverのパスを設定

```
$ vi nightwatch.json
```
```nightwatch.json
{
  "src_folders" : ["tests"],
  "output_folder" : "reports",
  "custom_commands_path" : "",
  "custom_assertions_path" : "",
  "page_objects_path" : "",
  "globals_path" : "node_modules/babel-register",

  "selenium" : {
    "start_process" : true,
    "server_path" : "node_modules/webdriver-manager/selenium/selenium-server-standalone-3.141.0.jar",
    "log_path" : "",
    "port" : 4444,
    "cli_args" : {
      "webdriver.chrome.driver" : "node_modules/webdriver-manager/selenium/chromedriver_2.43.exe",
      "webdriver.gecko.driver" : "node_modules/webdriver-manager/selenium/geckodriver-v0.23.0.exe",
    }
  },

  "test_settings" : {
    "default" : {
      "launch_url" : "http://localhost",
      "selenium_port"  : 4444,
      "selenium_host"  : "localhost",
      "silent": true,
      "screenshots" : {
        "enabled" : false,
        "path" : ""
      },
      "desiredCapabilities": {
        "browserName": "chrome",
        "marionette": true
      }
    },

    "chrome" : {
      "desiredCapabilities": {
        "browserName": "chrome",
      }
    }
  }
}
```

###スクリーンショットを撮影するテスト作成

```
# スクリーンショットを出力するディレクトリは作っておいてください。
$ mkdir screenshots

# スクリーンショットを撮影したいURL一覧を作成しておく
# 今回はこのURL一覧のスクリーンショットを撮影するようにします
$ vi urls.txt
https://qiita.com/
...
```
```
$ vi tests/test.js
```
```tests/test.js
require('date-utils'); // 使用する場合はnpm install date-utilが必要
const fs = require('fs');
const url = require('url');

module.exports = {
    '@tags': ['screenshots'],
    // スクリーンショットテストケース
    'screenshots': function (browser) {
        // 出力先ディレクトリ
        const time      = (new Date()).toFormat("YYYYMMDDHH24MISS");
        const outputDir = `screenshots/test_${time}`;

        // URL一覧取得
        const data = fs.readFileSync('urls.txt', "utf8");
        const urls = data.split("\n").filter((item) => {
            return url.parse(item).protocol; // URLチェック(プロトコルがあるのものだけ)
        });

        // スクリーンショットの出力先ディレクトリ作成
        fs.mkdirSync(outputDir);
        // URL一覧のスクリーンショットを撮影
        urls.forEach(function(targetUrl, index) {
            browser
            .url(targetUrl)
            .saveScreenshot(`${outputDir}/fileName${index}.png`);
        });
        browser.end();
    }
};
```

#Resemblejs
###スクリーンショットを比較スクリプト

resemblejsを使用してスクショを比較するスクリプトを書きます
かなり雑な書き方になってしまったのでお許しを。。。

```
$ vi compareScreenshot.js
```
```compareScreenshot.js
require('date-utils');
const fs = require('fs');
const compareImages = require("resemblejs/compareImages");

/* 実行時間 */
const nowtime        = (new Date()).toFormat("YYYYMMDDHH24MISS");
/* スクリーンショットディレクトリ */
const screenshotsDir = 'screenshots';
/* レポートの出力先 */
const outputDir      = `output/report_${nowtime}`;
/* 差分画像出力先 */
const outputDiffDir  = `${outputDir}/diff`;
/* レポートファイル名 */
const reportFile     = `${outputDir}/report.tsv`;
/* レポート項目 */
const reportItem = [
    "比較ファイル1",
    "比較ファイル2",
    "比較結果",
    "不一致率(%)",
    "差分画像ファイル"
];
/* 差分ファイル名用ユニークindex */
let index = 0;

/* 画像の比較 */
async function compareDiff(filePath1, filePath2) {
    const data = await compareImages(
        fs.readFileSync(filePath1), 
        fs.readFileSync(filePath2),
        {
            // 設定値は好きなように調整する
            output: {
                errorColor: { red: 255, green: 255, blue: 0 },
                errorType: "movement",
                transparency: 0.2,
                largeImageThreshold: 1200,
                useCrossOrigin: false,
                outputDiff: true
            },
            scaleToSameSize: true,
            ignore: "antialiasing"
        }
    );
    return data;
}

/* レポートの書き出し */
async function createReport(data, filePath1, filePath2) {
    let report = [];
    report[0] = filePath1;
    report[1] = filePath2;
    report[2] = 0;
    report[3] = 0;
    report[4] = '';

    if (data.misMatchPercentage > 0) {
        report[2] = 1;
        report[3] = data.misMatchPercentage;
        report[4] = `${outputDiffDir}/${index++}.png`;
        fs.writeFileSync(report[4], data.getBuffer());
    }
    fs.appendFileSync(reportFile, report.join("\t") + "\n");
}

/* チェック処理 */
async function check(filePath1, filePath2) {
    const data = await compareDiff(filePath1, filePath2);
    createReport(data, filePath1, filePath2);
}

/* ディレクトリなどの準備 */
function prepare() {
    fs.mkdirSync(outputDir);
    fs.mkdirSync(outputDiffDir);
    fs.writeFileSync(reportFile, "");
    fs.appendFileSync(reportFile, reportItem.join("\t") + "\n");
}

/* メイン処理 */
function main() {
    if (process.argv.length < 4) {
        console.log("引数を2つ指定してください。");
        process.exit();
    }

    const dir1 = `${screenshotsDir}/${process.argv[2]}`;
    const dir2 = `${screenshotsDir}/${process.argv[3]}`;
    if (!fs.existsSync(dir1)) {
        console.log(`指定ディレクトリが存在しません。${dir1}`);
        process.exit();
    }
    if (!fs.existsSync(dir2)) {
        console.log(`指定ディレクトリが存在しません。${dir2}`);
        process.exit();
    }

    const fileList = fs.readdirSync(dir1);
    if (fileList.length == 0) {
        console.log(`ディレクトリにデータが存在しません ${dir1}`);
    }

    prepare();
    fileList.forEach((fileName) => {
        check(`${dir1}/${fileName}`, `${dir2}/${fileName}`);
    });
    console.log(`チェックが完了しました！ => ${outputDir}`);
};
main();
```

#デグレ確認を行う！
####nightwatchを実行し、スクリーンショットを撮影する

デグレを確認したいのでmasterと作業ブランチでそれぞれスクリーンショットを撮影しましょう

```
$ node_modules/.bin/nightwatch --tag screenshots
# => test_YYYYMMDDhhmmssみたいなディレクトリにスクショが出力されます
```

####差分の出力
差分を比較したいディレクトリを指定して実行してください。

```
$ node_modules/.bin/babel-node compareScreenshot.js test_20181102114551 test_20181102114507
チェックが完了しました！ => output/report_20181102130524
```
####出力結果
以下みたいにtsvで差分情報が出力されます。
不一致率が0でなかったら差分があるので対象の差分画像ファイルを確認してみましょう

```output/report_20181102130524
比較ファイル1	比較ファイル2	比較結果	不一致率(%)	差分画像ファイル
screenshots/test_20181102130429/fileName0.png	screenshots/test_20181102130408/fileName0.png	1	3.29	output/report_20181102130524/diff/0.png
```
####差分画像ファイル
差分画像は以下みたいな感じになっています。
差分のある部分が黄色く表示されていますね
qiitaのトップページとかだと広告部分が変わるので差分としてでてるのがわかります
![0.png](https://qiita-image-store.s3.amazonaws.com/0/257468/2327a443-d42a-7d5e-4290-4a0125e9afdd.png)