Playwright-mcpとClaude Desktopで、Webフォームのテスト工程自動化を体感


## PlaywrightがMCPで登場
ここ数か月話題のMCP、
食いつくな、というのが難しいレベルで画期的だと感じています！

https://modelcontextprotocol.io/introduction

このMCPが、Playwrightにも用意されています。
今回はPlaywright-mcpを使った、Webフォームのテスト工程自動化を、現場の流れに近い形でやってみました。

https://github.com/microsoft/playwright-mcp

https://zenn.dev/shiaf/articles/8327e38d75f25a

### Playwright

https://playwright.dev/

> Playwright enables reliable end-to-end testing for modern web apps.

Microsoftが開発したWebブラウザ自動化ツールです。Chrome、Firefox、Safari など複数のブラウザでWebアプリケーションのテスト自動化が可能なツールです。
javaScriptやPythonなどをサポートしています。

### Playwright-mcpを用いると？
Playwrightの場合は、行いたい画面操作をプログラミング言語で表現していましたが、
Playwright-mcpを利用することで、
**自然言語を用いた形で**、Webフォームのテスト自動化が可能になります。

## 単体テスト以降に行うテスト
コードベースで行える単体テストではなく、それ以降の画面が絡むテストの話です。

**Testing Pyramid**、という単語の通りですが、単体より規模の大きいテストは開発コストがかかる、と一般的には言われています。

https://speakerdeck.com/twada/automated-test-knowledge-from-savanna-202305-edition?slide=42

### テスト工程の作業
以下の作業順で進めるのが一般的と思っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/1853bd45-7e8c-4651-b734-4b4a962e17cc.png)

1. 仕様書作成
2. データ準備（今回はスキップします）
3. テスト実施
4. エビデンス作成

実際は不具合などが発生して、
コード修正する場合がありますが、ここでは考えないことにします。

ClaudeやMCPを活用することで、いずれのステップもより短時間で実施できそうです。
以降は具体的な作業を進めていきます。

## MCPを用いたテスト実施を体感してみよう

まずは基本的なその画面限りで成立するフォームを用いて試します。

:::note info
**画面のテストについて**
今回は基本的なことを試してみるのがメインです。
画面遷移や認証が絡んで本格的になると、考えることが増えます。

Gmailの操作をplaywright-mcpを用いてやろうとすると、
最初は未ログインの状態でGmailが立ち上がるため、パスワードを入力する指示を文章に記載する必要がありました。
1度ログインを実施した状態で再度ブラウザを起動したときは再入力不要だったので、工夫すればソーシャルアカウントによる認証、といったテストも実施できるかもしれない、といった感じでした。
（アカウント情報をAIに渡すことについて、セキュリティ上どうなの？といった話はまた別に検討する必要はありますね。）


実際に色々試してみるしかなさそうです！
:::

### 環境と利用するMCP
私は、Claude Desktopを普段用いています。
最近、Proプラン（月20ドル）に切り替えました。

## テスト用のフォームを用意
入力項目・入力種類が複数あるフォームで、バリデーションも設けています。
とりあえず用意するだけなので、Streamlitを使ってフォームを作成しました。（作成と言っても、Claudeに聞いて出来上がったものです。）

<details><summary>コード載せておきます</summary>

