zudo-test-wisdom
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

レベル5: 決定論的 + 視覚的検証

決定論的な算出スタイルのアサーションと、非公式なスクリーンショットレビューを組み合わせる。

レベル5がテストするもの

レベル5は、UI/CSS検証を2つの異なる手法で組み合わせて行います:

  1. 決定論的な算出スタイルのアサーション -- 実行中のページから正確なCSS値を抽出し、期待値と比較します。LLMの解釈は介在しません。

  2. 視覚的レビューのためのスクリーンショット撮影 -- 算出スタイルだけでは見抜けない構成上の問題(重なり、スタッキングコンテキスト)を発見するための、非公式なレンダリング結果の確認です。

2つのツール:

  • /verify-ui(英語版のスキルドキュメント: /verify-ui) -- verify-styles.mjs を通じて算出スタイルをJSONとして出力する

  • /headless-browser(英語版のスキルドキュメント: /headless-browser) -- headless-check.js を通じてスクリーンショットを撮影する

両方が必要な理由

算出スタイルのチェックだけでは、要素間のインタラクション(重なり、スタッキングコンテキスト)から生じる視覚的な問題を見逃す可能性があります。スクリーンショットだけでは、確証バイアスの可能性がある人間/LLMの解釈に依存します。組み合わせることで以下を提供します:

アプローチ強み弱み
算出スタイルのアサーション決定論的、正確な値視覚的な構成を見られない
スクリーンショットレビュー完全な視覚的結果を確認できるパス/フェイルの信号ではなく解釈に依存する
両方の組み合わせ決定論的な値 + 非公式な視覚的な健全性チェックcanvas/非DOMサーフェスには依然として盲点がある(ブラインドスポット参照)

パート1: 決定論的な算出スタイルのアサーション

対象の要素に対して verify-styles.mjs を実行します。ページに遷移し、そのセレクタの算出スタイルとbounding boxを抽出し、結果をJSONとして出力します -- --check のようなフラグは存在しません。アサーションのステップは、JSONの値を期待値と比較することです。

LOGDIR=$(node $HOME/.claude/scripts/get-logdir.js)
mkdir -p "$LOGDIR"
node $HOME/.claude/skills/verify-ui/scripts/verify-styles.mjs \
  "http://localhost:3000" ".hero-title" "$LOGDIR/verify-ui" "1200" "light"

JSON出力の抜粋:

{
  "styles": {
    "font-size": "48px",
    "color": "rgb(255, 255, 255)",
    "display": "block"
  }
}

各値を要件と比較します:

[PASS] font-size: 48px (expected: 48px)
[PASS] color: rgb(255, 255, 255) (expected: rgb(255, 255, 255))
[PASS] display: block (expected: block)

同じスクリプトは、レベル1/2のテストでは見つけられない表示上の問題も明らかにします -- 詳しくは後述の「このレベルがキャッチする典型的な失敗」を参照してください。

パート2: 視覚的レビューのためのスクリーンショット撮影

Note

これは非公式な視覚的レビューであり、決定論的なピクセル差分チェックではありません。本ガイドの現在のスキルセットにはピクセル差分ツールが存在しません -- スクリーンショットを撮影して読むのは健全性の確認であり、算出スタイルだけでは見抜けない構成上の問題を見つけるのに有用ですが、繰り返し実行できるリグレッションゲートではありません。このスクリーンショット半分の決定論的でCI採点可能なバージョンについては、コミット済みベースラインによる決定的ビジュアルリグレッションを参照してください。

# Full-page screenshot
node $HOME/.claude/skills/headless-browser/scripts/headless-check.js \
  --url http://localhost:3000 --screenshot full

# Viewport-only screenshot
node $HOME/.claude/skills/headless-browser/scripts/headless-check.js \
  --url http://localhost:3000 --screenshot viewport

--width/--height のようなフラグは存在しません。特定のデバイスビューポートを指定するには、代わりにstdinのJSONで渡します:

echo '{"url":"http://localhost:3000","viewport":{"width":375,"height":812},"captureScreenshot":"viewport"}' \
  | node $HOME/.claude/skills/headless-browser/scripts/headless-check.js

