node.jsでサブディレクトリまで一度に作成してくれるmkdirpが便利

node.jsでディレクトリを再帰的に作成してくれるモジュールです。
"mkdir -p"コマンドを実行するのと同じです。

インストールは簡単。

npm install mkdirp

ディレクトリを作成するサンプルです。

const mkdirp = require('mkdirp');

mkdirp('./foo/bar/baz', err => {
    if (err) console.error(err)
    else console.log('success!')
});

地味に便利です。
github.com

jsでスクリーンショットを比較してデザインデグレの自動検出をする

E2Eテストの自動化にトライしてみました。
今回はUIの細かい挙動のテストではなく、デザインのデグレ検出にフォーカスをしています。

Webデザインにおいて1pxへのこだわりは非常に重要です。大規模なサービスになると、1pxの違いで数億円の売上に影響することも珍しくありません。
www.nikkei.com


複数人のチームで開発を進めていて、意図せずデザインのデグレが発生していまうことがあります。この時、1px単位のごくわずかな差分の場合、肉眼では見落としてしまいがちです。しかしながら1pxの差が事業に影響を与える可能性があるので、デザインのデグレは防ぐ必要があります。

今回はUIの差分を自動検知する簡易ツールを作成してみます。
nightwatch.jsとresemble.jsを利用します。
Nightwatch.js | Node.js powered End-to-End testing framework
Resemble.js : Image analysis


実装に入る前に、どれほどの精度で差分が検出できるのかお見せします。

nightwatch.jsの公式サイトでサンプルを用意してみました。
(今回はサンプルのため意図的に変更しています。)
beforeとafterで5箇所の差分が発生していますが、わかりますか?

# before

f:id:tky_bpp:20160712170619p:plain

# after

f:id:tky_bpp:20160712170609p:plain

いかがでしょうか?
ここで、beforeとafterの画像をresemble.jsを使って比較をしてみます。
そうすると、2つの画像の差分をこのように検出することができるのです。

# answer

f:id:tky_bpp:20160712170554p:plain

5つの差分は

  1. 左上のロゴの位置が2pxずれた
  2. 右上のHomeリンクからアンダーラインが消えた
  3. 画面中央の「Write End-to-End〜」の文章がボールドになった
  4. Downloadボタンの幅が狭くなった
  5. 画面下部の「Nightwatch.js is an easy〜」の文章のフォントカラーが変わった

でした。


細かい。


こんな細かい差分なら放っておいてもいいじゃん、という人もいます。
しかしながら、インターネットの世界では1pxの差分が数億円を左右することに繋がるのです。
とはいえ、こんな細かいところまで差分が無いか人力で確認しようとすればとても労力が必要になります。
肉眼だと気付きにくい差分でも自動化してしまえば一瞬ですね。

実装

2つの要素から構成しています。
nighwatch.js → 画面のスクリーンショットを保存する
resemble → スクリーンショットを比較して差分を検出する

スクリーンショットの保存はnighwatch.jsを利用すれば簡単にできます。
対象のURLを指定して、saveScreenshotを実行するだけです。

'Sample test' : function (browser) {
    browser
      .url('http://nightwatchjs.org/')
      .saveScreenshot('example.png')
      .end();
}

これを日次やコミット毎など定期的に実行するようにすれば便利です。

そして、画像ファイルの比較はresembleを利用してこのように書きます。

const fs = require('fs');
const resemble = require('node-resemble-js');

const file1 = Buffer(fs.readFileSync('file_path_to_before.png'));
const file2 = Buffer(fs.readFileSync('file_path_to_after.png'));

resemble.outputSettings({ transparency: 0.1 });
resemble(file1).compareTo(file2).onComplete( data => {
    if(data.misMatchPercentage >= 0.01) {
        data.getDiffImage().pack().pipe(fs.createWriteStream('file_path_to_diff.png'));
    }
});

比較結果はonComplete()の中で扱うことが出来ます。
差分の割合がmisMatchPercentageとして格納されているので、一定の割合で差分があった場合のみ処理をすることも簡単に出来ます。
また、差分の画像データもgetDiffImage()を利用して取得できるので、外部ファイルとして保存することが出来ます。

Node.jsを使って静的コンテンツを表示するHTTP/HTTPSサーバーを用意する

connectモジュールを使えば、簡単にHTTPサーバーを作成できます。
connectはNode.js 向けの拡張可能な HTTP サーバフレームワークです。ミドルウェアと呼ばれるプラグイン機構をつかって HTTP サーバを拡張することが出来ます。
これを包括する形でルーティングなどを簡単にしてくれるのがexpressですね。


カレントディレクトリをルートディレクトリとするサンプルを作ってみます。
ローカルで簡単に動作確認などテストを実施したい時に便利です。

const http = require('http');
const connect = require('connect');
const logger = require('morgan');
const serveStatic = require('serve-static');

const app = connect()
  .use(logger('dev'))
  .use(serveStatic(process.cwd()))

http.createServer(app).listen(3000);

