mcpField
E
e2e-tests-studio

e2e-tests-studio

github.com/mastra-aiテスト・品質29.2k2.5k 回インストール今日更新
リポジトリ

e2e-tests-studio について

e2e-tests-studioは、フロントエンド変更のPlaywright E2Eテストを作成・検証するスキルです。画面表示だけでなく、ユーザーが確認できる製品の動作、BDD形式の構成、現実的なフィクスチャを重視します。

主な機能

  1. 01製品の動作を中心にテスト
  2. 023段階のBDD構造を徹底
  3. 03機能を観測可能な結果に対応付け
  4. 04現実的なテストフィクスチャを案内
  5. 05永続化とエラー回復を検証
  6. 06ビルドとテストのコマンドを提示

活用シーン

  1. 01エージェント設定が応答に与える影響のテスト
  2. 02ツール実行とワークフローのデータ連携の検証
  3. 03複数ターンの会話での文脈維持の確認
  4. 04画面状態テストから動作テストへの移行

MCP 対応

ブラウザテストにはPlaywright MCPサーバーが必要です。ブラウザナビゲーションツールがない場合の追加方法も記載されています。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mastra-ai/mastra --skill e2e-tests-studio

インストール後、Claude Code で/e2e-tests-studioを入力して読み込まれたか確認します。

詳細

フロントエンド変更のE2E動作検証

基本原則:UIの状態ではなく、プロダクトの動作をテストする

重要:テストでは、UI要素が表示されるかどうかだけでなく、プロダクトの機能が正しく動作することを検証する必要があります。

テストしないこと(UIの状態):
  • ❌「クリックするとドロップダウンが開く」
  • ❌「ボタンのクリック後にモーダルが表示される」
  • ❌「リクエスト中に読み込みスピナーが表示される」
  • ❌「フォームのフィールドが表示されている」
  • ❌「サイドバーが折りたたまれる」
テストすること(プロダクトの動作):
  • ✅「LLM プロバイダーを選択すると、そのプロバイダーを使用するようエージェントが設定される」
  • ✅「新しいエージェントを作成すると保存され、エージェント一覧に表示される」
  • ✅「パラメーターを指定してツールを実行すると、期待どおりの出力が返される」
  • ✅「チャットメッセージが正しくストリーミングされ、会話のコンテキストが維持される」
  • ✅「ワークフローを実行すると、正しい順序でツールが呼び出される」

BDD構造(必須)

すべてのE2E仕様は、MSWテストと同じBDD構造に従う必要があります。 packages/playground では、e2e-bdd/test-needs-when-describe によってこの構造が強制されます。

構造は、以下のちょうど3つのレベルで構成されます。

  1. 外側の test.describe =テスト対象の単位(ファイルごとに1つのページまたは機能)。
  2. 内側の test.describe('when …') =前提条件をちょうど1つ指定します。タイトルは必ず when で始めます。
  3. 各 test =観測可能な結果をちょうど1つ指定します。
import { test, expect } from '@playwright/test';
import { resetStorage } from '../__utils__/reset-storage';

test.describe('Tools list page', () => {
  // the unit
  test.afterEach(async () => {
    await resetStorage();
  });

  test.describe('when a registered tool is clicked', () => {
    // ONE precondition (starts with "when")
    test('navigates to that tool detail page', async ({ page }) => {
      // ONE outcome
      await page.goto('/tools');
      await page.locator('text=Get current weather for a location').click();
      await expect(page).toHaveURL(/\/tools\/weatherInfo$/);
    });

    test('shows the tool name as the page heading', async ({ page }) => {
      // ONE outcome
      await page.goto('/tools');
      await page.locator('text=Get current weather for a location').click();
      await expect(page.locator('h2')).toHaveText('weatherInfo');
    });
  });
});

ルール:

  • ファイルごとに、テスト対象の単位を名前にした外側の test.describe を1つだけ置きます。
  • すべての末端の test は、前提条件を示す test.describe('when …') グループ内に置きます。トップレベルに単独の test() を置いてはいけません。
  • 複数のアサーションを含む test() は、アサーションが個別の結果を表す場合にのみ分割します。1つの結果を証明する密接に関連したアサーションはまとめておきます。アサーションを削除してはいけません。
  • beforeEach/afterEach は、それらを必要とする最も狭い describe スコープに配置します。

前提条件

Playwright MCP サーバーが必要です。browser_navigate ツールを使用できない場合は、ユーザーに追加するよう案内してください。