一度の実行で複数の幅にわたるレスポンシブチェックを行うには、代わりに verify-styles.mjsWIDTHS 引数を使用してください -- 算出スタイルのダンプと同時に、幅×カラースキームの組み合わせごとにスクリーンショットを撮影します:

node $HOME/.claude/skills/verify-ui/scripts/verify-styles.mjs \
  "http://localhost:3000" ".hero" "$LOGDIR/verify-ui" "375,800,1200" "light,dark"

このレベルがキャッチする典型的な失敗

典型的なシナリオを考えてみます:

  1. AIエージェントが通知バナーコンポーネントを追加する

  2. ユニットテストでコンポーネントがレンダリングされることを確認する(レベル1パス)

  3. DOMテストで要素がツリーに存在することを確認する(レベル2パス)

  4. しかし親コンテナに overflow: hiddenheight: 0 がある

  5. バナーはDOMに存在するが完全に非表示

レベル5ならこれをキャッチします。.notification-container に対して verify-styles.mjs を実行すると、次の結果が返ります:

{
  "styles": {
    "height": "0px",
    "overflow": "hidden"
  }
}

getComputedStyle は常に height をピクセル値に解決します -- auto になることはありません。height: auto ではなく、0px でないこと(またはレイアウトが要求する具体的なピクセル高さと一致すること)をアサートしてください:

[FAIL] height: 0px (expected: not 0px)
[FAIL] overflow: hidden (expected: visible)

headless-check.js のスクリーンショットも、視覚的に同じ結論を示します -- バナーはページ上に表示されていません。

メトリクスを選ぶ前に、視覚的な意図を捉える

パート1のアサーションが信頼できるのは、あなたが測定対象として選んだ値が信頼できる範囲までです。どんな決定論的なチェックもすり抜けてしまう失敗モードが存在します。実際の視覚的要件を表すメトリクスではなく、抽出するのが_都合のよい_メトリクスをアサートしてしまい、それがグリーンで返ってきて、verify-uiがまだ壊れているレイアウトに対してPASSを報告する、というものです。JSONは正確です。ただ、間違った問いに答えているだけなのです。これは決定論の衣をまとった確証バイアスであり、まさに /verify-ui(英語版のスキルドキュメント)が防ぐために存在するものです。ですから、_信号の選び方_を正しくすることがすべてなのです。

アサーションを1つでも書く前に、視覚的な意図を捉えてください。

まずExpectedとForbiddenの状態を名付ける

何かを測定する前に、2つの状態を平易な言葉で書き出します:

  • Expected -- その変更が生み出すべき、目標となる視覚的な関係またはレイアウト。

  • Forbidden -- 以前の壊れた状態、あるいは、より近づいて見えるものの依然として要件を満たさない、心惹かれる部分的な修正。

偽陽性をキャッチするのはForbidden状態です。代理メトリクスは、Forbiddenレイアウトが画面上に完全に残ったままでも、Expectedへと近づいていくことがあります。肯定的な側だけをアサートしていると、何も失敗しません。

DOMより先にスクリーンショットを読む

expected.pngnow.png、あるいは何らかの参照用スクリーンショットが存在する場合は、DOMを開いたり何を選択するかを決めたりする前に、まずそれらを確認してください。それらの差分を平易な言葉で記述し(「AIの理由は予算の_上_に位置するべきなのに、今は横並びになっている」)、それから_その文章から_決定論的なアサーションを導き出します。逆向きに -- 実装上の直感から、あるいはたまたまDOMで問い合わせやすいものから -- アサーションを導き出すことこそが、まさに代理メトリクスが忍び込む経路なのです。

ExpectedだけでなくForbidden状態もアサートする

既知の不正なレイアウトに対しては、ネガティブアサーション、すなわちForbidden状態が_存在しない_ことのチェックを追加します。そのうえで、次のルールを適用してください。

Warning

観測された信号のいずれかが古い壊れた状態と一致する、または期待されるスクリーンショットと矛盾する場合、判定はFAILまたはINCONCLUSIVEであり、PASSには決してなりません。Forbidden状態が存在すれば、判定は強制されます。疑わしい不一致の信号を、グリーンの結果に到達させるために都合よく退けることはできません。

都合のよい代理メトリクスが生む偽陽性

