pricing cardをcoding agentに直させるとします。desktopではきれいに見える。でも390px幅ではCTAが消え、変更前の画面は残っていない。agentは「完了しました」と返しているものの、何を確認して完了にしたのかはチャットを読み直さないと分かりません。
ここでpromptを長くしても、あまり改善しません。足りないのは指示の量ではなく、render後の合格条件です。
自分なら、UIを生成させる前に小さなacceptance contractをrepositoryへ置きます。agentの変更はworktreeへ隔離し、Playwrightとpreviewで同じ条件を再実行する。最後は人間がacceptかrejectを決めます。
acceptance contract
-> isolated worktree
-> agent change
-> Playwright + screenshot diff
-> preview review
-> accept / reject
この記事では/pricingのpricing-pro cardだけを対象に、このloopを組み立てます。
1画面分のacceptance contractを先に書く
最初に、変更対象と確認方法をYAMLへ固定します。これは特定toolの標準形式ではなく、project内でreviewするための小さなcontractです。
# acceptance/pricing-pro.yaml
route: /pricing
target: pricing-pro
viewport:
width: 390
height: 844
checks:
- role: button
name: 始める
- visual: pricing-pro.png
commands:
- pnpm lint
- pnpm test
- pnpm exec playwright test tests/ui-acceptance.spec.ts
fieldの役割は単純です。
-
route: 同じ画面を開く入口 -
target: componentまたは安定したselector -
checks: 意味の検証とvisual diff -
commands: agentの自己申告とは別に実行する判定
「pricingを今っぽくする」のようなpromptだけでは、どこまで変えてよいかが曖昧です。contractがあれば、少なくとも/pricing、pricing-pro、mobile viewport、CTAというreview対象がdiffの外に残ります。
このYAMLだけでtestが動くわけではありません。大事なのは、prompt transcriptより先に人間が読めることです。あとでschemaからtestを生成する仕組みを足すとしても、最初は1 route、1 componentで十分です。
1 taskを1 worktreeへ隔離する
agentには通常の作業directoryを直接触らせず、cleanなbase commitからworktreeを作ります。
git worktree add ../ui-pricing-acceptance \
-b agent/ui-pricing-acceptance HEAD
cd ../ui-pricing-acceptance
pnpm install
ここで固定したいのはbranch名だけではありません。
# acceptance/review-context.yaml
baseCommit: "<git rev-parse HEAD の値>"
worktree: ../ui-pricing-acceptance
devServer:
host: 127.0.0.1
port: 4317
複数taskを同時に走らせるなら、portも分けます。別のworktreeで起動したdev serverをPlaywrightが見に行くと、testは通っているのに対象diffを見ていない、という面倒な状態になります。
最近のagent clientには、worktree作成や会話のrewindを補助する機能も増えています。GitHub Copilot CLIの/worktreeと/rewindも、公開時点では実験的な機能です。便利でも、clientの会話状態、filesystemの変更、Git commitは別物として扱います。rewindをGitのrollback代わりにはしません。
Playwrightは見た目より先に意味を確認する
Playwrightを入れます。
pnpm add -D @playwright/test
pnpm exec playwright install chromium
以下はVite系projectを想定した最小構成です。dev scriptや起動commandはprojectに合わせて変えてください。
// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
use: {
baseURL: "http://127.0.0.1:4317",
viewport: { width: 390, height: 844 },
},
webServer: {
command: "pnpm dev -- --host 127.0.0.1 --port 4317",
url: "http://127.0.0.1:4317/pricing",
reuseExistingServer: false,
},
});
specでは、先にroleとaccessible nameを見ます。screenshotは最後です。
// tests/ui-acceptance.spec.ts
import { expect, test } from "@playwright/test";
test("pricing-pro cardをmobile幅で判定する", async ({ page }) => {
await page.goto("/pricing");
const card = page.getByTestId("pricing-pro");
await expect(card).toBeVisible();
const cta = card.getByRole("button", { name: "始める" });
await expect(cta).toBeVisible();
await expect(cta).toBeInViewport();
await expect(card).toHaveScreenshot("pricing-pro.png", {
animations: "disabled",
mask: [card.getByTestId("current-date")],
});
});
data-testidだけで全部を確認すると、buttonの名前が消えてもtestが通ります。逆にscreenshotだけでは、見た目が似たdivへbuttonを置き換えても意味の違いを見落とします。role、accessible name、viewport内にいることを確認してからvisual diffを取るほうが、失敗理由を追いやすくなります。
screenshotを安定させる
visual testが毎回揺れると、agentも人間もdiffを読まなくなります。最低限、次を処理します。
- animationとtransitionを無効にする
- 日時、乱数、localeをtest側で固定する
- API responseをfixtureまたはmockへ置き換える
- remote imageとweb fontの読込完了を待つ
- 意味を検証しない動的領域だけを
maskする
maskを増やして画面の半分を隠すのは逆効果です。変動値のほうを固定できないか先に見ます。
baseline更新も通常のtest commandから分けます。
{
"scripts": {
"test:ui": "playwright test tests/ui-acceptance.spec.ts",
"test:ui:update": "playwright test tests/ui-acceptance.spec.ts --update-snapshots"
}
}
agentにはpnpm test:uiまでを許可し、pnpm test:ui:updateは人間がvisual diffを確認したあとに実行します。最初のbaselineも同じです。生成できたことと、baselineとして承認したことを同じ操作にしないほうが安全です。
feedbackを「要素 + failure + artifact」にする
次の修正依頼は情報が足りません。
pricingをもう少し今っぽくして
代わりに、失敗した観測結果を渡します。
target: pricing-pro
failure: CTA "始める" is not visible at 390x844
artifact: test-results/pricing-card-mobile.png
expected: CTA remains visible without horizontal scroll
agentへ渡すのは、このpacket、失敗したspec、対象componentのdiffです。CSS Gridを使うかFlexboxを使うかまでは決めません。実装方法ではなく、次のrunでも観測できる期待値を固定します。
browser上の要素を指定してfeedbackできるagent機能も、この考え方と相性がよいです。ただし特定clientに依存しなくても、test id、failure message、screenshotがあれば同じloopを作れます。
fixtureは実際に作るUI patternから選ぶ
acceptance loopを試すためだけに、架空の派手なdashboardを作る必要はありません。pricing card、tool result、approval panelなど、projectで実装するpatternを2件ほど選びます。
生成UIの実装例やSDKを探すときは、生成AI UIデザインのリソース集のような分類済みの一覧をfixture選定の入口にできます。見つけたdemoをそのまま正解にせず、自分のcomponentへ落とした時点でrole、state、viewportをcontractへ書き直します。
ここで比較するのは見栄えではありません。
- loadingからresultへ移るときにlayoutが跳ねないか
- approval actionがkeyboardで操作できるか
- tool errorが成功状態と区別できるか
- mobile幅で主要actionがviewport外へ逃げないか
実例を眺める段階と、自分のproductで合否を決める段階を混ぜないのがコツです。
review packetを1つにまとめる
test結果がCI、screenshotがlocal directory、preview URLがchatに分散すると、また最初の問題へ戻ります。変更ごとにreview packetを1つ作ります。
# Review packet: pricing-pro
- base commit: <sha>
- branch: agent/ui-pricing-acceptance
- preview URL: <url>
- known failures: none | <failure list>
## Changed files
<git diff --name-only の結果>
## Checks
| command | exit status | artifact |
|---|---:|---|
| pnpm lint | <code> | review/lint.txt |
| pnpm test | <code> | review/unit-test.txt |
| pnpm test:ui | <code> | review/playwright-report/ |
## Visual artifacts
- before: review/before/pricing-pro.png
- after: review/after/pricing-pro.png
- diff: review/diff/pricing-pro.png
<code>や<url>をagentに推測させてはいけません。実行したprocessのexit statusと、実際に生成されたartifactだけを書かせます。preview URLもreview可能な実行物としては便利ですが、本番と同じ設定、network、権限まで保証するものではありません。
headlessなapp生成やSandbox上のdev server、共有可能なpreviewが揃うと、生成からreviewまでを自動化しやすくなります。それでもreview packetの正本はrepository側に置きます。serviceを替えてもdiff、spec、screenshotを残せるからです。
acceptとrejectの条件を分ける
最後に、人間が迷わない条件を決めます。
rejectにする例:
- Playwrightまたは既存testが失敗している
-
pricing-pro以外へ意図しないdiffが広がっている - screenshot差分を隠すためにbaselineだけ更新している
- previewで再現できず、failure artifactも残っていない
accept候補にできるのは、指定commandが通り、diffがscope内に収まり、before/afterを人間が確認できた変更です。ただしscreenshot一致はaccessibility、business logic、securityの保証ではありません。必要なreviewをvisual testだけで置き換えないようにします。
不合格ならworktreeを採用しません。修正を続ける場合も、同じacceptance contractとspecを再実行します。条件を途中で変えるなら、UIのdiffとは別にcontract変更としてreviewします。
生成を速くするほど、rejectを短くする
生成UIの品質をpromptだけで管理すると、agentを替えるたびにreview方法まで変わります。acceptance YAML、Playwright spec、Git diff、screenshotはmodelの外に残せます。
自分なら、まず/pricingの1 cardから始めます。mobileでCTAが見えることをroleとviewportで確認し、visual diffを人間が承認する。これが回ってから別routeや別componentへ広げます。
UIを一発で正しく生成させることには期待しすぎません。同じ条件で失敗を再現し、短時間でrejectできるほうが、日々の開発では扱いやすいです。