claude mcp add playwright -- npx @playwright/mcp@latest

ステップ1:機能の意図を理解する

テストを書く前に、必ず次の質問に答えてください。

  1. この機能は、ユーザーのどのような問題を解決しますか?
  2. 機能が正しく動作した場合、どのような結果が期待されますか?
  3. システム内をどのようなデータが流れますか?(ユーザー入力 → API → 状態 → UI)
  4. ページを再読み込みした後も、何が保持されるべきですか?
  5. この操作によって、どのような後続の影響が生じるべきですか?

これらの回答を、テストファイル内のコメントとして記載してください。

ステップ2:ビルドして起動する

pnpm build:cli
cd packages/playground/e2e/kitchen-sink && pnpm dev

http://localhost:4111 でサーバーが起動していることを確認します。

ステップ3:機能を動作テストに対応付ける

機能とテストの対応付けガイド
機能カテゴリテスト内容アサーションの例
エージェント設定設定の変更がエージェントの動作に反映されることメッセージを送信 → 選択したモデルが応答に使用されていることを確認
LLM プロバイダーの選択選択したプロバイダーがリクエストで使用されることAPI呼び出しをインターセプト → リクエストペイロード内のプロバイダーを確認
ツールの実行ツールが正しいパラメーターで実行され、結果を返すことツールを実行 → 出力が期待される変換結果と一致することを確認
ワークフローの実行ステップが順番に実行され、データがステップ間を流れることワークフローを実行 → 各ステップの出力が次のステップに渡されることを確認
チャット/ストリーミングメッセージが保持され、ターンをまたいでコンテキストが維持されること複数ターンの会話 → コンテキストが認識されていることを確認
MCP サーバーのツールサーバーのツールを呼び出せて、データが返されることMCP ツールを呼び出し → 応答の構造と内容を確認
メモリー/永続化ページを再読み込みしてもデータが保持されること項目を作成 → 再読み込み → 項目が存在することを確認
エラー処理エラーが正しくユーザーに通知されることエラー条件を発生させる → エラーメッセージと復旧を確認

ステップ4:動作に焦点を当てたテストを書く

テスト構造のテンプレート
import { test, expect, Page } from '@playwright/test';
import { resetStorage } from '../__utils__/reset-storage';
import { selectFixture } from '../__utils__/select-fixture';
import { nanoid } from 'nanoid';

/**
 * FEATURE: [Name of feature]
 * USER STORY: As a user, I want to [action] so that [outcome]
 * BEHAVIOR UNDER TEST: [Specific behavior being validated]
 */

