経緯
- BackstopJS を試す (Error: Failed to launch the browser process! エラー発生→ puppeteer のバージョンを指定して解決) – Ewig Leere(Lab2)
- BackstopJS で背景画像の高さを `vh` 単位で指定したページで画像やスクリーンショットが引き伸ばされる現象についてメモ (未解決) – Ewig Leere(Lab2)
BackstopJS が上手く行かなかったので Cypress を使う方法に切り替えたところ、わりと上手く行きそうだったのでこちらの方法に切り替えてみました。
成果物
スクリプトのベースは
こちらを参考に、自分で使いやすいようにさらにいくつかチョイ足しをした感じになります。
構成
ディレクトリ構造
PROJECT_ROOT/
├ cypress/
│ ├ fixtures/
│ │ └ visual-regression-test/
│ │ └ config.json.sample // 設定サンプル
│ │
│ ├ integration/
│ │ └ visual-regression-test/
│ │ ├ cafe_bij_nacht-before.spec.js // ref のキャプチャを取得するスクリプト
│ │ └ cafeterras_bij_nacht-after.spec.js // test のキャプチャを取得するスクリプト
│ │
│ ├ plugins/
│ │ └ index.js // イベントにフックさせて動作させるスクリプト
│ │
│ └ support/ // 略
│
├ package.json
├ sterrennacht.js // Cypress + reg-cli のチェック後に reg-cli のレポートのHTMLを開く
└ zonnebloemen.js // Cypress 実行時の設定パラメータを調整する
package.json
{
"name": "cypres_bij_sterrennacht",
"version": "0.0.1",
"description": "Cypress + reg-cli によるビジュアルリグレッションテストのスクリプト。",
"main": "sterrennacht.js",
"scripts": {
"result": "node ./sterrennacht",
"cypress:before": "cypress run --config-file false --config trashAssetsBeforeRuns=true -s cypress/integration/visual-regression-test/cafe_bij_nacht-before.spec.js",
"cypress:after": "cypress run --config-file false --config trashAssetsBeforeRuns=false -s cypress/integration/visual-regression-test/cafeterras_bij_nacht-after.spec.js",
"cypress": "run-s cypress:*",
"reg": "reg-cli ./cypress/screenshots/visual-regression-test/before ./cypress/screenshots/visual-regression-test/after ./cypress/screenshots/visual-regression-test/diff -R ./cypress/screenshots/visual-regression-test/report/index.html -M 0.2 -I",
"test": "run-s cypress:* reg result"
},
"author": "Arm Band",
"license": "ISC",
"dependencies": {
"browser-sync": "^2.26.14",
"cypress": "^7.6.0",
"npm-run-all": "^4.1.5",
"opener": "^1.5.2",
"reg-cli": "^0.17.0"
}
}
npm scripts にタスクを記述、 yarn test
で以下の4つのタスクを一括で動かします。
- Cypress による変更前サイトのキャプチャの撮影
- Cypress による変更後サイトのキャプチャの撮影
reg-cli
で1., 2.の比較とレポート生成- レポートのHTMLを
openner
で開く
zonnebloemen.js
/**
* zonnebloemen : 設定を生成する
*/
class zonnebloemen
{
/**
* getParameters : 設定用のフラグを生成
*
* @param {booelan} flag : フラグ
*
* @return {Object} : 設定パラメータの内容
*/
getParameters (flag)
{
return flag === 'BEFORE' ? {
outStr: 'before',
urlKey: 'ref',
flag : true,
} : {
outStr: 'after',
urlKey: 'test',
flag : false,
};
};
/**
* getConfig : 設定を生成
*
* @param {Object} data : JSONファイル のデータ
* @param {booelan} flag : フラグ
*
* @return {Object} : 設定の内容
*/
getConfig (data, flag)
{
if (data !== undefined && data !== null && Object.keys(data).length > 0) {
flag ? data.commons['trashAssetsBeforeRuns'] = true : data.commons['trashAssetsBeforeRuns'] = false;
}
return data;
};
};
module.exports = zonnebloemen;
フラグによって変更前なのか変更後なのか、スクリプトを実行する際の設定の配列のパラメータを弄るクラス。
具体的には以下の2つ。
- 変更前
- 参照先URL:
ref
キー にセットされた値 trashAssetsBeforeRuns
(キャプチャ取得スクリプト実行前にキャプチャ格納ディレクトリをクリーンアップするか):true
- 参照先URL:
- 変更後
- 参照先URL:
test
キー にセットされた値 trashAssetsBeforeRuns
:false
- 参照先URL:
config.json.sample
{
"commons": {
"url": {
"ref": "https://www.example.jp",
"test": "https://demo.example.jp"
},
"hideElements": [
"#__bs_notify__",
".returnPageTop",
".c-returnPageTop",
"#navbar",
".navbar"
],
"screenshot": {
"capture": "fullPage",
"waitMsec": 1000
}
},
"viewports": [
{
"name": "phone",
"width": 375,
"height": 667
},
{
"name": "tablet",
"width": 1024,
"height": 768
},
{
"name": "pc",
"width": 1920,
"height": 1080
}
],
"pages": [
{
"test_id": "top",
"uri": "/"
},
{
"test_id": "about",
"uri": "/about.html"
}
]
}
サンプルですがやりたいことは最低限押さえてあるかと。
commons
:url
:ref
: お手本として参照するサイトのURLtest
: テストしたいサイトのURL
viewports
: デバイス(ブラウザ)の幅と高さ。デフォルトではスマートフォン、タブレット、PCとして3つのサイズを指定pages
: テストしたいページのURLとキャプチャのファイル名に使用するID
基本的にこの設定に従って Cypress でキャプチャを撮っていきます。
cafe_bij_nacht-before.spec.js
const flag = 'BEFORE';
const configDataOrigin = require('../../fixtures/visual-regression-test/config.json');
const zonnebloemenClass = require('../../../zonnebloemen');
const zonnebloemen = new zonnebloemenClass();
const parameters = zonnebloemen.getParameters(flag);
const configData = zonnebloemen.getConfig(configDataOrigin, parameters.flag);
describe(`${parameters.outStr} screenshot`, function () {
const pages = configData.pages;
const domain = configData.commons.url[parameters.urlKey];
const viewports = configData.viewports;
const ss_dir = parameters.outStr;
var test_id = '';
var ss_path = '';
var url = '';
pages.forEach(({ test_id, uri }) => {
viewports.forEach(({ name, width, height }) => {
context(test_id, () => {
beforeEach(() => {
url = domain + uri;
cy.visit(url);
});
it('take screenshot', function () {
ss_path = ss_dir + '/' + test_id + '-' + name;
cy.wait(configData.commons.screenshot.waitMsec);
cy.viewport(width, height);
cy.scrollTo('bottom');
cy.screenshot(ss_path, {
onBeforeScreenshot($el) {
for (const selector of configData.commons.hideElements) {
const $selector = $el.find(selector);
if ($selector) {
$selector.hide();
}
}
},
capture: Cypress.env(configData.commons.screenshot.capture),
});
});
});
});
});
});
最初の定数 flag
以外は cafe_bij_nacht-before.spec.js
と cafeterras_bij_nacht-after.spec.js
で共通となります。本当は1つのファイルにしたかったのですが上手く動かなかったので2つにしました。
やっていることは flag
によって zonnebloemen.js
で設定パラメータの配列を生成、その値を以てキャプチャを撮影する、というもの。
cafeterras_bij_nacht-after.spec.js
const flag = 'AFTER';
const configDataOrigin = require('../../fixtures/visual-regression-test/config.json');
const zonnebloemenClass = require('../../../zonnebloemen');
const zonnebloemen = new zonnebloemenClass();
const parameters = zonnebloemen.getParameters(flag);
const configData = zonnebloemen.getConfig(configDataOrigin, parameters.flag);
describe(`${parameters.outStr} screenshot`, function () {
const pages = configData.pages;
const domain = configData.commons.url[parameters.urlKey];
const viewports = configData.viewports;
const ss_dir = parameters.outStr;
var test_id = '';
var ss_path = '';
var url = '';
pages.forEach(({ test_id, uri }) => {
viewports.forEach(({ name, width, height }) => {
context(test_id, () => {
beforeEach(() => {
url = domain + uri;
cy.visit(url);
});
it('take screenshot', function () {
ss_path = ss_dir + '/' + test_id + '-' + name;
cy.wait(configData.commons.screenshot.waitMsec);
cy.viewport(width, height);
cy.scrollTo('bottom');
cy.screenshot(ss_path, {
onBeforeScreenshot($el) {
for (const selector of configData.commons.hideElements) {
const $selector = $el.find(selector);
if ($selector) {
$selector.hide();
}
}
},
capture: Cypress.env(configData.commons.screenshot.capture),
});
});
});
});
});
});
上述で説明した通りなので省略。
plugins/index.js
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
const path = require('path');
const fs = require('fs');
const isExistFile = (file) => {
try {
fs.statSync(file);
return true;
}
catch (err) {
if (err.code === 'ENOENT') {
return false;
}
}
};
module.exports = (on, config) => {
on('after:screenshot', (details) => {
const str = details.path;
const newPath = str.replace(/[\w\d_]+\-(before|after)\.spec\.js/, '');
const newDir = path.dirname(newPath);
fs.mkdirSync(newDir, { recursive: true });
fs.renameSync(details.path, newPath);
return { path: newPath };
});
on('after:spec', (details) => {
const newPath = details.absolute.replace('integration', 'screenshots');
newPath.match(/[\w\d_]+\-(before|after)\.spec\.js/);
const childNewPath = path.join(newPath, RegExp.$1);
if (isExistFile(childNewPath)) {
fs.rmdirSync(childNewPath);
}
if (isExistFile(newPath)) {
fs.rmdirSync(newPath);
}
});
};
通常だとキャプチャの保存先のディレクトリ名がキャプチャ撮影スクリプトのファイル名そのものになってしまい、ディレクトリ名にドットが入るなどややもやっとする形式なので、そこを before
or after
のみになるように保存先ディレクトリのリネーム (ファイルコピー+オリジナルディレクトリ削除) を実行するようにしています。
sterrennacht.js
const opener = require('opener');
const dir = {
baseDir: './cypress/screenshots/visual-regression-test/',
startPath: 'report/index.html',
};
opener(`${dir.baseDir}${dir.startPath}`);
opener
で開くだけの簡単なスクリプト。
実行結果(サンプル)
試しに少し動かしてみます。サンプルはホーム | Kiribi Ususamaで、公開されているものを ref
、ローカルで Browsersync で開いたものを test
として動作させてみます。
reg-cli
のレポートのトップ。キャプチャのうちテストを通らなかったものが上部に、通ったものは下部に分類されて並べられています。
テストが通った例。トップページです。
テストNGの例。なんだかディレクトリ構造のツリー構造がずれているようです。左右にバーをスライドさせて比較を行うことができる他、差分を赤色で表示させるモードや、 ref
のみの表示、 test
のみの表示、と切り替えることもできます。ここも使いやすそうだとおもった点です。
これで、自分なりにビジュアルリグレッションテストができる環境が整備できた気がします。
余談
もはや恒例となりましたが、名前について。今回は「Cypres bij Sterrennacht」。オランダ語です。日本語訳すると「糸杉と星の見える道」。ゴッホの最晩年の作品ですね。名前は Cypress をツールとして使用しているから、というシンプルな理由です。
ちなみに自前の補助スクリプトの名前も
cafe_bij_nacht-before.spec.js
: Cafe bij nacht →「夜のカフェ」cafeterras_bij_nacht-after.spec.js
: Cafeterras bij nacht →「夜のカフェテラス」sterrennacht.js
: Sterrennacht →「星月夜」zonnebloemen.js
: Zonnebloemen →「ひまわり」
でいずれもゴッホの作品の名前から採りました。
参考
Cypress
きっかけ
- Cypress入門~初心者でも簡単にE2Eテストが作れる~ | フューチャー技術ブログ
- Cypress – 書きやすいテストの秘密と独自コマンドの実装 | フューチャー技術ブログ
- cypressを触ってみた – Qiita
ベース
Docs
前回実施時のスクリーンショットの破棄 (trashAssetsBeforeRuns)
- How to delete all screenshots before a test run while doing a cypress visual regression – Stack Overflow
- 【Cypress】Screenshotの設定 – UGA Boxxx
スクロール
要素を隠す
.hide()
find の戻り値
pluginsスクリプト のイベント
config
未使用
- Cypressのheadlessモードでlogを表示させる – Qiita
- Ability to add a message to stdout during cypress run ・ Issue #186 ・ cypress-io/cypress ・ GitHub
- e2e testing – Cypress pipe console.log and command log to output – Stack Overflow
- Cypress.config | Cypress Documentation