WebWrightが打鍵テストの救世主になるかもしれない

こんにちは。Microsoftが出したWebWrightを、自分で運用してる宿泊予約システムの管理画面で試してみたら思いのほか良かったので記録しておきます。

E2Eテストは便利、でもスクリプトを書くのがしんどい

PlaywrightみたいなE2Eテストツールはめちゃくちゃ便利です。ブラウザを自動で操作して「ボタンを押したらモーダルが出る」「フォームを埋めて送信したら一覧に出る」みたいなのを機械的に検証できます。

ただ欠点なのは、スクリプトを用意しないといけないことです。セレクタを調べて、待ち時間を入れて、要素が見つからずに落ちては直してみたいな面倒な作業に膨大な気力が必要になります。一回作れば楽なんですが、最初の一歩がとにかく腰が重いです。

生成AIが出てスクリプト自体は書いてくれて多少速度は上がりました。ブラウザ操作のMCPが出たおかげで、自律的にスクリプトを修正するフィードバックループが回せるようになりました。
ですがそれでもなお頻繁にスクリプトを間違うことがあったり、再現性のないスクリプトを書くことがあります。その度に人間が介入して「動かして→落ちて→セレクタ直して」のループを回す必要がありました。
また、出てくるスクリプトのフォーマットもまちまちで、ログがなく状況がわからなかったり、チームで運用するために一定の品質にしようと思ったらプロンプトで詳細な指示が必要になってました。

WebWrightとは

MicrosoftのWebWrightは、この課題に対して、探索ログを決まったルールで永続化し、最後のスクリプトも決まったフォーマットでスクリプト出力してくれる ので、自前でデバッグログの指示や成果物の指示をする必要がなく、成果物のフォーマットも特に指定が不要です。

本質的な違いは、ブラウザを操作することではなくスクリプトを書くことが目的なので、コードで再現性が担保されている点です。

試してみた

自分が個人で運用している宿泊予約システムの管理画面で試してみました。
これはReact + Chakra UIで作られていて、管理者は日常的に予約を作成し、それを印刷する操作を行います。

explore_2_step1.png:「予約を作成する」モーダル(Step1)

explore_2_step1.png:「予約を作成する」モーダル(Step1)

この管理画面はそろそろリファクタリングをしたいと思っていて、その前にデグレしていないかどうかを確認するためにE2Eテストをずっと書きたかったのですが、手で書くのが面倒で気が乗らず、ちょうどWebWrightの試し打ちにうってつけでした。

導入方法

Claude Codeのプラグインマーケットプレイス経由で入れます。

# マーケットプレイスを追加
/plugin marketplace add microsoft/Webwright

# プラグインをインストール
/plugin install webwright@webwright

# 反映
/reload-plugins

これで/webwright:runスキルが使えるようになります。

ちなみに自分の環境ではPlaywright自体がどのPython環境にも入ってなかったので、よしなに環境を作ってインストールしてくれました。

cd <workspace>
uv venv --python 3.13 .venv
uv pip install --python .venv playwright
.venv/bin/python -m playwright install firefox

webwright:run と指示内容

あとは/webwright:runに、やりたいことを自然言語で渡すだけです。今回はこう指示しました。

/webwright:run 以下の宿泊予約管理画面で、男2, 女1, の2泊三日の宿泊予約を登録し、
登録完了後に一覧に表示されていることを確認し、その内容で印刷を行って

https://******.cloudfront.net/admin?admin=*****

ここで感心したのが、いきなり書き込み操作に突っ込むのではなく、

  1. まず読み取り専用で画面とフォーム構造を調査
  2. 必須項目なのに指定がないもの(所属・氏名・電話番号・開始日など)を勝手に決めずに質問してくる

というように調査から進めてくれたことです。「男2女1」→大人男性2・大人女性1、「2泊三日」→duration=2泊、と解釈してくれた上で、足りない情報だけ確認してきました。今回はテストデータ(所属=テスト / テスト太郎 / 09000000000 / 開始日=1週間後の6/6)で進めてもらいました。

調査の段階で画面のスクショも自動で撮ってくれています。

explore_1_admin.png:管理画面のトップ

explore_1_admin.png:管理画面のトップ

explore_2_step1.png:「予約を作成する」モーダル(Step1)

explore_2_step1.png:「予約を作成する」モーダル(Step1)

できたもの

最終的にfinal_runs/run_1/配下に成果物が出来上がりました。

  • final_script.py … 再利用可能なPlaywrightスクリプト
  • final_script_log.txt … 各ステップの実行ログ
  • screenshots/final_execution_*.png … 各ステップのスクショ9枚