test.describe('[Feature Name] - Behavior Tests', () => {
  let page: Page;

  test.beforeEach(async ({ browser }) => {
    const context = await browser.newContext();
    page = await context.newPage();
  });

  test.afterEach(async () => {
    await resetStorage(page);
  });

  test.describe('when [the single precondition for these outcomes]', () => {
    test('[verb describing the single observable outcome]', async () => {
      // ARRANGE: Set up preconditions
      // - Navigate to the feature
      // - Configure any required state
      // ACT: Perform the user action that triggers the behavior
      // ASSERT: Verify the OUTCOME, not the UI state
      // - Check data persistence
      // - Verify downstream effects
      // - Confirm API calls made correctly
    });
  });
});
動作テストのパターン
パターン 1:設定が動作に影響する
test.describe('when a different LLM provider is selected', () => {
  test('uses that provider for agent responses', async () => {
    // ARRANGE
    await page.goto('/agents/my-agent/chat');

    // Intercept API to verify provider
    let capturedProvider: string | null = null;
    await page.route('**/api/chat', route => {
      const body = JSON.parse(route.request().postData() || '{}');
      capturedProvider = body.provider;
      route.continue();
    });

    // ACT: Select a different provider
    await page.getByTestId('provider-selector').click();
    await page.getByRole('option', { name: 'OpenAI' }).click();

    // Send a message to trigger the agent
    await page.getByTestId('chat-input').fill('Hello');
    await page.getByTestId('send-button').click();

    // ASSERT: Verify the selected provider was used
    await expect.poll(() => capturedProvider).toBe('openai');
  });
});
パターン 2:データの永続化
test.describe('when a new agent is created', () => {
  test('persists after page reload', async () => {
    // ARRANGE
    await page.goto('/agents');
    const agentName = `Test Agent ${nanoid()}`;

    // ACT: Create new agent
    await page.getByTestId('create-agent-button').click();
    await page.getByTestId('agent-name-input').fill(agentName);
    await page.getByTestId('save-agent-button').click();

    // Wait for creation to complete
    await expect(page.getByText(agentName)).toBeVisible();

    // ASSERT: Verify persistence
    await page.reload();
    await expect(page.getByText(agentName)).toBeVisible({ timeout: 10000 });
  });
});
パターン 3:ツールの実行で正しい出力が得られる
test.describe('when the weather tool is executed with a city', () => {
  test('returns formatted weather data for that city', async () => {
    // ARRANGE
    await selectFixture(page, 'weather-success');
    await page.goto('/tools/weather-tool');

    // ACT: Execute tool with parameters
    await page.getByTestId('param-city').fill('San Francisco');
    await page.getByTestId('execute-tool-button').click();

    // ASSERT: Verify OUTPUT content, not just that output appears
    const output = page.getByTestId('tool-output');
    await expect(output).toContainText('temperature');
    await expect(output).toContainText('San Francisco');

    // Verify structured data if applicable
    const outputText = await output.textContent();
    const outputData = JSON.parse(outputText || '{}');
    expect(outputData).toHaveProperty('temperature');
    expect(outputData).toHaveProperty('conditions');
  });
});
パターン 4:ワークフローのステップの連結
test.describe('when a multi-step workflow is run', () => {
  test('passes data between steps correctly', async () => {
    // ARRANGE
    await selectFixture(page, 'workflow-multi-step');
    const sessionId = nanoid();
    await page.goto(`/workflows/data-pipeline?session=${sessionId}`);

    // ACT: Trigger workflow execution
    await page.getByTestId('workflow-input').fill('test input data');
    await page.getByTestId('run-workflow-button').click();

    // ASSERT: Verify each step received correct input from previous step
    // Wait for completion
    await expect(page.getByTestId('workflow-status')).toHaveText('completed', { timeout: 30000 });

    // Check step outputs show data transformation chain
    const step1Output = await page.getByTestId('step-1-output').textContent();
    const step2Output = await page.getByTestId('step-2-output').textContent();

    // Verify step 2 received step 1's output as input
    expect(step2Output).toContain(step1Output);
  });
});
パターン 5:コンテキストを使用したストリーミングチャット
test.describe('when a multi-turn conversation is held', () => {
  test('maintains conversation context across messages', async () => {
    // ARRANGE
    await selectFixture(page, 'contextual-chat');
    const chatId = nanoid();
    await page.goto(`/agents/assistant/chat/${chatId}`);

    // ACT: Multi-turn conversation
    await page.getByTestId('chat-input').fill('My name is Alice');
    await page.getByTestId('send-button').click();
    await expect(page.getByTestId('assistant-message').last()).toBeVisible({ timeout: 20000 });

    await page.getByTestId('chat-input').fill('What is my name?');
    await page.getByTestId('send-button').click();

    // ASSERT: Verify context was maintained
    const response = page.getByTestId('assistant-message').last();
    await expect(response).toContainText('Alice', { timeout: 20000 });
  });
});
パターン 6:エラーからの回復
test.describe('when the API fails during tool execution', () => {
  test('shows an actionable error and allows a successful retry', async () => {
    // ARRANGE: Set up failure fixture
    await selectFixture(page, 'api-failure');
    await page.goto('/tools/flaky-tool');

    // ACT: Trigger the error
    await page.getByTestId('execute-tool-button').click();

    // ASSERT: Error is shown with recovery option
    await expect(page.getByTestId('error-message')).toContainText('failed');
    await expect(page.getByTestId('retry-button')).toBeVisible();

    // Switch to success fixture and retry
    await selectFixture(page, 'api-success');
    await page.getByTestId('retry-button').click();

    // Verify recovery worked
    await expect(page.getByTestId('tool-output')).toBeVisible({ timeout: 10000 });
    await expect(page.getByTestId('error-message')).not.toBeVisible();
  });
});

ステップ 5:既存のテストを更新する

テストファイルがすでに存在する場合:

  1. 既存のテストを読み、現在のテスト範囲を把握します
  2. テストが UI 中心か、動作中心かを確認します
  3. UI 中心のテストをリファクタリングし、代わりに動作を検証します:
リファクタリングの例

変更前(UI 中心):

