Visual Regression Testing で予期せぬレイアウト崩れを検知する
Atomic Designなどに倣ってコンポーネントを作成している場合、
あるOrganisms(Atoms, Moleculesでも可)のコンポーネントの構造やスタイルを修正した際、参照しているTemplatesのレイアウトも変化します。
修正したコンポーネントが複数のTemplatesから参照されている場合、
「あるTemplatesのレイアウトの変更は想定したものだったが、それ以外のTemplatesのレイアウトが変わることを予期していなかった」 ということも考えられるでしょう。
このような場合、レビューの段階でも意図していない箇所の変更に気づきにくいため、QAで発見されるということになることがあります。(最悪リリース後に発見される👀)
そこで、Visual Regression Testing による予期していないレイアウト変更を検知する仕組みについて説明していきます。後半では、reg-suit
を用いたテストの導入をしていきます。
Visual Regression Testing とは
そもそもの Visual Regression Testing(以降、VRTと記述する)はどのようなテストかについてですが、ブラウザにレンダリングした際のスクリーンショットをピクセル単位で差分を検出するテストです。
正解画像(Expected Image)と実際の画像(Actual Image)を比較し、
差分が閾値以下ならSuccess、しきい値を超えたらFailedになります。
Expected Image と Actual Image(reg-suitからの引用)
VRTでは差分をどれだけ許容するかの閾値が重要になります。
適切な値は各々によるかもしれませんが、僕は以下の記事を参考にしました。
差分閾値は 0.1% (
core.thresholdRate = 0.001
)
私たちのチームでは 0.1%、つまり 32x32 のメッシュの 1 箇所以上で差分があれば検出するようにしています。カタログの規模的に false positive を目視でチェックしきれるので、なるべく false negative を出さないであろう値にしています。今後の運用で見直すかもしれません。
VRTの仕組みを作る
ここからはStorybookのVRTの仕組みづくりについて説明していきます(Storybookのセットアップは省いてます)
この記事では Cypress, reg-suit を用いました。
- Storybook: コンポーネントカタログを作成する
- Cypress: Headlessブラウザを起動してStorybookのスナップショットを取得する
- reg-suit: キャプチャした画像と正解画像の比較、GithubやSlackへの通知、GCSとの連携
Storybookのスナップショット取得にCypressを使用しましたが、
reg-viz が提供している Storycap を用いても実現できます。Storycapは各Storiesをクロールしてくれるため、手軽にスナップショットを取得できるそうなので、試してみたいです👀
今回Cypressを採用したのは、
- Storycapでは実現できない?ボタンのfocus時のキャプチャを取得したいなどの発展したことができるようにする
- Storybookとスナップショット取得に関するコードを分離させる
Storycapの設定を*.stories.tsx
に追加した際のコード
最終的にはこのような構成になりました。
Cypress
Headlessブラウザを起動してStorybookのスナップショットを取得するためのCypressのセットアップから説明します。
yarn add cypress --dev
yarn run cypress open
起動できること確認したら閉じて、cypress/integration/examples
ディレクトリ以下は不要なので削除します。
次に、Storybookサーバが起動してからスナップショットを取得する必要があるので、start-server-and-test
をインストールします。
yarn add -D start-server-and-test
スナップショット取得の処理を記述します。
CI上で動かすことを考えたときの適切なcy.wait(3000)
の値を探る必要はありそうです。
/// <reference types="cypress" />
context('Storybook', () => {
it('logged in', () => {
cy.visit('http://localhost:6006/?path=/story/example-page--logged-in');
cy.wait(3000);
cy.get('#storybook-preview-wrapper').screenshot('login');
});
});
以下のコマンドをnpm-scriptsに記述した上で実行すると、
cypress/screenshots
ディレクトリ以下にスナップショットが出力されます。
start-server-and-test 'yarn storybook' http-get://localhost:6006 'yarn cypress:run'
取得したスナップショットです。
focusを当てた状態のスナップショット取得
少し余談になりますが、StorycapではなくCypressを採用した理由として
特定の要素にfocusが当たった状態のスナップショットを取得すると前述しました。
Cypressのブログに方法は示されているので、focusを当てた状態のキャプチャを試してみました。
/// <reference types="cypress" />
const getIframeDocument = () => {
return cy
.get('iframe[data-is-storybook="true"]')
.its('0.contentDocument').should('exist')
}
const getIframeBody = () => {
return getIframeDocument()
.its('body').should('not.be.undefined')
.then(cy.wrap)
}
context('Storybook', () => {
it('logged in', () => {
cy.visit('http://localhost:6006/?path=/story/example-page--logged-in');
cy.wait(3000);
getIframeBody().find('#logout').focus();
cy.get('#storybook-preview-wrapper').screenshot('login-focus');
});
});
きちんとfocusが当たってることを確認できました。
(Cypressでhover
が上手くできなかったのが気になる🤔)
reg-suit
次に、キャプチャした画像と正解画像の比較、GithubやSlackへの通知、GCSとの連携などを行うreg-suitについて説明します。
npm i -g reg-suit
reg-suit init
最初はローカル環境でVRTを実行するために、plugins周りは省いてます。
{
"core": {
"workingDir": ".reg",
"actualDir": "cypress/screenshots",
"thresholdRate": 0.001,
"ximgdiff": {
"invocationType": "client"
}
}
}
-
.reg/expected
ディレクトリにcypress/screenshots
ディレクトリをコピーする - コンポーネントに変更加える
- Cypressを実行する
reg-suit run
-
http-server .reg
でローカルサーバ立ち上げる
pluginsの設定
再び reg-suit init
を実行し、必要なpluginを追加します。
この記事では reg-notify-slack-plugin
, reg-publish-gcs-plugin
, reg-notify-github-plugin
, reg-keygen-git-hash-plugin
を追加しました。
reg-notify-github-plugin
のcliendId
に関してはplugin追加時にGithub Appの追加と認証を要求されると思います。
{
"core": {},
"plugins": {
"reg-keygen-git-hash-plugin": {},
"reg-notify-github-plugin": {
"prComment": true,
"prCommentBehavior": "default",
"clientId": "need to setup"
},
"reg-notify-slack-plugin": {
"webhookUrl": "need to setup"
},
"reg-publish-gcs-plugin": {
"bucketName": "need to setup"
}
}
}
CIの設定に関してはreg-suitに注意点が示されているので参考になります。
実際にコンポーネントに修正を加えた上でPRを作成してみましたが、
これでPRのコメントとしてレポートをしてくれるのでレビュワーにとって優しい状態を作れますね!
コンポーネントが増えるにつれて、Storybookの起動にかかる時間だけでなくCypressのスナップショット取得も時間がかかるのが懸念に感じてますが、
Atomic DesignにおけるTemplatesのみをビルド&キャプチャすることなども検討すると良さそうです。
Discussion