```python
import streamlit as st
import re
from datetime import datetime, date
import pandas as pd

# ページ設定
st.set_page_config(
    page_title="ユーザー登録フォーム",
    page_icon="📝",
    layout="centered"
)

# カスタムCSS
st.markdown("""
<style>
    .main-header {
        text-align: center;
        color: #1f77b4;
        margin-bottom: 30px;
    }
    .success-message {
        padding: 10px;
        background-color: #d4edda;
        border: 1px solid #c3e6cb;
        border-radius: 5px;
        color: #155724;
        margin: 10px 0;
    }
    .error-message {
        padding: 10px;
        background-color: #f8d7da;
        border: 1px solid #f5c6cb;
        border-radius: 5px;
        color: #721c24;
        margin: 10px 0;
    }
</style>
""", unsafe_allow_html=True)

# セッション状態の初期化
if 'submitted_data' not in st.session_state:
    st.session_state.submitted_data = []

# バリデーション関数
def validate_email(email):
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return re.match(pattern, email) is not None

def validate_phone(phone):
    pattern = r'^0\d{1,4}-\d{1,4}-\d{4}$|^0\d{10,11}$'
    return re.match(pattern, phone) is not None

def validate_age(age):
    return 0 <= age <= 120

# メイン画面
st.markdown('<h1 class="main-header">📝 ユーザー登録フォーム</h1>', unsafe_allow_html=True)

# フォーム作成
with st.form("user_registration_form"):
    st.subheader("基本情報")
    
    # 基本情報入力
    col1, col2 = st.columns(2)
    
    with col1:
        last_name = st.text_input("姓 *", placeholder="田中")
        first_name = st.text_input("名 *", placeholder="太郎")
        age = st.number_input("年齢 *", min_value=0, max_value=120, value=25)
    
    with col2:
        email = st.text_input("メールアドレス *", placeholder="example@email.com")
        phone = st.text_input("電話番号", placeholder="090-1234-5678")
        gender = st.selectbox("性別", ["選択してください", "男性", "女性", "その他", "回答しない"])
    
    # 追加情報
    st.subheader("追加情報")
    
    col3, col4 = st.columns(2)
    with col3:
        birth_date = st.date_input("生年月日", value=date(1990, 1, 1))
        prefecture = st.selectbox("都道府県", [
            "選択してください", "北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県",
            "茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県",
            "新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県", "静岡県", "愛知県",
            "三重県", "滋賀県", "京都府", "大阪府", "兵庫県", "奈良県", "和歌山県",
            "鳥取県", "島根県", "岡山県", "広島県", "山口県", "徳島県", "香川県", "愛媛県", "高知県",
            "福岡県", "佐賀県", "長崎県", "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県"
        ])
    
    with col4:
        occupation = st.text_input("職業", placeholder="エンジニア")
        interests = st.multiselect("興味のある分野", [
            "プログラミング", "デザイン", "マーケティング", "営業", "経営", 
            "教育", "医療", "法律", "芸術", "スポーツ", "音楽", "読書"
        ])
    
    # 自己紹介
    bio = st.text_area("自己紹介", placeholder="簡単な自己紹介をお書きください...", max_chars=500)
    
    # 利用規約同意
    agree_terms = st.checkbox("利用規約に同意する *")
    newsletter = st.checkbox("メルマガを受け取る")
    
    # 送信ボタン
    submitted = st.form_submit_button("登録する", type="primary", use_container_width=True)
    
    # バリデーションと処理
    if submitted:
        errors = []
        
        # 必須項目チェック
        if not last_name.strip():
            errors.append("姓を入力してください")
        if not first_name.strip():
            errors.append("名を入力してください")
        if not email.strip():
            errors.append("メールアドレスを入力してください")
        if not agree_terms:
            errors.append("利用規約に同意してください")
        
        # フォーマットチェック
        if email and not validate_email(email):
            errors.append("正しいメールアドレスの形式で入力してください")
        
        if phone and not validate_phone(phone):
            errors.append("電話番号の形式が正しくありません（例: 090-1234-5678）")
        
        if not validate_age(age):
            errors.append("年齢は0～120の間で入力してください")
        
        if gender == "選択してください":
            errors.append("性別を選択してください")
        
        if prefecture == "選択してください":
            errors.append("都道府県を選択してください")
        
        # エラーがある場合
        if errors:
            st.markdown('<div class="error-message"><strong>❌ 入力エラー</strong><br>' + 
                       '<br>'.join([f"• {error}" for error in errors]) + '</div>', 
                       unsafe_allow_html=True)
        else:
            # 成功時の処理
            user_data = {
                "登録日時": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                "姓": last_name,
                "名": first_name,
                "年齢": age,
                "メールアドレス": email,
                "電話番号": phone if phone else "未入力",
                "性別": gender,
                "生年月日": birth_date.strftime("%Y-%m-%d"),
                "都道府県": prefecture,
                "職業": occupation if occupation else "未入力",
                "興味分野": ", ".join(interests) if interests else "未選択",
                "自己紹介": bio if bio else "未入力",
                "メルマガ": "受け取る" if newsletter else "受け取らない"
            }
            
            st.session_state.submitted_data.append(user_data)
            
            st.markdown('<div class="success-message"><strong>✅ 登録完了</strong><br>ユーザー登録が正常に完了しました！</div>', 
                       unsafe_allow_html=True)
            
            # 登録データの表示
            st.subheader("登録内容")
            st.json(user_data)

# 登録済みデータの表示
if st.session_state.submitted_data:
    st.markdown("---")
    st.subheader("📊 登録済みユーザー一覧")
    
    if st.button("データをクリア", type="secondary"):
        st.session_state.submitted_data = []
        st.rerun()
    
    # データフレームで表示
    df = pd.DataFrame(st.session_state.submitted_data)
    st.dataframe(df, use_container_width=True)
    
    # CSV ダウンロード
    csv = df.to_csv(index=False, encoding='utf-8-sig')
    st.download_button(
        label="📥 CSVでダウンロード",
        data=csv,
        file_name=f"user_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
        mime="text/csv"
    )
```
</details>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/0f4c15b9-f873-46db-8f2f-da15142ebc64.png)