test('dropdown opens when clicked', async () => {
  await page.getByTestId('model-dropdown').click();
  await expect(page.getByRole('listbox')).toBeVisible();
});

変更後(動作中心+BDD のネスト):

test.describe('when a model is selected from the dropdown', () => {
  test('updates and persists the agent configuration', async () => {
    // Open dropdown and select model
    await page.getByTestId('model-dropdown').click();
    await page.getByRole('option', { name: 'GPT-4' }).click();

    // Verify the selection persists and affects behavior
    await page.reload();
    await expect(page.getByTestId('model-dropdown')).toHaveText('GPT-4');

    // Optionally: verify the model is used in actual requests
    // (via request interception or checking response metadata)
  });
});

ステップ 6:動作テスト用のキッチンシンクフィクスチャ

フィクスチャには、単なるモックデータではなく、現実的なシナリオを表すものを用意します:

フィクスチャの命名規則
<feature>-<scenario>.fixture.ts

Examples:
- agent-with-tools.fixture.ts
- chat-multi-turn-context.fixture.ts
- workflow-parallel-execution.fixture.ts
- tool-validation-error.fixture.ts
- mcp-server-timeout.fixture.ts
フィクスチャの内容要件

各フィクスチャでは、次の内容を定義する必要があります:

  1. シナリオの説明(テスト可能にする動作)
  2. 期待される結果(パスするべきアサーション)
  3. 対象となるエッジケース(エラー状態、空の状態など)
// fixtures/agent-provider-switch.fixture.ts
export const agentProviderSwitch = {
  name: 'agent-provider-switch',
  description: 'Tests that switching LLM providers changes agent behavior',

  // Mock responses for different providers
  responses: {
    openai: { content: 'Response from OpenAI', model: 'gpt-4' },
    anthropic: { content: 'Response from Anthropic', model: 'claude-3' },
  },

  expectedBehavior: {
    // When provider is switched, subsequent messages use new provider
    providerSwitchAffectsNextMessage: true,
    // Provider selection persists across page reload
    providerPersistsOnReload: true,
  },
};

ステップ 7:実行して検証する

cd packages/playground && pnpm test:e2e
テスト品質チェックリスト

テストが完了したと判断する前に、次の点を確認してください:

  • 各テストに、ユーザーストーリーを明確に示すコメントがある
  • 外側に置く test.describe は 1 つで、テスト対象の単位に名前を付けている
  • すべての test が test.describe('when …') の前提条件ブロック内にネストされている(トップレベルにフラットな test() を置かない)
  • 各 test が、観測可能な結果をちょうど 1 つだけアサートしている
  • 中間的な UI 状態ではなく、結果を検証している
  • 機能が壊れた場合にテストが失敗する(UI が変更された場合にのみ失敗するのではない)
  • 該当する場合、page.reload() を使って永続性を検証している
  • エラーのシナリオを網羅している
  • 非同期処理に適切なタイムアウトを使用している
  • フィクスチャが現実的な使用シナリオを表している

クイックリファレンス

ステップコマンド/操作
ビルドpnpm build:cli
起動cd packages/playground/e2e/kitchen-sink && pnpm dev
アプリ URLhttp://localhost:4111
ルート@packages/playground/src/App.tsx
テスト実行cd packages/playground && pnpm test:e2e
テストディレクトリpackages/playground/e2e/tests/
フィクスチャpackages/playground/e2e/kitchen-sink/fixtures/

避けるべきアンチパターン

❌ しないこと✅ 代わりにすること
モーダルが開くことをテストするモーダルの操作が完了し、その結果が永続化されることをテストする
ボタンがクリック可能であることをテストするボタンのクリックで期待される結果が得られることをテストする
読み込みスピナーが表示されることをテストする読み込まれたデータが正しいことをテストする
フォームの検証メッセージが表示されることをテストする無効なフォームは送信できず、かつ有効なフォームは送信できることをテストする
ドロップダウンに選択肢があることをテストする選択肢を選ぶことでシステムの動作が変わることをテストする
サイドバーのナビゲーションが機能することをテストする遷移先のページに正しいデータ/機能があることをテストする
要素が表示されていることをアサートする要素に期待されるデータ/状態が含まれていることをアサートする
前提条件の describe がないトップレベルのフラットな test()すべての test を test.describe('when …') ブロック内にネストする
1 つの test() で複数の無関係な結果をアサートする観測可能な結果ごとに 1 つの test() を作成する

コメント