ReadyCrewのトリアージ行では、AIの理由と予算が横並びで表示されていました。修正は、AIの理由を予算の_上に_積み重ねることでした。最初の試みはAIの理由のテキストを広げただけで、横並びのレイアウトはそのまま残ってしまいました -- それにもかかわらず、検証はPASSを報告しました。なぜなら、代理メトリクスをチェックしていたからです:

{
  "reasonSpansTriageWidth": true,
  "reasonConfinedToAiColumn": false,
  "currentIsSideBySide": true
}

最初の2つのフィールドはtrueで、それだけを見ると完了したように_見えます_:理由はいまやトリアージの幅いっぱいに広がり、もはや狭いAIカラムに押し込められていません。しかし currentIsSideBySide: true は、まさにスクリーンショットが修正しようとしていた状態です -- Forbidden状態が、まだ存在しているのです。横並びのレイアウトの中でテキストを広げるのは部分的な改善であって、要求された変更ではありません。正しい判定はFAILであり、代理メトリクスがそれを隠していたのです。

契約(contract)は、Forbiddenレイアウトに対するネガティブチェックを含めて、Expectedの関係を直接アサートすべきでした:

{
  "aiBudgetSideBySide": false,
  "aiAboveBudget": true,
  "sameColumn": true
}

このブロックは_要求される_契約値として読んでください。まだ存在している横並びのレイアウトによって aiBudgetSideBySidetrue と測定され、要求されている false に対して失敗します -- 壊れた状態が、契約をすり抜けるのではなく、契約に引っかかるのです。これは上の「このレベルがキャッチする典型的な失敗」の節で説明したのと同じ罠です:下位の信号がパスする一方で、ユーザーが実際に求めたものはまだ壊れているのです。

メトリクスのダンプではなく、差分による判定で締めくくる

生のJSONを報告して終わり、にしてはいけません。すべての視覚的検証は、数値を、あなたが名付けた2つの状態に結びつける明示的な差分判定で締めくくってください:

Expected layout: AI reason stacked directly above the budget, same column
Observed layout: AI reason widened but still left of the budget (side by side)
Still different: yes -- the side-by-side (Forbidden) layout is still present
Verdict: FAIL

信号がExpected状態を確認することも、Forbidden状態を排除することもできない場合 -- たとえばセレクタが解決しなかった、あるいはスクリーンショットが曖昧な場合 -- は、INCONCLUSIVEを報告してください。PASSを報告するのは、すべてのExpectedアサーションが成り立ち、_かつ_すべてのForbiddenアサーションが存在しないときだけです。

統合されたワークフロー

UI/CSSの変更に対する推奨検証ワークフロー:

  1. CSS/レイアウトの変更を行う

  2. verify-styles.mjs を実行して正確な算出値を抽出する

  3. headless-check.js でスクリーンショットを撮影して非公式に視覚確認する

  4. 算出スタイルはパスするがスクリーンショットが正しくない場合、スタッキング/コンポジションの問題を調査する

  5. スクリーンショットは正しいが算出スタイルの値が失敗する場合、元の要件から期待値を再導出する。期待値を更新するのは要件が変わった場合のみとし、スクリーンショットの印象で失敗チェックを黙らせるために更新してはならない

Note

この2段階のアプローチにより、偽陽性(スクリーンショットは良好だが値が不正)と偽陰性(値は正しいが視覚的なコンポジションが壊れている)の両方を排除します。

ブラインドスポット

  • OSごとのフォントレンダリングの違い

  • サブピクセルレンダリングのバリエーション

  • アニメーションのタイミング(フレーム途中の状態)

  • テストブラウザに存在しないブラウザ固有の癖

  • canvasで描画されたコンテンツ、または安定したDOM bounding rectを持たないサーフェス -- 算出スタイルチェックは <canvas> の内部には適用できず、アンチエイリアスやズームされたコンテンツに対してはスクリーンショット比較もノイズが多すぎます。レベル6を参照してください。

レベル5を使用するタイミング

シナリオレベル5は適切か?
CSSの変更が反映されないはい
DOMに存在するのに要素が表示されないはい
レイアウトの間隔が正しくないはい
色やfont-sizeが不正確はい
レスポンシブブレークポイントの問題はい

Revision History

作成更新