~/hayashigoto/logs/shimon-selftest

>cat/logs/shimon-selftest.md

title: 比較する前に、比較できることを証明する
published:
topics: ShimonUI TestingCLIAI Agent

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-demoCanvas 2D、mix-blend-modepreserve-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は、その実装から「変わってほしくない部分」を選び、変更前後で比べるための道具である。 その比較を信用するために、まず同じ画面を二回測っている。