AIの典型的な失敗パターン
バグが視覚的なのにロジックをテストしてしまうAIエージェントの失敗パターン。
パターン
AI支援開発における最も頻繁なテスト失敗は、予測可能なパターンに従います:
ユーザーが報告:「表示されない」 または 「まだ壊れている」
AIエージェントがユニットテストを書くかロジックをチェックする
ロジックテストがパス -- データは正しく、コンポーネントは正しいJSXを返す
AIエージェントが宣言:「修正完了!テストがパスしました。」
ユーザーが報告:「まだ表示されない。」
AIエージェントが同じテストを再実行し、同じパス結果を得る
ユーザーの信頼を失うまでサイクルが繰り返される
なぜ起こるか
AIエージェントがレベル5(視覚的検証)を必要とする問題に対してレベル1(ユニットテスト)またはレベル2(DOMテスト)を選択しています。
データとロジックは正しいです。コンポーネントはDOMツリーに正しい要素をレンダリングします。しかし、祖先チェーンのどこかにあるCSSルールが要素を非表示にしています。
具体例
開発者がAIに通知バナーの追加を依頼します。AIがコンポーネントを作成します:
// NotificationBanner.tsx
export function NotificationBanner({ message }: { message: string }) {
return (
<div className="notification-banner">
<p>{message}</p>
</div>
);
}そしてテストを書きます:
// NotificationBanner.test.tsx
it("renders the message", () => {
render(<NotificationBanner message="Update available" />);
expect(screen.getByText("Update available")).toBeTruthy();
});テストはパスします。しかしユーザーは画面に何も見えません。なぜでしょうか?
/* layout.css -- inherited from the page layout */
.main-content {
overflow: hidden;
max-height: 0;
transition: max-height 0.3s ease;
}
.main-content.expanded {
max-height: 1000px;
}通知バナーは .main-content 内にレンダリングされており、デフォルトで max-height: 0 と overflow: hidden が設定されています。要素はDOMに存在します(レベル2パス)が、高さ0にクリップされて視覚的に非表示です(レベル5ならキャッチ)。
修正方法
Note
ユーザーが何かが「表示されない」と言った場合、デフォルトでレベル5の検証を使用してください。要素とその祖先の算出スタイルをチェックし、スクリーンショットを撮影して視覚的な状態を確認します。
レベル5の検証で以下が明らかになります:
verify-ui result:
.main-content {
overflow: hidden // <-- clipping children
max-height: 0px // <-- zero height
}
.notification-banner {
display: block // present in DOM
// but parent clips it to invisible
}headless-browserのスクリーンショットにより、バナーが表示されていないことが視覚的に確認されます。
このパターンの他のバリエーション
overflow: hidden + height: 0 パターンはバリエーションの1つに過ぎません。他のよくある原因:
| CSSプロパティ | 効果 | レベル2で検出? | レベル5で検出? |
|---|---|---|---|
display: none | 要素がフローから除去される | 部分的* | はい |
visibility: hidden | 要素は非表示だがスペースを占める | いいえ | はい |
opacity: 0 | 要素が完全に透明 | いいえ | はい |
z-indexのスタッキング | 要素が別の要素の背後に | いいえ | はい |
祖先のoverflow: hidden | コンテンツがクリップされる | いいえ | はい |
transform: scale(0) | 要素が何もないサイズに縮小 | いいえ | はい |
position: absolute + 画面外 | 要素がビューポート外に配置 | いいえ | はい |
* 例外:jest-domの toBeVisible() は、インラインで設定された(style="display: none")、または hidden 属性による display: none を検出できます -- jsdomはこれらを直接解決でき、そのスタイルシートがテストのDOMに実際に存在していれば、そのCSSOMは <style> タグの単純なセレクタもマッチできます。実際にレベル2を無力化しているのは、たいていの場合スコープの問題です:コンポーネントテストはコンポーネント単体をレンダリングするため、上の .main-content のような祖先のスタイルシートはそのDOMに一切読み込まれません。そしてクリッピング、画面外への配置、スタッキングといったジオメトリ由来の非表示は、jsdomにレイアウトエンジンがないため、CSSがどう書かれていようとレベル2からは見えません。
ハイドレーションのミスネスト変種
上の CSS 系のバリエーションは「下位レベルはパスするのにページが壊れている」の1つの系統です。アイランドアーキテクチャには、もう1つの構造的な系統があります。ハイドレーションのミスネストです。アイランドがハイドレーションするとき、そのランタイムがマーカーのラッパーを再出力し、サーバーレンダリングされた子要素を1階層ずれた位置に付け替えてしまう -- 「ダブルラップ」です。子要素はすべてそのまま存在しています。ただ、1要素だけ深くネストされているのです。
この系統が厄介なのは、レベル4未満のあらゆるレベルがパスしてしまう点です:
レベル1 / レベル2(render-to-string による SSG プレゼンステストを含む)はパスする -- ハイドレーション前のマークアップは正しいから。
レベル3はパスする -- ビルドされた dist の HTML は正しいから。
破綻は実際のブラウザでハイドレーションが動いた後にのみ現れるため、静的な出力を検査するものは何も気づけません。これは構造的で決定論的なバグであり、ハイドレーションのレース(フレーク)ではありません。しかも再発します。あるプロジェクトはこのまったく同じ系統に、2つの異なるアイランドコンポーネントで2回ぶつかりました。
これを捕捉できるのは、ハイドレーション待ちを伴うレベル4テストだけです。ハイドレーションをトリガーし(アイランドをビューにスクロールするなどして起動する)、ハイドレーションのシグナルを待ち、それからハイドレーション後の DOM 構造 -- 単なる存在ではなく、子要素の数と縦方向の積み上がり -- をアサートします。
// island.hydration.spec.ts (Playwright)
test("island children are not double-wrapped after hydration", async ({ page }) => {
await page.goto("/article-with-island");
// Trigger + wait for hydration, not a fixed timeout.
await page.locator("[data-island]").scrollIntoViewIfNeeded();
await page.locator("[data-island][data-hydrated]").waitFor();
// Assert STRUCTURE, not just presence: the items must be direct
// children of the list, and they must stack vertically (a wrong
// nesting level collapses or re-flows them).
const items = page.locator("[data-island] > ul > li");
await expect(items).toHaveCount(3);
const first = await items.nth(0).boundingBox();
const second = await items.nth(1).boundingBox();
expect(second!.y).toBeGreaterThan(first!.y);
});重要なポイント
Warning
ロジックテストに基づいてビジュアルバグを修正済みと宣言しないでください。 ユーザーが何かが見えないと言う場合、テストは表示を検証する必要があり、それにはレベル5が必要です。
レベル5でも対象サーフェスに到達できない場合は?
ほとんどの表示バグはレベル5で捕捉できます。要素にはDOMのbounding rectがあり、算出スタイルがあり、スクリーンショット差分で比較できるピクセルコンテンツがあるためです。しかしcanvasベースやフォトエディタ風のサーフェスでは、それらのいずれも露出していないことがあります — レンダリングされた画像が <canvas> 要素の中にあり、アサーション可能な安定したDOM子要素を持たず、スクリーンショットのピクセル差分はノイズが多すぎて信頼できない。
同時に以下の両方が真である場合:
L4が書けない(canvasのドロップターゲット、多重カメラ変換、リサイズ可能なレイヤなど)
L5がアサーションに届かない(DOM rectがなく、算出スタイルが適用できず、ピクセル差分はノイズが多すぎる)
…そのときにレベル6(AIベース)が最終手段になります。L5が単に扱いにくいときの「次の試み」ではありません — L6は非決定論的でコストがかかり、CIには不向きです。アサーションに到達する決定論的な経路が他にない場合にのみ手を伸ばしてください。