Shimonは、Web画面の「変わってほしくない部分」を変更前後で比べるCLIである。
画面全体を画像として比べるのではなく、位置、表示中の文字、CSSの値など、プロジェクトが選んだ情報だけをJSONへ保存する。
たとえば、メニュー内部をリファクタリングするとする。
変更前後でメニューの開閉位置と選択中の項目が同じなら、見た目の振る舞いは保たれていると判断しやすい。
Shimonは、この比較をAIエージェントが繰り返し実行できる形にする。
ただし、比較より先に確かめることがある。
画面を二回測っただけで値が変わるなら、変更前後の差なのか、測定時の揺れなのか区別できない。
そこでShimonは、同じ条件を二回測る selftest を作業の最初に置いている。
画面のどこを比べるか
Shimon自身は、画面のどこが大切かを決めない。
各プロジェクトが shimon.config.mjs に二つの情報を書く。
- 画面状態:最初の画面、メニューを開いた画面、特定位置までスクロールした画面
- 観測値:要素の位置、transform、表示中の文字、計算後のCSSなど
最小の設定は次のようになる。
export default {
target: { url: "http://127.0.0.1:4322/" },
cases: [
{ name: "start" },
{
name: "menu-open",
prepare: (page) =>
page.getByRole("button", { name: "Menu" }).click(),
},
],
probe(page) {
return page.evaluate(() => ({
menuTransform: getComputedStyle(document.querySelector("nav")).transform,
selectedItem: document.querySelector("[aria-current=page]")?.textContent,
}));
},
};
cases は再現する画面状態で、probe は各状態から値を読む処理である。
Shimonはcaseを順番に再現し、probeが返したJSONを保存する。
最初の利用先である cookie-demo では、静止状態と14のスクロール地点を測っている。
Canvasの内容、47個の破片、主要要素の位置とtransform、描画に使うCSSの値を観測対象にした。
スクリーンショットを使わなかった理由
cookie-demo はCanvas 2D、mix-blend-mode、preserve-3dを使っている。
同じコードから撮ったスクリーンショットでも、GPUが描画する箇所の出力が数千バイトずれた。
画像全体を比べると、画面を変えていないリファクタリングでも差分が出た。
そこで、GPU由来の揺れを含みやすい画像全体ではなく、プロジェクトが守りたい値を選んで測ることにした。
Canvasはピクセルをハッシュ化し、DOM要素は位置や計算後のCSSを取得する。
この方法は、スクリーンショット比較より正確という意味ではない。
測る項目を絞るため、プローブが見ていない変化は検出できない。
代わりに、どの値が変わったかをJSONの項目名で特定できる。
selftestが必要になった失敗
前身の検査スクリプトは、結果が成功と表示されても信用できない状態を複数の形で作った。
- GPUの揺れ:同じ画面なのにスクリーンショットの差分が出た
- 古い成果物:二回の取得が失敗したあと、前回のJSON同士を比べて一致と判定した
- 観測操作の副作用:測定前に送った合成イベントが、見つけるはずの不具合を解消した
一つ目は、変化していない画面を「変わった」と報告する失敗である。
残り二つは、正しく測れていない画面を「変わっていない」と報告する失敗である。
Shimonの selftest は、新しいChromiumで画面を測り、もう一度新しいChromiumで同じ測定を行う。
保存済みの成果物は使わず、二つの新しい観測結果をメモリ上で比較する。
npx shimon selftest --json
二回の結果が一致すれば、次のJSONを返す。
{"ok":true,"command":"selftest","changes":[]}
取得時にはCSS animationとtransitionを停止し、フォントの準備を待ってから測定する。
画面固有の待ち方や操作は、プロジェクト側の設定へ書く。
変更前後を比べる手順
selftest が通ったら、変更前と変更後の結果を保存して比べる。
npx shimon selftest --json
npx shimon capture before --json
# 実装を変更する
npx shimon capture after --json
npx shimon diff before after --json
終了コードは、差分と実行失敗を分けている。
0:取得に成功した、または二つの結果が一致した
1:二つの結果に差分がある、または selftest が不安定だった
2:設定、対象URL、ブラウザ実行などに問題があり、測定できなかった
終了コード 1 は、検査が壊れた状態ではない。
比較できたうえで差分を見つけた状態なので、AIエージェントは変わった項目を調べられる。
終了コード 2 なら、画面を評価せず、設定や実行環境を直して測り直す。
selftestでは見つからない死角
selftest が確かめるのは、同じ条件から同じ観測結果が得られることだけである。
空のオブジェクトを二回返すprobeも安定しているため、selftest には通る。
何を測るかが十分かどうかは、別に考える必要がある。
実際、cookie-demo ではケース内側のgradientが壊れたとき、Canvasと要素の位置はすべて一致した。
当時のprobeが背景を測っていなかったためである。
この見逃しのあと、疑似要素を含む計算後の backgroundImage を観測へ加えた。
別の変更では、円盤上の手描きマークを追加したのに、観測対象の配列へ名前を加え忘れた。
現在は、DOM上のマーク数と観測対象数が違えば、測定そのものを失敗させる。
見逃しが起きるたびに、同種の変化を区別できる値をprobeへ加える。
Shimonがプロジェクトから独立したあとも、何を測るかは各リポジトリが持ち続ける。
Shimonが判断しないこと
Shimonが報告するのは、選んだ観測値が同じか、違うかである。
画面が美しいこと、変更後のデザインが妥当であること、必要な項目をすべて測っていることは判断しない。
意図したデザイン変更なら、差分が出るほうが正しい。
人が画面全体を見て変更を評価し、残すと決めた結果を次の基準にする。
現在の selftest が通っても、別のOSやブラウザで同じ値になるとは限らない。
成果物の互換性と実行環境の差をどう扱うかは、バージョン0.0.1ではまだ固定していない。
Design Harnessの記事では、動く実装をAIエージェントへ渡す考え方を書いた。
Shimonは、その実装から「変わってほしくない部分」を選び、変更前後で比べるための道具である。
その比較を信用するために、まず同じ画面を二回測っている。