保存ボタンを押して「Saved」が表示された、というテストだけでは、リロード後や無効な更新を送った後にも前の値が残るかは分かりません。今回は、正常保存を基準にして、次の操作の前後で同じ値を比較する検査を作りました。
この考え方は普通のPlaywrightテストにも使えます。この記事では、2つのパターンをJSON設定で繰り返せる小さなOSS「App Crash Lab」を実装例にします。AIを使って開発・執筆し、掲載した結果は2026年10月10日に実行して確認しました。
比較するのは「入力欄」ではなく保存済みの値
メモアプリなら、次の2つを別々に確認します。
-
Buy coffeeを保存し、保存済み表示がその値になったことを確認する。リロードして、保存済み表示を比較する。 - 同じ正常保存から始め、空のメモを送る。空入力が拒否されたことを確認してから、保存済み表示が
Buy coffeeのままか比較する。
空にした入力欄を比較すると、意図した編集まで障害として扱ってしまいます。エラー表示も当然変わります。比較対象は、保持されるべき保存済みの表示やAPIの値です。
また、操作前から空だった値が操作後も空なら「一致」になります。そこで最初に、正常保存が既知の値へ到達したことを確認します。セレクタが見つからない、読み出しに失敗した、正常保存が成立しない場合は、合格やアプリのバグと決めずINCONCLUSIVEにします。
同じ操作を故障版と修正版へ適用する
同梱の合成メモアプリには、意図的な故障を2種類入れました。
| 対象 | 保存→リロード | 無効入力を拒否→前の値を確認 |
|---|---|---|
| リロードで保存値を失う版 | FAIL | PASS |
| 拒否した更新が前の値を壊す版 | PASS | FAIL |
| 修正版 | PASS | PASS |
実行結果では、故障版の保存値が"Buy coffee"から""へ変化しました。これは仕込んだ故障の検出確認であり、第三者の製品から新しいバグを発見したという意味ではありません。
同じ結果を手元で試す
Node.js 22以上とnpmが必要です。
git clone https://github.com/aichance/business-ai-recipes.git
cd business-ai-recipes/tools/app-crash-lab
npm ci --ignore-scripts --no-audit --no-fund
npx playwright install chromium
npm run demo
demoが合成アプリの一時サーバーを起動し、上の3パターンを実行して終了します。端末に表示されるindex.htmlを開くと実際の判定と差分を確認できます。初回のChromium取得には通信と空き容量が必要です。Linuxではnpx playwright install --with-deps chromiumでシステム依存も必要になる場合があります。
正常操作・観察する値・拒否条件を設定する
同梱のexamples/notes.jsonは次の内容です。これはデモアプリ用の完全な設定です。
{
"schemaVersion": 1,
"pack": "state-preservation@1",
"name": "Notes — save survives reload and rejection",
"baseURL": "http://127.0.0.1:4173",
"path": "/?mode=fixed",
"reset": [],
"setup": [
{ "action": "fill", "target": { "label": "Note" }, "value": "Buy coffee" },
{ "action": "click", "target": { "role": "button", "name": "Save" }, "save": true },
{ "action": "expectText", "target": { "testId": "status" }, "value": "Saved" }
],
"observe": { "savedNote": { "source": "text", "target": { "testId": "saved" } } },
"expected": { "savedNote": "Buy coffee" },
"rejectedUpdate": {
"steps": [
{ "action": "fill", "target": { "label": "Note" }, "value": "" },
{ "action": "click", "target": { "role": "button", "name": "Save" } }
],
"observe": { "message": { "source": "text", "target": { "testId": "status" } } },
"expected": { "message": "Rejected: note is empty" }
}
}
save: trueは保存を起こす操作、expectedは正常な保存の確認、observeは変化してはいけない値です。rejectedUpdateには無効な更新と、その拒否を確認する条件を置きます。
自分のアプリでは、URL、操作、セレクタ、保持すべき値、拒否条件を実装に合わせて変更します。本番データのないローカル開発用インスタンスで実行してください。
node cli.mjs run my-app.json --out .crash-lab/my-first-run
毎回、新しい出力ディレクトリを指定します。ブラウザは検査ごとに新しいコンテキストですが、サーバーDBはそれだけでは初期化されません。必要ならresetに初期化操作を入れるか、合成データを丸ごと置き換える正常保存を使います。
「拒否された」と「元の値が残った」は別の条件
保存値が同じでも、無効入力を受け付けているなら検査は合格にしません。拒否の成立と保存値の維持を、両方確認します。
APIなら期待した422などの4xxを拒否信号にできます。期待した422に対して500が返った場合は、正しい拒否の証明にせず、結果不明として扱います。HTTP 200で業務エラーを返すアプリでは、ステータスだけで判断せず、新たに現れるエラー表示やエラー値を設定します。
修正後に残せるもの
出力にはHTML/JSONの結果、差分、スクリーンショット、Playwright trace、repro.spec.mjsと設定ファイルが入ります。対象アプリを起動したまま、生成したテストを再実行できます。
npx playwright test --config .crash-lab/my-first-run/playwright.config.mjs
生成テストは@playwright/testだけに依存し、このCLIをimportしません。今回の検証では、修正版で成功し、リロード故障版では失敗することを再実行でも確認しました。
確認した範囲と限界
ローカルの31テストと、配布用ソースZIPを別の場所に展開した初回インストールを確認しました。別アプリとして、Glyphaの固定コミットをローカルで起動し、同じrunnerへ設定だけを渡して、リロードと422拒否後の保存状態・ETagが維持されることも確認しています。こちらは互換性の確認で、Glypha作者の採用や画面のピクセル一致を示すものではありません。
検査できるのは設定で選んだ値と時間範囲です。既定では期待状態を待ってから250msの安定期間を観察します。停電耐性、複数ユーザーの競合、観察していない値、もっと後に起きる破損は、この初版の対象外です。
Playwrightで直接書ける検査を、2つの定型パターンと共通レポートにしたものです。URLを渡すだけでアプリの正しい仕様を推測するツールではありません。
自分のアプリで試した場合、どの値を保持したかったか、設定で詰まった点、実際の判定が分かると次の検査パターンの改善に役立ちます。結果を共有する際は、traceや画面に含まれる秘密情報・実データを取り除いてください。