## テストケースを作成
`playwright-mcp`の出番はもう少し先ですが、
ケース作成もAIに任せると、開発者はケース選定に集中できます！

ここでは、`fileSystem-mcp`を使います。

https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem

https://zenn.dev/takna/articles/mcp-server-tutorial-02-filesystem

### fileSystem-mcpを利用するために

PCの特定のフォルダに対して、
Claudeからアクセスしていいよ！と明記する必要があります。

Claude Desktopの設定画面で、
「開発者」から「構成を編集」をクリックします。

`claude_desktop_config.json`に記述を追加します。
私はWindows環境なので、以下のようにバックスラッシュをエスケープしています。`args`の第2引数以降は、各利用者によって記載が変わります。

記載後はファイルを保存し、Claude Desktopを再起動します。
```json
{
  "mcpServers": {
    "filesystem": {
      "command": "C:\\Program Files\\nodejs\\node.exe",
      "args": [
        "C:\\Users\\{username}\\AppData\\Roaming\\npm\\node_modules\\@modelcontextprotocol\\server-filesystem\\dist\\index.js",
        "C:\\Users\\{username}\\Documents\\images",
        "C:\\Users\\{username}\\Documents\\MCP\\streamlit_playwright_test",
      ]
    }
  },
  "globalShortcut": "Shift+Ctrl+Space"
}
```

https://modelcontextprotocol.io/quickstart/user

### チャットからケース作成を依頼
上記設定完了後、
作成したフォームを以下チャットと合わせて添付しました。
```text
このフォームに対するテストケースを作成します。

ケースごとにmarkdownファイルを作成してOKです。
ファイル名は、「{nnn}_どういったテストをするか.md」でお願いします。
nnn：001～999の連番です。

作成したmarkdownファイルはC:\Users\{username}\Documents\imagesに「テストケース」というフォルダを作成して、そこに配置しておいてください。

各markdownファイルの中身のガイドラインです。
最初にどういったケースは見出しを記載しておきます。

以降、各ケースについて、ブラウザでどういった操作をするか、記載します。
最初に2ステップは以下で共通にしてください。
1.ブラウザを起動し、http://localhost:8501にアクセス
2.画面にフォームが表示されるまで待機
以降は各ケースごとに異なる
```

**10分もかからずに**、私が指定したフォルダに、
テストケース用のmarkdownファイルが作成されていました。
![スクリーンショット 2025-06-18 154221.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/a53d3061-87ca-4405-b0af-1903fecdf7c3.png)

### 作成されたテスト仕様書を見てみる
以下が作成された仕様書です。

**これだけでも1日以上は時間が浮くと思いました。**
テスト仕様書の記載は、思っている以上に時間を取られますし、間違えることもしばしばありそうです。

この後開発者がすべきことは、
各ケースを読み込み、仕様と照らし合わせて、ケースの削除や追加することかと思います。

また、これだけのフォームで30ケースもあるのかな？と思いましたが、確認してみると確かにそういうケースもあるな～～と思いました。