final_script.pyの中身はこんな感じです。パラメータが冒頭の定数にまとまっていて、Chakra UIのラジオボタンが隠しinputなのでラベルをクリックする、といった実際に画面を見ないと分からないハマりどころもちゃんと処理されています。

import asyncio
from pathlib import Path
from playwright.async_api import async_playwright

# 確定パラメータ
CHURCH = "テスト"
NAME = "テスト太郎"
PHONE = "09000000000"
ADULT_MALE = "2"
ADULT_FEMALE = "1"
START_DATE = "2026-06-06"   # startDate select の value (yyyy-MM-dd)
ARRIVED = "-1"              # 未定
DURATION = "2"             # 2泊

# ...

        # --- CP3: 日程 ---
        await page.select_option('select[name="startDate"]', START_DATE)
        await page.select_option('select[name="arrivedAtHour"]', ARRIVED)
        await asyncio.sleep(1)  # durationのラジオは開始日入力後に展開される
        # Chakra Radioの実inputは非表示なので、ラベルをクリックして選択する
        await page.locator(f'label.chakra-radio:has(input[name="duration"][value="{DURATION}"])').click()

screenshots/の中には、explore_*.png(調査時に自動で撮られたもの)とfinal_execution_*.png(最終スクリプト実行時のもの)が入っています。

一発では動かなかった:window.print()でハング

WebWrightが組み立ててくれたスクリプトを、後から自分で動かしてみたらエラーになりました。ログを見ると、予約登録〜一覧表示〜印刷ページを開くところまでは全部成功していて、最後の「印刷ページのスクショ撮影」だけがタイムアウトしていました。

step 10 action: 印刷ページを開いた URL=https://******.cloudfront.net/admin/print?admin=*****
...
playwright._impl._errors.TimeoutError: Page.screenshot: Timeout 30000ms exceeded.
Call log:
  - taking page screenshot
  - waiting for fonts to load...

原因は、印刷ページ(/admin/print)が表示時にwindow.print()を自動実行する作りになっていたことです。headless環境だとこの印刷処理がページを「ビジー状態」にしてしまい、screenshot()waiting for fonts to load...のまま固まってしまいます。最初の実行ではタイミングよく撮れていましたが、要はフレーキー(不安定)な状態でした。

修正は、全ページでwindow.print()を無効化するinit scriptを仕込むだけです。

        # 印刷ページは表示時に window.print() を自動実行する。headlessだとこれが
        # ページをビジー状態にして screenshot が "waiting for fonts to load" で
        # タイムアウトするため、全ページで window.print を無効化しておく。
        await context.add_init_script("window.print = () => {};")

ここだけ直したら無事に最後まで通りました。

成功したときの実行ログ

最終的にこういうログが出て、登録から印刷ページの内容確認までひと通り通りました。

step 1 action: 管理画面を開いた
step 2 action: 「予約を作成する」モーダル(Step1)を開いた
step 2 action: 人数入力: 大人男性=2, 大人女性=1, 子供男女=0
step 3 action: 基本情報入力: 所属=テスト, 氏名=テスト太郎, 電話=09000000000
step 3 action: 日程入力: 開始日=2026-06-06, 到着時間=未定, いつまで=2泊
step 3 action: Step1入力状態確認: {'church': 'テスト', 'name': 'テスト太郎', 'phone': '09000000000', 'male': '2', 'female': '1', 'cmale': '0', 'cfemale': '0', 'startDate': '2026-06-06', 'arrived': '-1', 'duration': '2'}
step 5 action: Step2(食事・宿泊人数)を既定値のまま表示
step 6 action: Step3確認画面表示: 大人男性2人=True, 2泊=True
step 7 action: 「予約をする」を押下、申込完了(alert自動承認)
step 8 action: 一覧で『テスト太郎』かつ『6/6』を含む行: 3件
step 9 action: 詳細モーダルを開いた (大人男性2人=True, 2泊=True)
step 10 action: 印刷ページを開いた URL=https://******.cloudfront.net/admin/print?admin=*****
step 10 action: 印刷ページ内容に氏名含む=True, 所属含む=True, 男2=True, 女1=True

FINAL_RESPONSE: 予約登録完了: テスト所属 / テスト太郎 / 男2・女1 / 2026-06-06から2泊 / 一覧表示3件 / 印刷ページ表示OK

まとめ

一番しんどかった「最初のE2Eスクリプトを書く」部分を、AIが画面を見ながら肩代わりしてくれるのは想像以上に体験が良かったです。window.print()のハングみたいに一発では動かない箇所も残りますが、出来上がったスクリプトのどこを直せばいいかが明確なので、ゼロから書くのとは比べ物にならないくらい楽でした。

管理画面のリファクタリング前のガードレールとしては十分使えそうなので、もう少し育ててみようと思います。


See also