JavaScript
Node.js
npm
Nightwatch
Resemble.js

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でなかったら差分があるので対象の差分画像ファイルを確認してみましょう

比較ファイル1 比較ファイル2 比較結果    不一致率(%) 差分画像ファイル
screenshots/test_20181102130429/fileName0.png   screenshots/test_20181102130408/fileName0.png   1   3.29    output/report_20181102130524/diff/0.png

差分画像ファイル

差分画像は以下みたいな感じになっています。
差分のある部分が黄色く表示されていますね
qiitaのトップページとかだと広告部分が変わるので差分としてでてるのがわかります
0.png