visual-qa について
visual-qaは、ローカルSPAを確認するためのヘッドレス視覚テストワークフローです。スクリーンショットのベースライン比較、ページ内のデザイン・アクセシビリティ検査、全ルートの確認を組み合わせ、修正後に再検証します。
主な機能
- 01ルートのスクリーンショットとマニフェストを生成
- 02ピクセル単位でベースラインと比較
- 03ページ内のデザインとアクセシビリティを検査
- 04全ルートの問題を確認
- 05修正後に再撮影して検証
活用シーン
- 01再デザイン後の意図しない視覚変化の検出
- 02アプリの各テーマや状態でのUI変更確認
- 03ブラウザー拡張でlocalhostを撮影できない場合のヘッドレス検査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rheinmir/ setup --skill visual-qaインストール後、Claude Code で/visual-qaを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/.gitignorepackage-lock.jsonpackage.jsonroute-shots.mjs詳細
スキル:visual-qa — 視覚的セルフチェックのワークフロー(capture → diff → judge → fix → re-verify)
存在理由: UI を修正したエージェントが 自画自賛 するのは、体系的な失敗です(failure
ui-pass-without-full-visual-review)。スクリーンショットを撮っても、誰も開かなければテストしていないのと同じです。このワークフローは、「エージェントが自分で見て、自分で合格とする」方法を、3 層の独立したチェックに置き換えます。そのうち 2 層は機械によるものです。2026/07/13 の判断(/last30days より): AI 生成 UI に関して業界で成熟しているものは、ただ 1 つです。それは ベースラインスナップショット + ピクセル差分です(
argos-ci/argos:「意図しない視覚的な変更を検出」;PlaywrighttoHaveScreenshot)。「LLM がルーブリックに基づいてスクリーンショットを評価する」層はまだ最先端の段階です(Agent-as-a-Judge、論文段階)。そのため、誰かが対応するのを待たずに、この層は自分たちで実装します。
WHAT
目的とコンテキスト
- 目的: SPA のローカル環境で、「エージェントが自分で見て、自分で合格とする」方法を、3 層の独立したチェックに置き換えます。ベースラインのピクセル差分(機械)、デザインと a11y の不変条件(機械)、ルーブリックに沿った全画像の確認(エージェント)を行い、機械と目視の両方で問題がない場合に限り、証拠となる画像ファイルを添えて合格とします。
- トリガー(使用するタイミング):
- テーマ/UI/ブランドを変更し、意図せず何かを壊していないか確認する必要がある場合。
ui_role≠noneのフレームで受け入れ判定のゲートが必要な場合(スキルbrの「視覚的セルフチェックのループ」を参照)。- ブラウザー拡張機能で localhost をキャプチャできない場合(よくあります)→ 代わりに headless CLI を使用します。
- ユーザーが「視覚 QA」「visual qa」「route-shots」「各 route のスクリーンショット」「UI をチェック」「ベースライン差分」「UI にリグレッションがあるか」「headless で UI テスト」「/visual-qa」と言った場合。
- 対象外:
computer-use(デスクトップ AX/Orca ブラウザー)と重複しません。4 項目のシニア判定ではありません(それは/qc-uiuxで、このエンジンを使用します)。「エラーをなくす」ために閾値を緩めません。
メンタルモデル
実行中のアプリ + 実際の route リスト + ログイン → route-shots(画像 + MANIFEST、すべての theme/state)→ 第 1 層 ベースライン差分(閾値 0)→ 第 2 層 ページ内不変条件(contrast-aa · monochrome-surface · rogue-slab · shadow-clipped · overlap · row-misalign)→ 第 3 層 N 枚すべての画像をルーブリックに沿って確認 → FINDINGS.md → 修正 → 再撮影 + 再確認。
入力と出力の契約
| フィールド | 必須? | 意味 | |
|---|---|---|---|
| In | 実行中のアプリ(--base) | はい | localhost の SPA |
| In | 実際の router から取得した route リスト + ログインアカウント | はい | 推測しない;ログインに失敗した場合はゲートを失敗させる必要があります |
| In | アプリ自体から取得した theme/state の一覧 | はい | アプリに 2 種類より多くある場合に light,dark をハードコードしないこと |
| In | ベースラインディレクトリ | いいえ | 初回に自動作成します |
| Out | <out>/*.png + MANIFEST.json | はい | ファイルがなければ = 一度も撮影していません |
| Out | --assert 使用時の終了コード | ゲートでは必須 | route エラー/画像不足/不変条件違反/ベースラインとの差異がある場合は 1 |
| Out | FINDINGS.md | はい | 「問題なし」を含め、route ごとに必ず 1 行 |
ルールと機能
血のにじむ教訓 — 繰り返さないでください:
- RULE-01 (MUST): 計算できるものは必ず機械に計算させてください — 目視に任せるのは禁止です。 文字のコントラストは 数値 です(WCAG ratio = f(文字色, 背景色))。ルーブリックに「文字がAAに適合」と書いてあっても、不変条件を設定しなければ評価器は盲目です。薄い文字がログインページ全体をすり抜け、エージェントも画像を見ただけでは判別できません。定量化できる基準は、必ずassertにしてください。ルーブリックの文章にしてはいけません。
- RULE-02 (MUST): どのルートも盲点にしてはいけません。 ドライバーが先にログインすると ⇒
/authにリダイレクトされ ⇒ サインインページは一度も監査されません — まさに不具合があるページなのに。まず未ログイン状態を撮影し、その後にログインしてください。再確認してください。モーダル、エラー、空、読み込み中など、まだ一度も画面に収めていない状態はありませんか? - RULE-03 (MUST): テーマも状態のひとつです — lightとdarkの両方を撮影してください。 アプリは通常、
prefers-color-schemeではなく アプリの設定 に応じてクラス(<html>の.dark)を有効にします ⇒ headlessでは、デフォルトでlightしか見えず、darkは完全な盲点になります。ユーザーがdarkでアプリを開くとUIが壊れているのに、ゲートは合格してしまいます。--themes light,dark(デフォルト)を有効にすると、初回実行で、lightは問題ない一方、darkではAAAに不適合な箇所が172か所見つかりました。設計上の帰結:アクセントカラーはテーマに応じて切り替える必要があります(明るい背景 → 暗いアクセント、暗い背景 → 明るいアクセント)。両方のテーマで同じアクセントカラーを使うのは、定義上誤りです。また、アクセント上の文字色をハードコードするのも禁止です(color:#fff)。もう一方のテーマでは白地に白になり、コントラスト比は1.13:1です。 - RULE-04 (MUST): アプリが提供するすべてのテーマを網羅してください — リストはアプリから取得し、独自に決めないでください。 アプリにはテーマが3つ(
default/default-dark/paper)あるのに、light,darkをハードコードしてしまいました ⇒paperは誰も確認せず ⇒ darkと同じように壊れており、ゲートではなくユーザーが見つけました。ルール:実行前に、アプリ自体からテーマ一覧を読み取ってください(Settings→Preferencesのドロップダウン、またはソースのTHEME_OPTIONS)。それからすべてを網羅します。一般化すると、状態のあらゆる軸(テーマ、ロケール、ロール、データなし/全データ)は、エージェントの記憶ではなくアプリから取得する必要があります。 - RULE-05 (MUST): 状態の変更は、アプリが変更するときと同じ経路で行い、実際に変わったことを検証してください。 立て続けに2段階の失敗がありました。①
.darkクラスを手動で付与した(アプリはdata-themeとCSSの注入を使用しているため、その仕組みは存在しない)。②localStorageを設定した(アプリがサーバー上のアカウント設定で上書きする)。どちらの場合も、実在しない状態でゲートが合格しました — 不合格よりも悪い結果です。確実に正しい方法は、ユーザーがクリックする経路です(Settings → Preferences → 選択)。そして必ず不変条件を設けてください。異なる2つのテーマで背景がまったく同じなら、画像は偽物です ⇒ ASSERT FAIL(ゲートは同じテーマを2回撮影して、自分を褒めているだけです)。 - RULE-06 (MUST): テーマはアプリの仕組みです。まずソースを読んでください。
.darkやprefers-color-schemeを前提にしないでください。documentElement.classList/data-theme/localStorage/<style>の注入をgrepして、アプリが何でテーマを変更するかを確認してください。テーマは通常、実行時にCSSを注入して読み込まれます ⇒ 通常のトークンでは静的なテーマファイルより優先されますが、!importantルールには負けます ⇒ UIが中途半端になります(アプリのテーマの背景 + 自分のテーマのカード = 画面の中に奇妙な色の領域)。正しい修正方法:独自の配色は、アプリが注入するテーマファイルそのものに記述してください。アプリが決して読み込まない場所に.dark{}ブロックを書いてはいけません。 - RULE-07 (MUST): ログインが失敗したら、ゲートも失敗しなければなりません。 ログインに失敗すると、すべてのルートが
/authにリダイレクトされます ⇒ ドライバーはサインインページのコピーを8枚監査し、「design-ok」と出力します。このような合格は不合格よりも悪い結果です。一度も見たことのないアプリを確認済みだと証明してしまいます。ログインに失敗したら--assertが直ちに失敗するようにしてください。 - RULE-08 (MUST): ユーザーが不具合を報告し、機械が問題なしと判定したら、間違っているのは機械であり、ユーザーではありません。(2026年7月14日の教訓 — ユーザーの言葉:"これは開発であって裁判ではないのに、自分の主張をかたくなに守るんだね"。)ゲートの合格は盾にはなりません。証明できるのは、教えられたことを、撮影できた状態で確認したことだけです。ユーザーが不具合を見たなら、ゲートが一度も確認していない状態/テーマ/バンドルが存在します。推測でユーザーに反論するのは禁止です(「たぶんキャッシュのせい」「たぶんキーを押し間違えた」)。推測するたびに検証作業をユーザーに押しつけることになり、私はそのやり方で3回連続で間違えました。正しい手順は、(1) 報告を信じる、(2) ユーザーとまったく同じ状態(ルート + テーマ + バンドル + ログイン/フォーカス状態)を再現する、(3) 測定してから話す、です。再現できなければ、「まだ再現できていません」と正直に伝えてください。仮説を作り上げて、それを擁護してはいけません。
- RULE-09 (MUST): 撮影前にランダム要因を取り除き、しきい値を緩めないでください。 点滅するカーソルとアニメーションによって、すべてのルートのベースラインが撮影のたびに約0.04%ずれます ⇒ ゲートはいつまでも不合格 ⇒ 無視される ⇒ 本当のリグレッションが見逃されます。ドライバーの初期化時にCSSで
animation/transition:none; caret-color:transparentを注入し、CodeMirrorのカーソルも非表示にすると、差分は**0%**になります。--diff-thresholdを緩めて「警告を止める」のは、機械を盲目にする行為であり、バグの修正ではありません。 - RULE-10 (MUST): 欲張りなセレクターは自分の首を絞めます。 ノイズ除去用CSSの
[class*="cursor"]は、Tailwindのcursor-pointer/cursor-textにも一致します ⇒ 入力欄が非表示になり、ログインに失敗し、ゲートが検査対象のアプリを壊します。ゲートが注入するCSS/セレクターはすべて、可能な限り狭くしてください。追加後は必ずフルフロー(ログイン → すべてのルート)を実行して確認してください。 - RULE-11 (MUST): ゲートはクリーンなリポジトリから実行できなければなりません。
playwright-coreがないと ⇒node route-shots.mjsはimport行で直ちに停止します ⇒ UIフレームのacceptance_testが一度も実行されません = 誰にも気づかれずにゲートが停止します。依存関係はskills/visual-qa/assets/package.jsonに記載します。インストール:npm i --prefix skills/visual-qa/assets。 - RULE-12 (MUST): 言葉より証拠。 pass/fixedが有効なのは、画像ファイルがある場合だけです。ファイルがなければ、まだ実施していません。推測によるpassの報告は禁止です。
- RULE-13 (MUST): 選り好みせず、すべて読んでください。 画像を7枚中3枚だけ読んで結論を出すのは、
ui-pass-without-full-visual-reviewfailureの根本原因です。未確認の画像に原因が最初から写っています。 - RULE-14 (MUST): 「機械はOK」≠「UIが美しい」。 不変条件がわかるのは、教えられたことだけです。ゲートの合格を、画像を読む手順を省く言い訳にしてはいけません。
- RULE-15 (MUST): しきい値は諸刃の剣です。 しきい値が緩すぎると本物の不具合を見逃します(差分0.5%で、カード全体を平らにするリグレッションが見逃されました)。厳しすぎると誤検知になり、ノイズの多いゲートは停止します。ベースラインにはしきい値0と手動レビューを使ってください。DOMの不変条件には、本物の不具合とノイズを区別できるしきい値が必要です。
- RULE-16 (MUST): CSSは推測せず、測定してください。 目視での診断は4回連続で間違えました(クリップ → 衝突 → セレクターの誤り → specificityの誤り)。
getComputedStyleを照会して、初めて根本原因がわかりました。CSSを修正する前に、必ず computed style と rect を測定してください。その後で修正します。修正後は、画像を見る前にもう一度測定してください。 - RULE-17 (MUST): 「エラーをなくす」ために機械を盲目にしないでください。 blurを検出器のしきい値より下げるのは、不具合の隠蔽であって修正ではありません(オーラは依然として切り取られます)。測定器ではなく、欠陥を修正してください。
- RULE-18 (MUST):
overflow:hiddenのコンテナ(狭いrail、タブバー)では、outsetシャドウを使わないでください。 クリップされたり、すき間に落ち込んだりして、見苦しい筋ができます。inset(はみ出さない)を使うか、シャドウをなくしてください。もう一方の軸がautoの場合、overflow-x:visibleは実現できません(CSSによってautoに強制されます)。 - RULE-19 (MUST):
playwright-core+ システムのChromeを使用してください(Chromiumを130MBもダウンロードしない)。ベースライン差分にはpixelmatch+pngjsが必要です(小さいパッケージです。ない場合はベースライン検査を自動的にスキップします)。 - RULE-20 (MUST): リアルタイムSPAで
networkidleを使うのは禁止です —domcontentloaded+ 固定時間の待機を使ってください。 - RULE-21 (MUST):
computer-useと競合させないでください(デスクトップAX/Orcaブラウザー)。このスキルは、localhostのSPA向けheadless CLIです。 - 機能:localhost上でブラウザーをheadless実行する、ページ内でcomputed style + rectを測定する、ピクセルをベースラインと比較する、画像/manifest/findingsを記録する、対象プロジェクトのUIを修正する。
失敗の境界
- ログイン失敗 →
--assertは即座に failed。サインインページのコピーは監査しません。 playwright-coreがない → import 行でゲートが停止します:blocked。npm i --prefix skills/visual-qa/assetsをインストールしてください。- ベースラインとの差分が 0 ではない → 閲覧し、
--update-baselineで承認するまで failed。 - ユーザーからエラーの報告があったのにマシンが問題なしと判定 → マシンの誤りです。同じ状態を再現してください。再現できない場合は「再現できませんでした」と明言してください。
- マシンの判定は問題なしでも、N 枚の画像をすべて読んでいない → 未完了。
方法
主なワークフロー
W01~W07 は、以下のステップ 0~6 に対応します。
| ステップ | 種別 | 入力 | アクション | 出力/終了条件 | 失敗時/次の手順 |
|---|---|---|---|---|---|
| W01 | 決定的 | アプリ、ルーター、アカウント | アプリの稼働を確認し、アプリから実際のルート一覧とテーマ一覧を取得します | ルート一覧+テーマ一覧 | アプリが動作しない → blocked |
| W02 | 副作用あり | ルート一覧 | route-shots.mjs(+--assert)で撮影します | 画像+MANIFEST.json | ログイン失敗 → failed |
| W03 | 決定的 | 画像+ベースライン | 閾値 0 でベースライン差分を確認します | 差分 | 0 ではない → 確認して承認(B02)または修正 |
| W04 | 決定的 | ページ内 | マシン検証可能な不変条件 | 違反または問題なし | ハード FAIL → W06 |
| W05 | 判断 | MANIFEST の N ルート | Read で N 枚すべての画像を読み、ルーブリックに沿って評価します | ルートごとに 1 行 | 未読画像がある → 未完了 |
| W06 | 副作用あり | 指摘事項 | FINDINGS.md(根本原因)を作成 → 修正 | 修正済みコード | — |
| W07 | 決定的 | 修正済みコード | 再撮影 → 再読 | マシン判定も目視判定も問題なし | 問題が残る → W06 |
各ステップの詳細(W01~W07 の信頼できる情報源):
0. アプリが稼働していること。 ルート一覧(推測せず、実際のルーターから取得)とログインアカウントを把握してください。
1. 撮影+証拠。
node skills/visual-qa/assets/route-shots.mjs \
--base http://localhost:5230 --out <dir>/qa-shots \
--baseline <dir>/qa-baseline \
--user demo --pass demo --assert
<out>/*.png + MANIFEST.json(ルート → ファイル → バイト数)を生成します。ファイルがない場合は、一度も撮影されていません。
2. ベースライン差分(マシン第 1 層)—意図しない変更を検出します。
初回はベースラインを自動作成します。次回以降はピクセル単位で比較します。デフォルトの閾値 = 0:差分が 0 ではないものはすべて FAIL とし、--update-baseline を使って確認し、手動で承認する必要があります。これは、CSS 不変条件の検査でもエージェントの目でも検出できない種類の問題(例:過剰な修正で UI 全体を平坦化してしまう)を検出できる唯一の層です。
3. マシン検証可能な不変条件(マシン第 2 層)—デザイン準拠+アクセシビリティ。 computed style を使ってページ内で実行します:
contrast-aa— すべてのテキストノードで WCAG AAA(文字色+実効背景色からコントラスト比を算出。通常の文字は 7:1、大きな文字は 4.5:1)。ハード FAIL。monochrome-surface— 大きなペイン(サイドバー/aside/nav)の背景色は統一されている必要があります。ハード FAIL。rogue-slab— すべてのビューポートの 2% 以上を占める要素について、不透明な背景色が body の背景色から 1.6:1 を超えて異なる場合に検出します。ハード FAIL。 (monochrome-surfaceが調べるのはサイドバー/aside/nav のみのため、ページ中央にある任意の<div>の不自然な色面は検出できません。)shadow-clipped—overflow:hiddenの親要素内で、外側に広がるオーラが切り取られている場合(ぼかしの閾値あり)。警告。overlap— 親子関係にない 2 つのコントロール(button/a/input/[role=button])の矩形が、縦横両方向で 4px 以上重なっている場合に検出します。ボタン同士の重なり(例:バッジ幅が変動するのに、position:fixedとマジックオフセットを使用)を検出します。ハード FAIL。row-misalign— 同じ親要素内のコントロールがほぼ同じ行(top のずれが ≤6px)にあり、高さの差が >3px の場合に検出します。隣り合うボタンの高さが揃っていない問題(例:パディングやフォントが異なり、トグルボタンがバッジより高い)を検出します。ハード FAIL。 (この 2 つのルールは 15/07/26 に追加されました。/redesign-existing-projects(CSS を静的に読み取るため、矩形の測定はできません)も unit-test(コンテンツを確認するだけで、座標は確認しません)も、レンダリング時の幾何学的な問題を検出できないためです。overlap の閾値に面積比を意図的に使わないのは、実際のバグではボタンの端だけがクリップされる(面積比 18%)ことがあり、面積比の閾値では見逃すためです。独立した 2 つのコントロールが数 px でも重なっていれば問題です。)
プロジェクトのデザイン仕様に合わせて不変条件を拡張してください。
4. 画像を読む — 厳格な網羅性+ルーブリック(人/エージェント層)。
MANIFEST に N ルートがある場合、N 枚すべての画像を Read する必要があります。FINDINGS には、各ルートにつき 1 行(「問題なし」の場合も含む)を記載してください。主観ではなくルーブリックに沿って評価します。例:ニューモーフィズムの場合、① 単色の面 ② 影が切り取られていない ③ CTA はアクセントカラーのフラットなデザイン ④ 文字/アイコンは AA ⑤ empty-state はブランドに準拠 ⑥ フォーカスリング。
5. FINDINGS.md → 修正。 # · route · 問題 · 重大度 · **根本原因** · 修正方法 の表。
6. 再撮影 → 再読 → その後に初めて合格。 マシンの判定も目視判定も問題がないこと。どちらかが欠けている場合は未完了です。
ブランチ
| ID | 種類 | ガード | 動作 | スキップ/失敗 | 再合流 |
|---|---|---|---|---|---|
| B01 | conditional_required | フレームゲートでは1つのルートのみ必要 | --route <r> を実行 | — | W03 |
| B02 | user_optional | ベースラインとの差異は、確認済みの意図的な変更 | --update-baseline | 画像を確認していない → 承認不可 | W04 |
| B03 | capability_optional | pixelmatch と pngjs がある | ベースライン差分を実行 | ない → ベースラインを自動でスキップ | W04 |
| B04 | recovery | ユーザーから不具合の報告があったが、ゲートは成功 | 状態(ルート + テーマ + バンドル + ログイン/フォーカス)を正確に再現し、測定してから説明 | 再現できない → 率直に伝える | W02 |
検証と停止条件
機械:--assert の終了コード(ルートの不具合/画像不足/不変条件/ベースライン)。目視:FINDINGS に MANIFEST の各ルートにつき1行ずつあること。両方に問題がなく、かつ画像ファイルがある場合のみ合格です。ファイルがない場合=未実施です。
例
- 成功例: アプリのテーマを変更し
/brを生成、THEME_OPTIONSの7ルート × 3テーマ →route-shots.mjs --assertの終了コードは0、ベースライン差分は0%、エージェントがすべての画像をReadで確認、FINDINGS に「問題なし」の7行 → 画像付きで合格。 - 境界/失敗例: light は問題ないが dark を網羅していない →
--themes light,darkを有効にすると AAA に違反する箇所が172件見つかる →--assertの終了コードは1。テーマに応じて変わるようアクセントカラーを修正して再撮影し、--diff-thresholdは緩和しない。
参考 — ドライバーフラグ
--base --out --user --pass · --route <r>(1ルート、フレームゲート用)· --assert
(終了コード1となる条件:ルートの不具合/画像不足/不変条件違反/ベースラインとの差異)· --baseline <dir> ·
--update-baseline(意図的な変更を承認)· --diff-threshold <%>(デフォルトは0)。