Open8

PlayWrightのAgentsを試す

Megumu UedaMegumu Ueda

環境構築

Claude CodeもCopilotもも契約していないのでopencodeで試してみる。

npx playwright init-agents --loop=opencode

最近はサプラチェーン攻撃でClaude Codeとかを悪用されることがあるので、気休め程度ではあるのだけどinstallはせずにnpxで実行する。

まずは利用する生成AIの設定。ここではOpenAI(gpt-4.1)を設定した。

npx opencode-ai auth login

APIキー設定後にツール起動。

npx opencode-ai
Megumu UedaMegumu Ueda

Planを生成

seed.spec.jsを準備して以下を実行。

@playwright-test-planner 私が作ったゲーム(https://uemegu.github.io/sayin5min/)を操作して、ノンデグテストケースを立案したいです。

* Seed fileはここです: tests/seed.spec.js
* テストプランは specs/ 配下にMarkdownで出力してください
* sayin5min配下にゲームのソースコードがあります
* ソースコードの構造はdeepwiki(https://deepwiki.com/uemegu/sayin5min)を確認してください
* テストプランはノンデグレードテストとして、全機能を浅く網羅するような形で作ってください

出力は日本語でお願いします。

ここでseed.spec.jsは以下を準備してた。

const { test } = require("@playwright/test");

test("seed", async ({ page }) => {
  await page.goto("https://uemegu.github.io/sayin5min/");
});

AIの応答は以下。

まず、指示内容を整理します。

## やることの整理

1. ゲーム(https:uemegu.github.io/sayin5min/)の全機能を浅く網羅するノンデグレードテストプランを立案
2. テストプランは specs/ 配下にMarkdownで出力
3. Seedファイル(tests/seed.spec.js)とゲームソース(sayin5min/配下)を参考にする
4. ソース構造は deepwiki(https:deepwiki.com/uemegu/sayin5min)で確認
5. 出力は日本語

## 次のアクション

1. 指定URL(ゲーム本体)をfetchし、機能を把握
2. deepwikiでソース構造を調査
3. seedファイルを確認し、テスト観点を抽出
4. テストプランを日本語Markdownで作成し、specs/配下に出力

---

まず、ゲームURLとdeepwikiをfetchして機能・構造を把握します。

Fetch https://uemegu.github.io/sayin5min/ (format=markdown)

           5秒で告白

Fetch https://deepwiki.com/uemegu/sayin5min (format=markdown)

uemegu/sayin5min | DeepWiki

Index your code with Devin /private%E2%80%91repo

DeepWiki https:deepwiki.com

DeepWiki

uemegu/sayin5min https:github.com/uemegu/sayin5min

ゲームの機能概要とソース構造が把握できました。 次は seed ファイル(tests/seed.spec.js)を確認し、テスト観点を抽出します。

その後、全機能を浅く網羅するノンデグレードテストプランを日本語で作成し、specs/配下にMarkdownで出力します。

まず tests/seed.spec.js を読みます。

Read tests/seed.spec.js

const { test } = require("@playwright/test");

test("seed", async ({ page }) => {
  await page.goto("https:uemegu.github.io/sayin5min/");
});


tests/seed.spec.js は、単にトップページへアクセスするだけのシードテストでした。
このため、実際の機能網羅には、deepwikiやゲーム画面から機能を洗い出す必要があります。

### ここまでの情報整理

#### 主な機能(deepwiki・画面・READMEより)

• トップメニュー(スタート・コンティニュー・言語切替)
• ストーリー進行(分岐あり、選択肢あり)
• 3Dアバター表示・アニメーション・リップシンク
• フルボイス再生
• セーブ・ロード(ローカルストレージ)
• BGM・効果音
• マルチリンガル(日本語・英語)
• エンディング・クレジット
• 各種UI(メッセージウィンドウ、選択肢、トースト等)

---

次に、これらを浅く網羅するノンデグレードテストプランを日本語で作成します。 テスト観点ごとに1ケースずつ、Markdownでまとめます。

作成後、specs/配下に出力します。

生成されたプランは以下。

# 5秒で告白 ノンデグレードテストプラン

本ドキュメントは「5秒で告白」ゲームの主要機能を浅く網羅するノンデグレードテストプランです。

## テスト観点一覧

1. **トップメニュー表示・スタートボタン動作**
    - ゲーム起動時にトップメニューが正しく表示されること
    - スタートボタン押下でストーリーが開始されること

2. **コンティニューボタン動作**
    - セーブデータが存在しない場合、コンティニューボタンが無効または非表示であること
    - セーブデータが存在する場合、コンティニューボタン押下で途中から再開できること

3. **言語切替(日本語・英語)**
    - 言語切替UIが表示され、切替後に全UI・ストーリーが選択言語で表示されること

4. **ストーリー進行(メッセージ送り・分岐選択肢)**
    - メッセージ送りが正常に動作し、選択肢が表示されること
    - 選択肢を選ぶとストーリーが分岐すること

5. **3Dアバター表示・アニメーション**
    - ストーリー進行中に3Dアバターが正しく表示され、アニメーションすること

6. **リップシンク・ボイス再生**
    - セリフ再生時にリップシンクが音声と同期して動作すること
    - フルボイスが再生されること

7. **BGM・効果音再生**
    - シーンごとにBGMが再生され、必要に応じて効果音も再生されること

8. **セーブ・ロード機能**
    - 任意のタイミングでセーブ・ロードができ、再開時に状態が正しく復元されること

9. **エンディング到達・クレジット表示**
    - いずれかのエンディングに到達し、クレジットが表示されること

10. **UI表示(メッセージウィンドウ、選択肢、トースト等)**
    - メッセージウィンドウ、選択肢、トースト等のUIが正しく表示・動作すること

---

各観点ごとに1ケースずつ、浅く網羅することを目的とします。
Megumu UedaMegumu Ueda

コードのGenerate

次にテストコードを生成してもらう。

@playwright-test-generator specs/nondegrade_test_plan.mdをもとにテストコードを生成してください

結果がおかしい&OpenAIのダッシュボードを見る限りサブエージェントの活動は終わってるので結果を受け取れていないので追加で2回指示。

結果がおかしいですね。URLはhttps://uemegu.github.io/sayin5min/であることを @playwright-test-generator に明示的に依頼してください。
ログを見る限り、エージェントは動き終わってますよね。結果を受け取ってますか?

比較してないのでわからないけど、うまくいかないのはOpenCodeの問題??
出力された結果は以下。

import { test, expect } from '@playwright/test';

// 1. トップメニュー表示・スタートボタン動作
test('トップメニューが表示され、スタートボタンでストーリー開始', async ({ page }) => {
  // トップページにアクセス
  await page.goto('https://uemegu.github.io/sayin5min/');
  // トップメニューが表示されていることを確認
  await expect(page.getByRole('heading', { name: 'SAY in 5min' })).toBeVisible();
  // スタートボタンが表示されていることを確認
  await expect(page.getByRole('button', { name: /スタート|Start/i })).toBeVisible();
  // スタートボタンをクリック
  await page.getByRole('button', { name: /スタート|Start/i }).click();
  // ストーリーが開始され、メッセージウィンドウが表示されることを確認
  await expect(page.locator('.message-window')).toBeVisible();
});

// 2. コンティニューボタン動作
test('セーブデータ無しでコンティニューボタンが無効/非表示', async ({ page }) => {
  // ローカルストレージのセーブデータをクリア
  await page.goto('https://uemegu.github.io/sayin5min/');
  await page.evaluate(() => localStorage.clear());
  await page.reload();
  // コンティニューボタンが無効または非表示であることを確認
  const continueBtn = page.getByRole('button', { name: /コンティニュー|Continue/i });
  await expect(continueBtn).toBeDisabled();
});

test('セーブデータ有りでコンティニューボタンから再開', async ({ page }) => {
  // セーブデータをセット(仮の例: 実際のキー名・値は要調整)
  await page.goto('https://uemegu.github.io/sayin5min/');
  await page.evaluate(() => {
    localStorage.setItem('saveData', JSON.stringify({ scene: 2, ...{} }));
  });
  await page.reload();
  // コンティニューボタンが有効であることを確認
  const continueBtn = page.getByRole('button', { name: /コンティニュー|Continue/i });
  await expect(continueBtn).toBeEnabled();
  // コンティニューボタンをクリック
  await continueBtn.click();
  // 途中からストーリーが再開されることを確認(例: メッセージウィンドウが表示)
  await expect(page.locator('.message-window')).toBeVisible();
});

// 3. 言語切替(日本語・英語)
test('言語切替UIで全UI・ストーリーが切替言語で表示', async ({ page }) => {
  await page.goto('https://uemegu.github.io/sayin5min/');
  // 言語切替UIが表示されていることを確認
  await expect(page.getByRole('button', { name: /English|日本語/ })).toBeVisible();
  // 英語に切り替え
  await page.getByRole('button', { name: /English/ }).click();
  // UIが英語表記になっていることを確認
  await expect(page.getByRole('button', { name: /Start/ })).toBeVisible();
  // スタートボタンでストーリー開始
  await page.getByRole('button', { name: /Start/ }).click();
  // ストーリーのメッセージが英語で表示されていることを確認
  await expect(page.locator('.message-window')).toContainText(/[a-zA-Z]/);
});

// 4. ストーリー進行(メッセージ送り・分岐選択肢)
test('メッセージ送りと選択肢表示・分岐', async ({ page }) => {
  await page.goto('https://uemegu.github.io/sayin5min/');
  await page.getByRole('button', { name: /スタート|Start/i }).click();
  // メッセージ送り(クリックで次のメッセージへ)
  await page.locator('.message-window').click();
  // 選択肢が表示されることを確認
  await expect(page.locator('.choices')).toBeVisible();
  // 選択肢を選ぶ
  await page.locator('.choices button').first().click();
  // ストーリーが分岐し、次のメッセージが表示されることを確認
  await expect(page.locator('.message-window')).toBeVisible();
});

// 5. 3Dアバター表示・アニメーション
test('ストーリー進行中に3Dアバターが表示・アニメーション', async ({ page }) => {
  await page.goto('https://uemegu.github.io/sayin5min/');
  await page.getByRole('button', { name: /スタート|Start/i }).click();
  // 3Dアバターが表示されていることを確認
  await expect(page.locator('canvas')).toBeVisible();
  // アバターがアニメーションしている(canvasの描画が変化する)ことを確認(簡易的にcanvasのピクセル値が変化するかチェック)
  const before = await page.locator('canvas').screenshot();
  await page.waitForTimeout(1000);
  const after = await page.locator('canvas').screenshot();
  expect(before).not.toEqual(after);
});

// 6. リップシンク・ボイス再生
test('セリフ再生時にリップシンクとフルボイスが動作', async ({ page }) => {
  await page.goto('https://uemegu.github.io/sayin5min/');
  await page.getByRole('button', { name: /スタート|Start/i }).click();
  // セリフ再生時にaudio要素が存在し再生されていることを確認
  const audio = page.locator('audio');
  await expect(audio).toBeVisible();
  // リップシンク用のクラスや属性が変化していることを確認(例: .avatar.lipsync-active など)
  await expect(page.locator('.avatar.lipsync-active')).toBeVisible();
});

// 7. BGM・効果音再生
test('シーンごとにBGM・効果音が再生される', async ({ page }) => {
  await page.goto('https://uemegu.github.io/sayin5min/');
  await page.getByRole('button', { name: /スタート|Start/i }).click();
  // BGM用audio要素が存在し再生されていることを確認
  const bgm = page.locator('audio.bgm');
  await expect(bgm).toBeVisible();
  // 効果音用audio要素も存在する場合は確認
  const se = page.locator('audio.se');
  if (await se.count() > 0) {
    await expect(se).toBeVisible();
  }
});

// 8. セーブ・ロード機能
test('任意タイミングでセーブ・ロードし状態復元', async ({ page }) => {
  await page.goto('https://uemegu.github.io/sayin5min/');
  await page.getByRole('button', { name: /スタート|Start/i }).click();
  // セーブボタンをクリック
  await page.getByRole('button', { name: /セーブ|Save/i }).click();
  // ロードボタンをクリック
  await page.getByRole('button', { name: /ロード|Load/i }).click();
  // 状態が復元されていることを確認(例: メッセージ内容や進行状況が変わらない)
  await expect(page.locator('.message-window')).toBeVisible();
});

// 9. エンディング到達・クレジット表示
test('エンディング到達でクレジットが表示される', async ({ page }) => {
  await page.goto('https://uemegu.github.io/sayin5min/');
  await page.getByRole('button', { name: /スタート|Start/i }).click();
  // メッセージ送りや選択肢選択を繰り返しエンディングまで進める(例: 20回クリック)
  for (let i = 0; i < 20; i++) {
    if (await page.locator('.choices').isVisible()) {
      await page.locator('.choices button').first().click();
    } else {
      await page.locator('.message-window').click();
    }
  }
  // クレジットが表示されていることを確認
  await expect(page.getByText(/クレジット|credit/i)).toBeVisible();
});

// 10. UI表示(メッセージウィンドウ、選択肢、トースト等)
test('メッセージウィンドウ・選択肢・トースト等のUIが正しく表示', async ({ page }) => {
  await page.goto('https://uemegu.github.io/sayin5min/');
  await page.getByRole('button', { name: /スタート|Start/i }).click();
  // メッセージウィンドウが表示されていることを確認
  await expect(page.locator('.message-window')).toBeVisible();
  // メッセージ送りで選択肢が表示されることを確認
  await page.locator('.message-window').click();
  await expect(page.locator('.choices')).toBeVisible();
  // トースト通知が表示される場合は確認
  const toast = page.locator('.toast');
  if (await toast.count() > 0) {
    await expect(toast).toBeVisible();
  }
});

うーん、妄想で書いてるコードがあるな。

  // リップシンク用のクラスや属性が変化していることを確認(例: .avatar.lipsync-active など)
  await expect(page.locator('.avatar.lipsync-active')).toBeVisible();
Megumu UedaMegumu Ueda

テストを実行する

npx playwright test tests/nondegrade.spec.ts

全ケース失敗。。。

Megumu UedaMegumu Ueda

Healerでテストケースを修正する

@playwright-test-healer test-results配下の結果を確認してテストケースを修正してください。

動かしてログを確認してみるとplaywrightのmcpがエラーになってそう。

opencode.jsonを修正してみる。これであってるかはわからない。

  "mcp": {
    "playwright-test": {
      "type": "local",
      "command": [
        "npx",
        "playwright",
        "run-test-mcp-server",
        "--config", //ここを追加
        "./playwright.config.ts" //ここを追加
      ],
      "enabled": true
    }

もう1回指示を出すと頑張って修正し始める。
が、どうもHealerを呼んだのは最初の1回だけでそのあとは自分で頑張って修正をし続けてる。
2〜3件パスしたけど頑張りすぎるので止める。

あなたが修正するのではなくて@plauwright-test-healerに修正を任せてくれますか?

するとちゃんとサブエージェントを起動させて動作確認してからテストコードの修正に入った。
直ったかと思いきや、解消できなったものはfixmeになってた。

  // ストーリーが開始され、最初の選択肢ボタンが表示されることを確認
  // FIXME: .choices button が見つからずテスト失敗。メッセージウィンドウが表示されていればストーリー進行中とみなす。
  // FIXME: ストーリー進行UIが見つからないためテストをスキップ
  test.fixme();

Megumu UedaMegumu Ueda

ちなみにplanner / generator / healer はこんな感じのプロンプトが内部的に生成されて、AIに指示を与えるときにこのプロンプトで子エージェントを起動させて動かすというだけの仕組み。
ものすごい何かがあるわけではなくて、仕組み的にはplaywrightとmcpとプロンプトエンジニアリング。
(コンテキストエンジニアリングレベルはplaywrightの範囲外)

Megumu UedaMegumu Ueda

趣味で作るものが最近はゲームしかなかったのでゲームを題材にしたけど、題材としては適切ではなかったかな。
あと試した環境はOpenCode+GPT-4.1なので最新の賢い環境というわけでもない。