```markdown
# 正常系_全項目入力での登録

## テストケース概要
全ての項目に正しい値を入力して、正常に登録が完了することを確認する

## 操作手順
1. ブラウザを起動し、http://localhost:8501にアクセス
2. 画面にフォームが表示されるまで待機
3. 姓に「田中」を入力
4. 名に「太郎」を入力
5. 年齢に「30」を入力
6. メールアドレスに「tanaka@example.com」を入力
7. 電話番号に「090-1234-5678」を入力
8. 性別で「男性」を選択
9. 生年月日で「1993-01-01」を選択
10. 都道府県で「東京都」を選択
11. 職業に「エンジニア」を入力
12. 興味のある分野で「プログラミング」「デザイン」を選択
13. 自己紹介に「よろしくお願いします」を入力
14. 「利用規約に同意する」をチェック
15. 「メルマガを受け取る」をチェック
16. 「登録する」ボタンをクリック

## 期待結果
- 緑色の成功メッセージ「✅ 登録完了」が表示される
- 登録内容がJSON形式で表示される
- 登録済みユーザー一覧に新しいデータが追加される

```

## ブラウザを使って、ケースの操作を実施する

### playwright-mcpを利用するために

fileSystem-mcpと同様、`claude_desktop_config.json`に記載を追加します。

[Getting started](https://github.com/microsoft/playwright-mcp?tab=readme-ov-file#getting-started)で記載された通りｎ記述します。
（Claude Desktop以外の選択肢がいくつもあります。）

### チャットで指示
```text
C:\Users\eno49\Documents\MCP\streamlit_playwright_test\test_caseフォルダにあるmarkdownについて、
テストをしていきます。

また同じ階層に「実施結果.md」というファイルがあるので、
ケース実施完了後に、ケース番号 | OK/NG | NGの理由をテーブル形式で記載しておいてほしいです。
```

### チャット送信後の挙動
全部乗せると、少し長くなってしまうので、カットしている箇所もあります。
Claude Desktopの動きです。
![playwright-mcp_claude.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/81651eef-4262-4f8f-9d80-597114267488.gif)

この動画の後、
ブラウザが起動しました。（作成したアプリは起動状態）

以下のように、テスト仕様書に沿って、入力作業を進めてくれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/e6d06f2a-850e-49f1-a480-2f9b3b916a30.png)

**より人間っぽい動きで、面白いと感じる部分もありました。**

テストケースの文章では記載していませんでしたが、
都道府県のプルダウンで、一旦`t`のみを入力していました。
候補がなかったことを確認後、漢字で直接入力するといった挙動も見せていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/25571e83-7b34-443f-b832-7bb2f2e02713.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/c127ce3d-f945-4768-9f40-e93bb5f3517a.png)

## エビデンス取得もやってもらいたい（ケースのブラッシュアップ）
playwright自体にスクリーンショットを撮る実装があることは知っていたので、
エビデンス取得作業もやってもらえそう、と思いました。

テスト仕様書の最後にスクリーンショット撮影のための手順を追加して、再度プロンプトを実行しました。
```markdown
12. スクリーンショット保存
    1. ファイル名は001_{nnn}とする。(nnn:001～999の連番)
        1.  撮影
        2.  2秒待機
        3.  画面の高さ分下にスクロール
        4.  スクロールできなければ終了、まだスクロールできる場合は撮影を続ける
```
どこにスクリーンショットがあるかわからなかったので聞いてみると、

`C:\Users\eno49\AppData\Local\Temp\playwright-mcp-output\2025-06-18T11-32-08.986Z`に保存した、と返ってきました。

確かに、スクリーンショットが保存されていました。

フォルダパスに`Temp`とあることからも想像できますが、
「暫定フォルダで消える可能性がある」と言われたのでこの画像をデスクトップやらに移動すれば、エビデンスにできるかもしれません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/7a06e2fa-e98c-4da2-80d8-cda1a8af03c1.png)

## 感想・まとめ
今回、簡単ではありますが、
フォーム画面に対するテストをClaude Desktopとplaywright-mcpを用いて、自動化する作業に取り組んでみました。

全部を任せっきりにするというよりは、単調で間違える可能性が低いと考えらえるケース消化は自動化し、
人の手で進める必要があるケース（複雑な操作をするケースや認証絡みで情報を公開したくなかったりするケース）に専念する形で、作業を効率的に進められると感じました。

## 参考資料
https://gihyo.jp/article/2025/06/mcp-server-lists-for-vscode

https://code.visualstudio.com/mcp

https://shiftasia.com/ja/column/%E3%83%87%E3%82%B7%E3%82%B8%E3%83%A7%E3%83%B3%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%EF%BC%88%E6%B1%BA%E5%AE%9A%E8%A1%A8%EF%BC%89%E3%81%A8%E3%81%AF/

https://service.shiftinc.jp/column/8223/