backstopjs_boilerplate
v0.11.1
Published
boilerplate for backstopjs
Downloads
41
Readme
Boilerplate for backstopjs
:rabbit: Getting started
パッケージインストール
npm i -D backstopjs backstopjs_boilerplate
package.json 編集
"scripts": {
"init": "backstop init && bsbl init",
"sync": "bsbl sync",
"watch": "bsbl watch",
"test": "bsbl test && backstop test",
"reference": "bsbl reference && backstop reference"
},
初期化
npm run init
テンプレート生成
手動による生成
npm run sync
または chokidar を利用したファイル監視による自動生成
npm run watch
テスト実行
npm run reference
and npm run test
目次
- :cl: コマンド一覧
- :wrench: javascript による backstopjs との統合
- :hammer: 動機
- :scream: 問題点
- :sunglasses: 拡張した機能
:cl: コマンド一覧
package.json内からであれば bsbl
として呼び出すことができる。
通常のコマンドラインでは node_modules/backstopjs_boilerplate/boilerplate/cli.js
に対してサブコマンドを渡す必要がある。
init
bsbl init [path]
事前にbackstop init
が実行済みでなければならない。
[path]
には backstop.json へのパスを指定する。省略した場合、./backstop.json として扱われる。
backstop_data/ 内に boilerplate のためのディレクトリと設定ファイル(boilerplate.json)を生成し、onBefore, onReady の動作を書き換える。書き換えについては「engine_scripts のモジュール化」を参照。
sync
bsbl sync [path]
事前にbsbl init
が実行済みでなければならない。
[path]
には backstop.json へのパスを指定する。省略した場合、./backstop.json として扱われる。
設定ファイル(boilerplate.json)内のシナリオ定義を利用して backstop_data/boilerplate/ 内のディレクトリとファイルを同期する。設定ファイル内にシナリオの定義が存在していて、その定義が示すディレクトリやファイルが存在しない場合はそれらを生成する。既に存在しているファイルに対しては何も行わない。設定ファイル内のシナリオ定義に一致しないディレクトリやファイルが存在する場合はそれらを削除する。基本的にディレクトリとファイルの生成・削除は手動で行わず、設定ファイルの編集と bsbl sync
コマンドで行う。
watch
bsbl watch [path]
事前にbsbl init
が実行済みでなければならない。
[path]
には backstop.json へのパスを指定する。省略した場合、./backstop.json として扱われる。
chokidar を利用して backstop_data/boilerplate/ 内の json ファイルを監視し、変更があった際に bsbl sync
を暗黙的に呼び出すラッパーコマンド。
test, reference
bsbl test [path]
bsbl reference [path]
backstop test
backstop reference
を実行する前に実行しなければならない。
[path]
には backstop.json へのパスを指定する。省略した場合、./backstop.json として扱われる。
backstop_data/boilerplate/ 内のシナリオを全てマージして backstop.json の scenarios キーにアサインする。各シナリオのラベル命名規則は以下の通りとなっている。
{endpoint}:{scenario}:{viewport}
:wrench: javascript による backstopjs との統合
backstopjs と boilerplate の連携を javascript で行いたい場合は、init
コマンドで生成される integration_example.js を参照。
const backstop = require("backstopjs");
const boilerplate = require("backstopjs_boilerplate");
const cmd = process.argv[2];
const cnf = process.argv[3] || require("path").join(process.cwd(), "backstop.json");
switch (cmd) {
case "init":
backstop(cmd).then(() => boilerplate(cmd, cnf));
break;
case "sync":
boilerplate(cmd, cnf);
break;
case "test":
case "reference":
boilerplate(cmd, cnf).then(() => backstop(cmd, { config: require(cnf) }));
break;
}
:hammer: 動機
backstopjsは非常にシンプルなインターフェースで簡単にビジュアルレグレッションテストを導入できるライブラリだったので自分が仕事で担当しているサイトに使ってみた。しかしシンプル過ぎて大量のページや複雑さのあるシナリオが必要なケースでは規則やヘルパーが無さ過ぎて辛い。ので色々と痒い所に手を届かせる規則と機能を追加した。
:scream: 問題点
シナリオを作るのがめんどくさい
ランディングページの様な数ページ程度の規模のサイトであれば手動でも良いが、二桁超えた辺りでシナリオの動的生成がしたくなってくる。
シナリオの再利用性が考慮されていない
問い合わせフォームの確認・完了のような何かの操作の結果で表示できる状態があったとして、その操作のパターンが途中で分岐しそれぞれに対してテストを実施したい場合、jsonを複製するような原始的な方法しか用意されていない。これは長期的に見ると全体を把握することが困難になる気がしてならないし複製した箇所を仕様変更などで書き換えなければならなくなった時に死ねる。
シナリオの見通しが悪い
backstopjs では定義されたビューポート毎にスクリーンショットを撮ってくれるがそれ以上のことはできない。もしビューポート毎に異なる操作が必要であれば別のシナリオを作らなければならない。ビューポート毎にサイト上で表示されるものや実行される処理が違う事は往々にしてあるので前提として個別にシナリオを書けないと辛いと感じた。しかし単純にシナリオを増やしていく形をとると シナリオを作るのがめんどくさい と感じる原因になる。その上 再利用性が考慮されていない ので保守が難くなる。
engine_scripts の呼び出しもシナリオから制御したい
backstopjs 自体が提供するヘッドレスブラウザに対する操作は非常に少なく恐らく作ってる側もビジュアルレグレッションでカバーするテストはこのくらいの機能で実現できるものであるべき。というようなスタンスな気がする。しかし実際に使ってみるとWEBベースの report(画像比較)が秀逸で凄い安心感なので網羅的にテストしたくなってくる。そしていざ独自セレクタと関数による拡張をしてみるとそこには何の規則もないのでコードとシナリオの関係性が把握し難く保守が難しいと感じた。
:sunglasses: 拡張した機能
1. シナリオの自動生成
全てのエンドポイントに対するテンプレートなシナリオを設定ファイルとコマンドで自動生成する。
init
コマンドを実行すると、backstop_data/ 以下にディレクトリとファイルを生成する。
./backstop_data
/boilerplate
boilerplate.json
template_endpoint.json
template_subscenario.json
{engine}_scripts.js
/_common
common.json
1-1. 設定ファイルを編集する
boilerplate.json
{
"test": "http://your.test.site.com",
"reference": "http://your.reference.site.com",
"endpoints": {
"index": [
"some_scenario_a",
"some_scenario_b"
],
"/some_endpoint": [
"some_scenario_c"
],
"/some_endpoint/some_nested_endpoint": [
"some_scenario_d"
],
"/some_endpoint/#some_link": [
"some_scenario_e"
],
"/empty_endpoint": []
},
"skip": {
"when": "reference",
"endpoints": {
}
}
}
単純にスクリーンショットを取るだけの場合は設定ファイルの test, reference に URL、endpoints に適当なエンドポイントと空の配列を記述するだけでテストが行える状態になる。
test と reference
backstopjs における同パラメータと同義。シナリオ毎に異なるドメインを有するシナリオは生成する際は個々のシナリオで url を書き換える。
endpoints
key にはスクリーンショットを実行する対象パスを、value にはその対象パスに対するシナリオを配列で記述する。
index は特殊な値で /
に対する定義と解釈される。
同一ページに対して backstopjs が提供する機能(selectors) では対応できないマルチスクリーンショットを実行したい場合に個別にシナリオ定義を追記して実現する。またページ内リンクやネストしたパスを持つエンドポイント(例では /some_endpoint/some_nested_endpoint や /some_endpoint/#some_link)はその全部を一つのエンドポイントとして扱う。
空配列(例では /empty_endpoint)として定義するとtest
コマンドや reference
コマンドで出力されるシナリオにのみ含まれ、テンプレートファイル自体は生成されない。
skip
when には test
または reference
を指定する。指定された方でシナリオの生成が行われた場合 skip 内の endpoints とマッチするシナリオを除外する。boilerplate における test
, reference
の処理の違いはこの点のみなっている。backstopjs の filter がホワイトリストなのに対して、このパラメータはブラックリストの役割をはたしている。
空配列として定義するとそのエンドポイントに含まれる全てのシナリオ出力がスキップされる
template_endpoint.json
エンドポイントを追加した際に初期定義として使用されるテンプレート。
{
"$scripts": [],
"$subscenarios": [],
"selectors": [],
"removeSelectors": [],
"keyPressSelectors": [],
"hoverSelectors": [],
"clickSelectors": [],
"hideSelectors": []
}
template_subscenario.json
サブシナリオを追加した際に初期定義として利用されるテンプレート。詳細については「3-2. 同一エンドポイント内での再利用(サブシナリオ)」を参照。
{
"$scripts": [],
"selectors": [],
"removeSelectors": [],
"keyPressSelectors": [],
"hoverSelectors": [],
"clickSelectors": [],
"hideSelectors": []
}
{engine}_scripts.js
boilerplate における、スクリプトのエントリーポイント。詳細については「4-1. 拡張操作の実装」を参照。
module.exports = preimplements => ({
...preimplements,
// add more handlers here...
});
1-2. テンプレートの生成
sync
コマンドを実行するとその時点の boilerplate.json の内容に従って backstop_data/boilerplate/ 下に各シナリオが生成される。
例 「設定ファイルを編集する」の定義による生成結果
./backstop_data
/boilerplate
boilerplate.json
{engine}_scripts.js
/_common
common.json
/index
some_scenario_a.json
some_scenario_b.json
/some_endpoint
some_scenario_c.json
/some_endpoint_some_nested_endpoint
some_scenario_d.json
/some_endpoint_-somelink
some_scenario_e.json
2. 同一シナリオファイル内でビューポート毎の記述を可能にする
当初、何も考えずに各ビューポート毎にシナリオを分けて作ってみたものの全体を把握するのが容易ではないと感じたので、同一シナリオファイル内にビューポート毎の記述ができるような構成にした。ビューポート毎の差異程度であれば寧ろ同じファイルに記述されていた方が見通しが良い。生成直後の backstop.json の viewports には tablet, phone という値が設定されているので、何も手を加えなかった場合に生成されるシナリオは以下の形になる。 all ブロックに記述した操作はそのシナリオの全ビューポートに対して適用される。
{
"all": {
something...
},
"tablet": {
something...
},
"phone": {
something...
}
}
3. 再利用可能なシナリオ
シナリオの再利用は大きく三ヵ所で行える。
3-1. シナリオファイル内での再利用
「2. 同一シナリオファイル内でビューポート毎の記述を可能にする」 の all ブロック。(ビューポート間共有)
3-2. 同一エンドポイント内での再利用(サブシナリオ)
同一エンドポイント内には複数のシナリオを定義できるが、時として各シナリオで共通操作が存在しそれらを一か所で定義したい場合がある。そのような場合に利用する。(シナリオ間共有)
便宜的にこの共通操作を記述した json ファイルを サブシナリオ を命名している。
シナリオ内の $subscenarios 配列に任意のサブシナリオ名を追加して bsbl sync
コマンドを実行するとその名前を持つファイルがディレクトリ内に生成される。
サブシナリオの中で再帰的に $subscenarios を定義することはできない。
./backstop_data/boilerplate/index/scenario_a.json
./backstop_data/boilerplate/index/scenario_b.json
{
"all": {
"$subscenarios": [
+ "sheared_scenario"
],
something ...
},
"tablet": {
"$subscenarios": [
+ "sheared_scenario_only_tablet"
],
something ...
},
"phone": {
"$subscenarios": [
+ "sheared_scenario_only_phone"
],
something ...
}
}
bsbl sync
コマンド実行後
./backstop_data
/boilerplate
/index
some_scenario_a.json
some_scenario_b.json
+ sheared_scenario.json //some_scenario_a と some_scenario_b の全ビューポートで共通操作
+ sheared_scenario_only_tablet.json //some_scenario_a と some_scenario_b の tablet のみの共通操作
+ sheared_scenario_only_phone.json //some_scenario_a と some_scenario_b の phone のみの共通操作
3-3. サイト全体を通した再利用
init
コマンドで生成される common.js 。ここに記述した操作は全てのシナリオに反映される。
グローバルスコープに対する設定は backstop.json でも行えるのでどちらを利用するかの判断は任意としている。
3-4. シナリオの適用順序
一つのシナリオを出力する際にいくつものシナリオ(json)が関わってくるため、読み込まれる順序を理解する必要がある。
- 最も優先度が高いのは boilerplate.json に定義されているシナリオ。
- 次に boilerplate.json に定義されているシナリオのサブシナリオ。
- 次が common.json
- 最後に common.js の中で定義されているサブシナリオ。
$subscenarios 配列は先頭から積み上げていくので配列に複数のシナリオが含まれる場合、前方より後方が優先される。
3-5. カスタムプレフィックス
再利用時に記述内容に重複があった場合、具体的にどの様にデータに作用するのか。そしてどの様な操作オプションがあるのか。
シナリオを上書きしたいと思った場合、数値や文字列などのプリミティブ型であれば代入以外の選択肢は無いが、配列には二通りの上書きが存在する。一つはプリミティブ同様に単純な上書き、もう一つは既存の配列に対するマージ。
他方、特定のシナリオを実行する場合にのみ、再利用しているシナリオの一部操作を除外したいという特殊なケースもある。boilerplate ではカスタムプレフィックスを利用してそれらの意図を柔軟に制御できるようになっている。
| prefix | description | |:----:|----| | +: | 対象配列にマージする。配列内で重複が見つかった場合、後方の値を破棄する | | -: | 対象配列に含まれる同じ値を取り除く | | =: | 配列全体を入れ替える |
オブジェクトやプリミティブな値を保持するキーにカスタムプレフィックスを指定しても意味はない。
配列を保持するキーにカスタムプレフィックスの指定が無い場合、 +:
として扱われる。
3-6. カスタムプレフィックスの適用順序
同一シナリオファイル内に異なるカスタムプレフィックスを持つ同名のキーが存在する場合、処理優先度は-:
< +:
< =:
となる。(つまり =:
が定義されている場合は、-:
や +:
は無視されるし、-:
と +:
が定義されている場合は、-:
が先に処理される。)異なるシナリオ間でカスタムプレフィクスを持つ同名のキーが存在する場合、「シナリオの適用順序」 に従って出力内容が決まる。
例
上書き先
{
"clickSelectors": [
".someelement-1",
".someelement-2",
".someelement-3"
],
"hoverSelectors": [
".someelement-4"
],
}
上書き内容
{
"-:clickSelectors": [
".someelement-1",
".someelement-3"
],
"clickSelectors": [
".someelement-2",
".someelement-4"
],
"=:hoverSelectors": [
".overwrite"
]
}
上書き結果
{
"clickSelectors": [
".someelement-2",
".someelement-4"
],
"hoverSelectors": [
".overwrite"
]
}
4. engine_scripts のモジュール化
既存の backstopjs でも onBefore, onReady を活用したユーザー定義の engine_script に対するモジュール化は実現されているが、そこには明確なルールがないためそれらの呼び出しに関する設計と実装に時間を取られてしまう。boilerplate では規則を設けることでその負担を減らし、ヘッドレスブラウザに対するコーディングに集中できるようにしている。実装したコードはシナリオからプラガブルに編集できる。
4-1. 拡張操作の実装
boilerplate における engine_scripts のエントリーポイントは onBefore, onReady ではなく、init
コマンドで生成された {engine}_scripts.js となる。 {engine} はテストに使用しているエンジンの名前となる。(デフォルトではpuppeteer)
onBefore, onReady は init
コマンド実行時にシナリオから制御するためのフック関数へ書き換えられる。
※その他のエンジンについての切り分けも考えて作っているが、自分自身が puppeteer 以外のエンジンを利用していないので他のエンジンは現状ではサポートしていない。
{engine}_scripts.js を見ると分かるが、このモジュール関数は preimplements 引数が渡されている。このオブジェクトの中には backstopjs が提供する clickAndHoverHelper , loadCookies , overrideCSS の実装が含まれる。(ignoreCSP , interceptImages については追加のパッケージが必要なので含めていない) もしこれらの実行タイミングを制御したいのであればこのオブジェクトから取り出して任意のタイミングで呼び出すことができる。
再利用を意識した単位で記述すれば1ファイルでも十分だと思うが、気になる場合は preimplements と同様に意味ある塊ごとに外部ファイル化し、後からこのファイルのモジュールに組み込む。
例
実装例
./backstop_data/boilerplate/puppeteer_scripts.js
module.exports = preimplements => ({
...preimplements,
// bodyに設定されているheightスタイルをautoに書き換える
overwriteBodyHeight: async function(page) {
await page.addStyleTag({ content: `body { height: auto !important; }` });
},
// 指定秒数待機する
wait: async function(page, scenario, vp, isReference, engine, config, ms) {
await page.waitFor(ms);
},
});
4-2. 拡張操作の呼び出し
シナリオ内から拡張操作を呼び出すには、 $scripts 配列に関数名を記述する。呼び出しタイミング(onBefore, onReady)を制御するためにカスタムプレフィックスを利用する。また関数名の後に :
で区切った値を加えるとその値を引数として関数へ渡すことができる。(実装例では wait 関数の ms 引数がそれを利用している)
実行順序に関しては、before プレフィックスが付いているものは、ready プレフィックスが付いているものよりも必ず先に呼ばれる。同じプレフィックス同士の優先順位は配列への追加順となる。
| prefix | description | |:----:|----| | before: | 指定した関数をonBefore時に呼び出す | | ready: | 指定した関数をonReady時に呼び出す |
例
シナリオ
./backstop_data/boilerplate/index/scenario_a.json
{
"all": {
"$subscenarios": [
"sheared_scenario"
],
"$scripts": [
+ "before:wait:1000",
],
},
"tablet": {
"$subscenarios": [
"sheared_scenario",
"sheared_scenario_only_tablet"
],
"$scripts": [
+ "ready:overwriteBodyHeight"
]
},
"phone": {
"$subscenarios": [
"sheared_scenario",
"sheared_scenario_only_phone"
]
}
}