serveStaticはローカルに存在するファイルを返すミドルウェアです。
connectのv2.xでは、staticプロパティとしてconnectに組み込まれていましたが、v3.0から分離されています。
同じくloggerもmorganと名前を変えて分離されています。

これだけで完了です。

続いてHTTPSサーバーを用意してみます。

まずはSSLに必要な鍵を生成します。

$ openssl genrsa -out localhost.key 2048
$ openssl req -new -x509 -key localhost.key -out localhost.cert -days 3650 -subj /CN=localhost

適当にapp配下にkeysディレクトリを作成し、生成されたkeyとcertを移動します。
あとはこの生成した2つのファイルを読み込んでオプションとして渡してあげるだけでHTTPS対応が完了します。

const https = require('https');
const fs = require('fs');
const connect = require('connect');
const logger = require('morgan');
const serveStatic = require('serve-static');

const options = {
  cert: fs.readFileSync('./keys/localhost.cert'),
  key: fs.readFileSync('./keys/localhost.key')
};

const app = connect()
  .use(logger('dev'))
  .use(serveStatic(process.cwd()))

https.createServer(options, app).listen(3000);

確認してみましょう

$ node server.js
$ curl 'https://localhost:3000/app/' -vk     
*   Trying localhost...
* Connected to localhost (localhost) port 3000 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: localhost
> GET /app/ HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Cache-Control: public, max-age=0
< Last-Modified: Thu, 07 Apr 2016 14:39:04 GMT
< ETag: W/"62e-153f1298740"
< Content-Type: text/html; charset=UTF-8
< Content-Length: 1582
< Date: Fri, 06 May 2016 16:17:05 GMT
< Connection: keep-alive
<
<!doctype html>
<html>
<head>
.
.

ステータスコードは200となっており、HTMLファイルの中身も表示されていることが確認できます。
もちろんブラウザで https://localhost:3000/app/ にアクセスしても表示されます。

存在しないファイルにアクセスするときちんと404を返してくれます

$ curl 'https://localhost:3000/NotFound/' -vk 
*   Trying localhost...
* Connected to localhost (localhost) port 3000 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: localhost
> GET /not_found/ HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< X-Content-Type-Options: nosniff
< Content-Type: text/html; charset=utf-8
< Content-Length: 23
< Date: Fri, 06 May 2016 16:19:35 GMT
< Connection: keep-alive
<
Cannot GET /NotFound/

Node.jsからgitコマンドを実行してコミットログなどを取得する

前回はnode.jsからシェルコマンドを実行する方法を紹介しました。
tkybpp.hatenablog.com

これを利用することで、gitのログなども取得できるようになります。
git logコマンドを実行して最新のコミットIDと日付を取得してみます。

const execSync = require('child_process').execSync;
const cmd = 'git log -n 1 --format=%H,%cd';
const result = execSync(cmd).toString().split(',');
const commitID = result[0];
const commitDate = new Date(result[1]);

console.log(commitID);
console.log(commitDate);
$ node test.js
e12e8a92d3469ac3d9cfeeg0cbxb2c2b3aa7d441
Sun Apr 24 2016 09:34:58 GMT+0100 (CET)

今回は最新の1つのみを取得しましたが、git logのオプションは他にもあります。
formatも自由に記述できるので、使いこなせると便利かもしれません。
Git - git-log Documentation


エンジニアのためのGitの教科書 実践で使える!バージョン管理とチーム開発手法

エンジニアのためのGitの教科書 実践で使える!バージョン管理とチーム開発手法

Node.jsからシェルコマンドを実行する

node.jsからシェルコマンドを実行するにはexecを使えば簡単にできます。

const exec = require('child_process').exec;
exec('ls -la ./', (err, stdout, stderr) => {
  if (err) { console.log(err); }
  console.log(stdout);
});

exec関数は非同期関数であり、callbackに渡されるのは、err、標準出力文字列、標準エラー出力文字列です。
また、同期的に処理を実行したい場合にはexecSyncが利用できます。

const execSync = require('child_process').execSync;
const result =  execSync('ls -la ./');
console.log(result);

execSyncの返り値はBufferなので注意しましょう。

$ node test.js   
<Buffer 74 6f 74 61 6c 20 38 30 0a  ... >

出力結果を処理したい場合にはtoString()などを利用しましょう。

const result =  execSync('ls -la ./').toString();
$ node test.js                                                                                                      
total 10
drwxr-xr-x   17 user  test    578  4 24 17:17 .
drwxr-xr-x   14 user  test    476  4 17 05:02 ..
-rw-r--r--@   1 user  test   6148  3 27 10:49 file1
-rw-r--r--    1 user  test    188  1 21 12:55 file2
.
.


詳しい仕様は公式ドキュメントを参照ください。
Child Process Node.js v6.1.0 Manual & Documentation


サーバサイドJavaScript Node.js入門 (アスキー書籍)

サーバサイドJavaScript Node.js入門 (アスキー書籍)

実践Node.js プログラミング (Programmer's SELECTION)

実践Node.js プログラミング (Programmer's SELECTION)