mcpField
E
e2e-tests-studio

e2e-tests-studio

github.com/mastra-ai测试与质量29.2k2.5k 次安装今天更新
代码仓库

关于 e2e-tests-studio

e2e-tests-studio 是用于编写和验证前端改动 Playwright 端到端测试的技能。它强调用户可观察到的产品行为、规范的 BDD 测试结构和真实场景夹具,而非只检查界面状态。

主要功能

  1. 01围绕产品行为设计测试
  2. 02规范三级 BDD 结构
  3. 03将功能映射为可观察结果
  4. 04指导编写真机场景夹具
  5. 05覆盖持久化与错误恢复
  6. 06提供构建和测试命令

适用场景

  1. 01测试 Agent 配置是否影响响应
  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 提供方后,Agent 会使用该提供方"
  • ✅ "创建新 Agent 后会持久化保存,并显示在 Agent 列表中"
  • ✅ "使用参数运行工具会返回预期输出"
  • ✅ "聊天消息能正确流式输出,并保持对话上下文"
  • ✅ "工作流执行会按正确顺序触发工具"

BDD 结构(必须遵循)

每个 E2E 规范都必须遵循与 MSW 测试相同的 BDD 结构。 在 packages/playground 中,e2e-bdd/test-needs-when-describe 会强制执行该结构。

该结构只有三层:

  1. 外层 test.describe:被测单元(每个文件对应一个页面或功能)。
  2. 内层 test.describe('when …'):恰好一个前置条件。标题必须以 when 开头。
  3. 每个 test:恰好一个可观察的结果。
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,用于命名被测单元。
  • 每个叶子 test 都必须位于某个 test.describe('when …') 前置条件分组内。不允许顶层扁平的 test()。
  • 仅当多个断言代表不同结果时才拆分多断言的 test();紧密耦合、用于证明同一结果的断言应保持在一起。绝不能丢弃任何断言。
  • 将 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 步:将功能映射为行为测试

功能到测试的映射指南
功能类别测试内容示例断言
Agent 配置配置变更会影响 Agent 行为发送消息 → 验证响应使用了所选模型
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 步:用于行为测试的全能型 Fixture

Fixture 应体现真实场景,而不仅仅是模拟数据:

Fixture 命名约定
<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
Fixture 内容要求

每个 Fixture 必须定义:

  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 命名被测单元
  • 每个 test 都嵌套在 test.describe('when …') 前置条件块中(不存在扁平的顶层 test())
  • 每个 test 只断言一个可观察的结果
  • 测试验证的是结果,而非中间 UI 状态
  • 如果功能出现故障,测试会失败(而不只是 UI 发生变化)
  • 在适用的情况下,通过 page.reload() 验证持久化
  • 覆盖了错误场景
  • 测试为异步操作使用了合适的超时时间
  • Fixture 体现真实的使用场景

快速参考

步骤命令/操作
构建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/
Fixturepackages/playground/e2e/kitchen-sink/fixtures/

应避免的反模式

❌ 不要✅ 应当改为
测试模态框是否打开测试模态框操作能否完成并被持久化
测试按钮是否可点击测试点击按钮是否产生预期结果
测试加载动画是否出现测试加载后的数据是否正确
测试表单校验消息是否显示测试无效表单无法提交,且有效表单能够成功提交
测试下拉框是否有选项测试选择某个选项是否改变系统行为
测试侧边栏导航是否可用测试导航后的页面是否具有正确的数据和功能
断言元素可见断言元素包含预期的数据或状态
顶层扁平 test(),没有前置条件 describe将每个 test 嵌套在 test.describe('when …') 块中
一个 test() 断言多个互不相关的结果每个可观察结果对应一个 test()

评论

支持 Markdown