Quickstrom
新しいプロジェクトの仕様とテストを何で書こうかなと色々探していたら辿り着いたのですが, ブラウザテストでPBTができるというのが面白そうだったので試してみました。
「ボタンをランダムに数百回押した時に意図しない挙動にならないだろうか」などをテストできます。
仕様自体はPureScriptで記述しますが, TLA+に強い影響を受けているようです。
公式のチュートリアル( https://docs.quickstrom.io/tutorials/first.html )があり,とてもわかりやすいので真面目にやるひとはそちらをお勧めしますが,今回はチュートリアルとは違うケースを考えてやってみます。
前提
こんな感じの複数選択可能なアイテムを作ろうと思います。
また,何も選択されていない場合は警告メッセージを表示します。
HTML
まずはレイアウトです。このようなHTMLを用意しました。
<!doctype html>
<html>
<head>
<style>
#warn { /* 略 */ }
.choice { /* 略 */ }
.selected { background-color: #ffcc33; }
</style>
</head>
<body>
<div class="choice">A</div>
<div class="choice">B</div>
<div class="choice">C</div>
<div class="choice">D</div>
<div id="warn"></div>
<script>
/* ここに選択の処理を書いていく */
</script>
</body>
</html>
Quickstromで仕様を書く
モジュールの宣言とインポート
module Choice where
import Quickstrom
import Data.Tuple (Tuple(..))
import Data.Maybe (Maybe(..), maybe)
import Data.Array (length)
テストを開始するタイミングを宣言する
readyWhen にセレクターを書いておくと,その対象がreadyになったタイミングでテストが開始されます。
今回は .choice
をクリックしたいので次のようにします。
readyWhen :: Selector
readyWhen = ".choice"
Quickstromが行うActionを宣言する
ActionsはQuickstromがブラウザに対して行ってくれる操作たちです。
デフォルトで clicks という Actions が提供されていますが, これはDOMの button やinput[type=submit] などが対象で div はクリックされないので自前で定義してみます。
actions :: Actions
actions = [ Tuple 1 (Click ".choice") ]
Tupleの最初の要素に指定している1
はそのアクションが全体に占める比率です。
複数のアクションを指定する際には 2:1:1 のようにどのアクションを多めにするかなどを指定できます。
テストでチェックするデータを定義しておく
selected :: Array String
selected = map _.textContent (queryAll ".choice.selected" { textContent } )
warning :: Maybe String
warning = map _.textContent (queryOne "#warn" {textContent} )
複数の該当するDOMのプロパティをリストで返す queryAll とユニークなDOMのプロパティをMaybeで返すqueryOne があります。
また,取得できるプロパティについては textContent, value, disabled, checked が提供されています。
(記事を書いた時点では, property が外部に公開されていないのでそれ以外を自分で定義するのは現時点ではできなさそう)
今回は選択されたアイテムのリストを取得する selected
と, 選択されていない状態で表示する警告メッセージを取得する warning
を定義しています。
UIが満たすべき制約を書く
ここがメインです。
proposition 関数に画面が満たすべき制約を記述します。
コメントを入れていますが, 現状日本語が入っているとエラーになるようですので, 実際にはコメントを削除しています。
proposition :: Boolean
proposition =
let
-- 初期状態。"A"が選択されていて, 且つ警告メッセージは表示されていない
initial =
selected == ["A"]
&& warning == Just ""
-- 選択可能なアイテムは最大4つである
-- 選択されているアイテムが4つの場合, 次に変化があれば選択されているアイテムは3つとなっている
-- 選択されているアイテムがない場合, 次に変化があれば選択されているアイテムが1つとなっている
maximumSelection =
length selected <= 4
&& implies (length selected == 4) (length (next selected) == 3)
&& implies (length selected == 0) (length (next selected) == 1)
-- <div id="warn"></div>自体が画面から消えることはない
-- 選択されているアイテムがない場合は, warnのメッセージは空文字ではない
-- 選択されているアイテムがある場合は, warnのメッセージは空文字である
warningShown =
warning /= Nothing
&& implies (selected == []) (warning /= Just "")
&& implies (selected /= []) (warning == Just "")
in
initial
&& always (maximumSelection && warningShown)
一度テストをしてみる
まだ実装していないけれども, ここで一度テストをします。
この記事を書いている時点では以下のような感じでセットアップしていますが, この時点ではドキュメントがちょいちょい更新されているので公式サイトに倣ってください。
% docker network create quickstrom
% docker run --rm -d \
--network quickstrom \
--name webdriver \
-v /dev/shm:/dev/shm \
-v $PWD:/spec \
selenium/standalone-chrome:3.141.59-20200826
docker run --rm \
--network quickstrom \
-v $PWD:/spec \
quickstrom/quickstrom \
quickstrom check \
--webdriver-host=webdriver \
--webdriver-path=/wd/hub \
--browser=chrome \
--tests=5 \
/spec/Choice.spec.purs \
/spec/index.html
エラーが出ました。
Quickstromは大量にイベントを発火させてエラーケースを探し出すのですが, 見つけた後にshrinkingで簡約してくれます。
このエラーはshrinkingするまでも無いのですが, "#warnのtextContentが空文字で且つselectedが空な状態になっている" ことを教えてくれているようです。
propositionのinitial
が満たされていないということですね。
修正してみる
初期状態では"A"が選択されているべきでしたので, index.htmlを修正します。
<div class="choices">
<div class="choice selected">A</div>
<div class="choice">B</div>
<div class="choice">C</div>
<div class="choice">D</div>
<div id="warn"></div>
</div>
もう一度テストをしてみると...
テストを通りました!
initial
以外については, そもそも現時点ではイベントハンドラを作っていないので初期状態以外はテストできていません。
JavaScriptで実装する
<script>
document.querySelectorAll('.choice').forEach( (c) => {
c.addEventListener('click', () => {
c.classList.toggle('selected');
});
});
</script>
こんな感じかな。
テストをしてみましょう。
(ここではうっかり何も選択されてない時に警告メッセージを表示することを忘れています)
初期状態から, click .choice[0]("A"をクリック) すると何も選択していないのに, #warnのtextContentが空文字 になっている と表示されています。
うっかり警告メッセージの実装を忘れていたので, そのことを怒られたみたいですね。
JavaScriptを修正する
<script>
document.querySelectorAll('.choice').forEach((c) => {
c.addEventListener('click', () => {
c.classList.toggle('selected');
// 何も選択されていないと警告メッセージを表示
const selected = document.querySelectorAll('.choice.selected').length;
const warn = document.getElementById('warn');
if(selected == 0){
warn.innerText = "一つ以上選んでください。";
} else {
warn.innerText = "";
}
});
});
</script>
成功です。
所感
Quickstromはfirst commitが今年の4月でまだまだ新しく, 2020/09/13時点では認証に対応していなかったりと自分が今関わっているプロジェクトでE2Eテストとして導入するのは難しそうです。
ただ, 複雑なコンポーネントなんかが改修によってデグレを起こしていないかをチェックするなんていう用途には今からでも使えそうなので, StrobyBook的なカタログに組み込んでいけないかなどを試してみようと思います。