e2e-tester
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseE2E Testing with Playwright & Testcontainers
基于Playwright与Testcontainers的E2E测试
Write end-to-end tests using Playwright against a full Redpanda Console stack running in Docker containers via testcontainers.
通过Testcontainers在Docker容器中运行完整的Redpanda Console栈,并使用Playwright编写端到端测试。
When to Use This Skill
适用场景
- Testing 2+ step user journeys (login → action → verify)
- Multi-page workflows
- Browser automation with Playwright
NOT for: Component unit tests → use testing
- 测试包含2步及以上的用户流程(登录→操作→验证)
- 多页面工作流
- 基于Playwright的浏览器自动化
不适用场景: 组件单元测试 → 请使用测试
Critical Rules
核心规则
ALWAYS:
- Run before running E2E tests (frontend assets required)
bun run build - Use API for container management (never manual
testcontainerscommands in tests)docker - Test 2+ step user journeys (multi-page, multi-step scenarios)
- Use and
page.getByRole()selectors (avoid CSS selectors)page.getByLabel() - Add attributes to components when semantic selectors aren't available
data-testid - Use Task tool with MCP Playwright agents to analyze failures and get test status
- Use Task tool with Explore agent to find missing testids in UI components
- Clean up test data using to call cleanup API endpoints
afterEach
NEVER:
- Test UI component rendering (that belongs in unit/integration tests)
- Use brittle CSS selectors like or
.class-name#id - Use force:true when calling .click()
- Use waitForTimeout in e2e tests
- Hard-code wait times (use with conditions)
waitFor - Leave containers running after test failures
- Commit test screenshots to git (add to )
.gitignore - Add testids without understanding the component's purpose and context
必须遵守:
- 运行E2E测试前先执行(需要前端资源)
bun run build - 使用API管理容器(测试中禁止手动执行
testcontainers命令)docker - 测试包含2步及以上的用户流程(多页面、多步骤场景)
- 使用和
page.getByRole()选择器(避免CSS选择器)page.getByLabel() - 当语义化选择器不可用时,为组件添加属性
data-testid - 使用Task工具结合MCP Playwright代理分析失败原因并获取测试状态
- 使用Task工具结合Explore代理查找UI组件中缺失的testid
- 使用调用清理API端点来清除测试数据
afterEach
禁止操作:
- 测试UI组件渲染(此类测试属于单元/集成测试范畴)
- 使用脆弱的CSS选择器,如或
.class-name#id - 调用时使用force:true
.click() - 在E2E测试中使用waitForTimeout
- 硬编码等待时间(使用带条件的)
waitFor - 测试失败后让容器继续运行
- 将测试截图提交到git(添加到)
.gitignore - 在不理解组件用途和上下文的情况下添加testid
Test Architecture
测试架构
Stack Components
栈组件
OSS Mode ():
bun run e2e-test- Redpanda container (Kafka broker + Schema Registry + Admin API)
- Backend container (Go binary serving API + embedded frontend)
- OwlShop container (test data generator)
Enterprise Mode ():
bun run e2e-test-enterprise- Same as OSS + Enterprise features (RBAC, SSO, etc.)
- Requires repo checked out alongside
console-enterpriseconsole
Network Setup:
- All containers on shared Docker network
- Internal addresses: ,
redpanda:9092console-backend:3000 - External access: ,
localhost:19092localhost:3000
OSS模式():
bun run e2e-test- Redpanda容器(Kafka broker + Schema Registry + Admin API)
- 后端容器(提供API服务的Go二进制文件 + 嵌入式前端)
- OwlShop容器(测试数据生成器)
企业模式():
bun run e2e-test-enterprise- 包含OSS模式的所有组件 + 企业功能(RBAC、SSO等)
- 需要将代码仓与
console-enterprise代码仓放在同一目录下console
网络配置:
- 所有容器位于共享Docker网络
- 内部地址:、
redpanda:9092console-backend:3000 - 外部访问:、
localhost:19092localhost:3000
Test Container Lifecycle
测试容器生命周期
Setup (global-setup.mjs):
1. Build frontend (frontend/build/)
2. Copy frontend assets to backend/pkg/embed/frontend/
3. Build backend Docker image with testcontainers
4. Start Redpanda container with SASL auth
5. Start backend container serving frontend
6. Wait for services to be ready
Tests run...
Teardown (global-teardown.mjs):
1. Stop backend container
2. Stop Redpanda container
3. Remove Docker network
4. Clean up copied frontend assets初始化(global-setup.mjs):
1. 构建前端(frontend/build/)
2. 将前端资源复制到backend/pkg/embed/frontend/
3. 使用testcontainers构建后端Docker镜像
4. 启动带SASL认证的Redpanda容器
5. 启动提供前端服务的后端容器
6. 等待服务就绪
测试运行中...
清理(global-teardown.mjs):
1. 停止后端容器
2. 停止Redpanda容器
3. 删除Docker网络
4. 清理复制的前端资源Workflow
工作流
1. Prerequisites
1. 前置条件
bash
undefinedbash
undefinedBuild frontend (REQUIRED before E2E tests)
构建前端(运行E2E测试前必须执行!)
bun run build
bun run build
Verify Docker is running
验证Docker是否在运行
docker ps
undefineddocker ps
undefined2. Write Test
2. 编写测试
File location:
tests/<feature>/*.spec.tsStructure:
typescript
import { test, expect } from '@playwright/test';
test.describe('Feature Name', () => {
test('user can complete workflow', async ({ page }) => {
// Navigate to page
await page.goto('/feature');
// Interact with elements
await page.getByRole('button', { name: 'Create' }).click();
await page.getByLabel('Name').fill('test-item');
// Submit and verify
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Success')).toBeVisible();
// Verify navigation or state change
await expect(page).toHaveURL(/\/feature\/test-item/);
});
});文件位置:
tests/<feature>/*.spec.ts结构:
typescript
import { test, expect } from '@playwright/test';
test.describe('功能名称', () => {
test('用户可完成完整工作流', async ({ page }) => {
// 导航到页面
await page.goto('/feature');
// 与元素交互
await page.getByRole('button', { name: '创建' }).click();
await page.getByLabel('名称').fill('test-item');
// 提交并验证
await page.getByRole('button', { name: '提交' }).click();
await expect(page.getByText('成功')).toBeVisible();
// 验证导航或状态变更
await expect(page).toHaveURL(/\/feature\/test-item/);
});
});3. Selectors Best Practices
3. 选择器最佳实践
Prefer accessibility selectors:
typescript
// ✅ GOOD: Role-based (accessible)
page.getByRole('button', { name: 'Create Topic' })
page.getByLabel('Topic Name')
page.getByText('Success message')
// ✅ GOOD: Test IDs when role isn't clear
page.getByTestId('topic-list-item')
// ❌ BAD: CSS selectors (brittle)
page.locator('.btn-primary')
page.locator('#topic-name-input')Add test IDs to components:
typescript
// In React component
<Button data-testid="create-topic-button">
Create Topic
</Button>
// In test
await page.getByTestId('create-topic-button').click();优先使用无障碍选择器:
typescript
// ✅ 推荐:基于角色(无障碍)
page.getByRole('button', { name: '创建Topic' })
page.getByLabel('Topic名称')
page.getByText('成功提示')
// ✅ 推荐:当角色不明确时使用Test ID
page.getByTestId('topic-list-item')
// ❌ 不推荐:CSS选择器(脆弱)
page.locator('.btn-primary')
page.locator('#topic-name-input')为组件添加Test ID:
typescript
// 在React组件中
<Button data-testid="create-topic-button">
创建Topic
</Button>
// 在测试中
await page.getByTestId('create-topic-button').click();4. Async Operations
4. 异步操作
typescript
// ✅ GOOD: Wait for specific condition
await expect(page.getByRole('status')).toHaveText('Ready');
// ✅ GOOD: Wait for navigation
await page.waitForURL('**/topics/my-topic');
// ✅ GOOD: Wait for API response
await page.waitForResponse(resp =>
resp.url().includes('/api/topics') && resp.status() === 200
);
// ❌ BAD: Fixed timeouts
await page.waitForTimeout(5000);typescript
// ✅ 推荐:等待特定条件
await expect(page.getByRole('status')).toHaveText('就绪');
// ✅ 推荐:等待导航完成
await page.waitForURL('**/topics/my-topic');
// ✅ 推荐:等待API响应
await page.waitForResponse(resp =>
resp.url().includes('/api/topics') && resp.status() === 200
);
// ❌ 不推荐:固定超时
await page.waitForTimeout(5000);5. Authentication
5. 身份验证
OSS Mode: No authentication required
Enterprise Mode: Basic auth with
e2euser:very-secrettypescript
test.use({
httpCredentials: {
username: 'e2euser',
password: 'very-secret',
},
});OSS模式: 无需身份验证
企业模式: 使用基础认证,账号
e2euser:very-secrettypescript
test.use({
httpCredentials: {
username: 'e2euser',
password: 'very-secret',
},
});6. Run Tests
6. 运行测试
bash
undefinedbash
undefinedOSS tests
OSS测试
bun run build # Build frontend first!
bun run e2e-test # Run all OSS tests
bun run build # 先构建前端!
bun run e2e-test # 运行所有OSS测试
Enterprise tests (requires console-enterprise repo)
企业测试(需要console-enterprise代码仓)
bun run build
bun run e2e-test-enterprise
bun run build
bun run e2e-test-enterprise
UI mode (debugging)
UI模式(调试用)
bun run e2e-test:ui
bun run e2e-test:ui
Specific test file
运行特定测试文件
bun run e2e-test tests/topics/create-topic.spec.ts
bun run e2e-test tests/topics/create-topic.spec.ts
Update snapshots
更新快照
bun run e2e-test --update-snapshots
undefinedbun run e2e-test --update-snapshots
undefined7. Debugging
7. 调试
Failed test debugging:
bash
undefined测试失败调试:
bash
undefinedCheck container logs
查看容器日志
docker ps -a | grep console-backend
docker logs <container-id>
docker ps -a | grep console-backend
docker logs <container-id>
Check if services are accessible
检查服务是否可访问
curl http://localhost:3000
curl http://localhost:19092
curl http://localhost:3000
curl http://localhost:19092
Run with debug output
带调试输出运行
DEBUG=pw:api bun run e2e-test
DEBUG=pw:api bun run e2e-test
Keep containers running on failure
测试失败后保持容器运行
TESTCONTAINERS_RYUK_DISABLED=true bun run e2e-test
**Playwright debugging tools:**
```typescript
// Add to test for debugging
await page.pause(); // Opens Playwright Inspector
// Screenshot on failure (automatic in config)
await page.screenshot({ path: 'debug.png' });
// Get page content for debugging
console.log(await page.content());TESTCONTAINERS_RYUK_DISABLED=true bun run e2e-test
**Playwright调试工具:**
```typescript
// 在测试中添加以下代码进行调试
await page.pause(); // 打开Playwright Inspector
// 失败时自动截图(已在配置中启用)
await page.screenshot({ path: 'debug.png' });
// 获取页面内容用于调试
console.log(await page.content());Common Patterns
常见模式
Multi-Step Workflows
多步骤工作流
typescript
test('user creates, configures, and tests topic', async ({ page }) => {
// Step 1: Navigate and create
await page.goto('/topics');
await page.getByRole('button', { name: 'Create Topic' }).click();
// Step 2: Fill form
await page.getByLabel('Topic Name').fill('test-topic');
await page.getByLabel('Partitions').fill('3');
await page.getByRole('button', { name: 'Create' }).click();
// Step 3: Verify creation
await expect(page.getByText('Topic created successfully')).toBeVisible();
await expect(page).toHaveURL(/\/topics\/test-topic/);
// Step 4: Configure topic
await page.getByRole('button', { name: 'Configure' }).click();
await page.getByLabel('Retention Hours').fill('24');
await page.getByRole('button', { name: 'Save' }).click();
// Step 5: Verify configuration
await expect(page.getByText('Configuration saved')).toBeVisible();
});typescript
test('用户创建、配置并测试Topic', async ({ page }) => {
// 步骤1:导航并创建
await page.goto('/topics');
await page.getByRole('button', { name: '创建Topic' }).click();
// 步骤2:填写表单
await page.getByLabel('Topic名称').fill('test-topic');
await page.getByLabel('分区数').fill('3');
await page.getByRole('button', { name: '创建' }).click();
// 步骤3:验证创建结果
await expect(page.getByText('Topic创建成功')).toBeVisible();
await expect(page).toHaveURL(/\/topics\/test-topic/);
// 步骤4:配置Topic
await page.getByRole('button', { name: '配置' }).click();
await page.getByLabel('保留时长(小时)').fill('24');
await page.getByRole('button', { name: '保存' }).click();
// 步骤5:验证配置结果
await expect(page.getByText('配置已保存')).toBeVisible();
});Testing Forms
表单测试
typescript
test('form validation works correctly', async ({ page }) => {
await page.goto('/create-topic');
// Submit empty form - should show errors
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.getByText('Name is required')).toBeVisible();
// Fill valid data - should succeed
await page.getByLabel('Topic Name').fill('valid-topic');
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.getByText('Success')).toBeVisible();
});typescript
test('表单验证功能正常', async ({ page }) => {
await page.goto('/create-topic');
// 提交空表单 - 应显示错误
await page.getByRole('button', { name: '创建' }).click();
await expect(page.getByText('名称为必填项')).toBeVisible();
// 填写有效数据 - 应成功
await page.getByLabel('Topic名称').fill('valid-topic');
await page.getByRole('button', { name: '创建' }).click();
await expect(page.getByText('成功')).toBeVisible();
});Testing Data Tables
数据表测试
typescript
test('user can filter and sort topics', async ({ page }) => {
await page.goto('/topics');
// Filter
await page.getByPlaceholder('Search topics').fill('test-');
await expect(page.getByRole('row')).toHaveCount(3); // Header + 2 results
// Sort
await page.getByRole('columnheader', { name: 'Name' }).click();
const firstRow = page.getByRole('row').nth(1);
await expect(firstRow).toContainText('test-topic-a');
});typescript
test('用户可筛选和排序Topic', async ({ page }) => {
await page.goto('/topics');
// 筛选
await page.getByPlaceholder('搜索Topic').fill('test-');
await expect(page.getByRole('row')).toHaveCount(3); // 表头 + 2条结果
// 排序
await page.getByRole('columnheader', { name: '名称' }).click();
const第一行 = page.getByRole('row').nth(1);
await expect(第一行).toContainText('test-topic-a');
});API Interactions
API交互测试
typescript
test('creating topic triggers backend API', async ({ page }) => {
// Listen for API call
const apiPromise = page.waitForResponse(
resp => resp.url().includes('/api/topics') && resp.status() === 201
);
// Trigger action
await page.goto('/topics');
await page.getByRole('button', { name: 'Create Topic' }).click();
await page.getByLabel('Name').fill('api-test-topic');
await page.getByRole('button', { name: 'Create' }).click();
// Verify API was called
const response = await apiPromise;
const body = await response.json();
expect(body.name).toBe('api-test-topic');
});typescript
test('创建Topic会触发后端API调用', async ({ page }) => {
// 监听API调用
const apiPromise = page.waitForResponse(
resp => resp.url().includes('/api/topics') && resp.status() === 201
);
// 触发操作
await page.goto('/topics');
await page.getByRole('button', { name: '创建Topic' }).click();
await page.getByLabel('名称').fill('api-test-topic');
await page.getByRole('button', { name: '创建' }).click();
// 验证API已被调用
const response = await apiPromise;
const body = await response.json();
expect(body.name).toBe('api-test-topic');
});Testcontainers Setup
Testcontainers配置
Frontend Asset Copy (Required)
前端资源复制(必填)
The backend Docker image needs frontend assets embedded at build time:
typescript
// In global-setup.mjs
async function buildBackendImage(isEnterprise) {
// Copy frontend build to backend embed directory
const frontendBuildDir = resolve(__dirname, '../build');
const embedDir = join(backendDir, 'pkg/embed/frontend');
await execAsync(`cp -r "${frontendBuildDir}"/* "${embedDir}"/`);
// Build Docker image using testcontainers
// Docker doesn't allow referencing files outside build context,
// so we temporarily copy the Dockerfile into the build context
const tempDockerfile = join(backendDir, '.dockerfile.e2e.tmp');
await execAsync(`cp "${dockerfilePath}" "${tempDockerfile}"`);
try {
await GenericContainer
.fromDockerfile(backendDir, '.dockerfile.e2e.tmp')
.build(imageTag, { deleteOnExit: false });
} finally {
await execAsync(`rm -f "${tempDockerfile}"`).catch(() => {});
await execAsync(`find "${embedDir}" -mindepth 1 ! -name '.gitignore' -delete`).catch(() => {});
}
}后端Docker镜像在构建时需要嵌入前端资源:
typescript
// 在global-setup.mjs中
async function buildBackendImage(isEnterprise) {
// 将前端构建产物复制到后端嵌入目录
const frontendBuildDir = resolve(__dirname, '../build');
const embedDir = join(backendDir, 'pkg/embed/frontend');
await execAsync(`cp -r "${frontendBuildDir}"/* "${embedDir}"/`);
// 使用Testcontainers构建Docker镜像
// Docker不允许引用构建上下文外的文件,因此我们临时将Dockerfile复制到构建上下文
const tempDockerfile = join(backendDir, '.dockerfile.e2e.tmp');
await execAsync(`cp "${dockerfilePath}" "${tempDockerfile}"`);
try {
await GenericContainer
.fromDockerfile(backendDir, '.dockerfile.e2e.tmp')
.build(imageTag, { deleteOnExit: false });
} finally {
await execAsync(`rm -f "${tempDockerfile}"`).catch(() => {});
await execAsync(`find "${embedDir}" -mindepth 1 ! -name '.gitignore' -delete`).catch(() => {});
}
}Container Configuration
容器配置
Backend container:
typescript
const backend = await new GenericContainer(imageTag)
.withNetwork(network)
.withNetworkAliases('console-backend')
.withExposedPorts({ container: 3000, host: 3000 })
.withBindMounts([{
source: configPath,
target: '/etc/console/config.yaml'
}])
.withCommand(['--config.filepath=/etc/console/config.yaml'])
.start();Redpanda container:
typescript
const redpanda = await new GenericContainer('redpandadata/redpanda:v25.2.1')
.withNetwork(network)
.withNetworkAliases('redpanda')
.withExposedPorts(
{ container: 19_092, host: 19_092 }, // Kafka
{ container: 18_081, host: 18_081 }, // Schema Registry
{ container: 9644, host: 19_644 } // Admin API
)
.withEnvironment({ RP_BOOTSTRAP_USER: 'e2euser:very-secret' })
.withHealthCheck({
test: ['CMD-SHELL', "rpk cluster health | grep -E 'Healthy:.+true' || exit 1"],
interval: 15_000,
retries: 5
})
.withWaitStrategy(Wait.forHealthCheck())
.start();后端容器:
typescript
const backend = await new GenericContainer(imageTag)
.withNetwork(network)
.withNetworkAliases('console-backend')
.withExposedPorts({ container: 3000, host: 3000 })
.withBindMounts([{
source: configPath,
target: '/etc/console/config.yaml'
}])
.withCommand(['--config.filepath=/etc/console/config.yaml'])
.start();Redpanda容器:
typescript
const redpanda = await new GenericContainer('redpandadata/redpanda:v25.2.1')
.withNetwork(network)
.withNetworkAliases('redpanda')
.withExposedPorts(
{ container: 19_092, host: 19_092 }, // Kafka
{ container: 18_081, host: 18_081 }, // Schema Registry
{ container: 9644, host: 19_644 } // Admin API
)
.withEnvironment({ RP_BOOTSTRAP_USER: 'e2euser:very-secret' })
.withHealthCheck({
test: ['CMD-SHELL', "rpk cluster health | grep -E 'Healthy:.+true' || exit 1"],
interval: 15_000,
retries: 5
})
.withWaitStrategy(Wait.forHealthCheck())
.start();CI Integration
CI集成
GitHub Actions Setup
GitHub Actions配置
yaml
e2e-test:
runs-on: ubuntu-latest-8
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build frontend
run: |
REACT_APP_CONSOLE_GIT_SHA=$(echo $GITHUB_SHA | cut -c 1-6)
bun run build
- name: Install Playwright browsers
run: bun run install:chromium
- name: Run E2E tests
run: bun run e2e-test
- name: Upload test report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: frontend/playwright-report/yaml
e2e-test:
runs-on: ubuntu-latest-8
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
- name: 安装依赖
run: bun install --frozen-lockfile
- name: 构建前端
run: |
REACT_APP_CONSOLE_GIT_SHA=$(echo $GITHUB_SHA | cut -c 1-6)
bun run build
- name: 安装Playwright浏览器
run: bun run install:chromium
- name: 运行E2E测试
run: bun run e2e-test
- name: 上传测试报告
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: frontend/playwright-report/Test ID Management
Test ID管理
Finding Missing Test IDs
查找缺失的Test ID
Use the Task tool with Explore agent to systematically find missing testids:
Use Task tool with:
subagent_type: Explore
prompt: Search through [feature] UI components and identify all interactive
elements (buttons, inputs, links, selects) missing data-testid attributes.
List with file:line, element type, purpose, and suggested testid name.Example output:
schema-list.tsx:207 - button - "Edit compatibility" - schema-edit-compatibility-btn
schema-list.tsx:279 - button - "Create new schema" - schema-create-new-btn
schema-details.tsx:160 - button - "Edit compatibility" - schema-details-edit-compatibility-btn使用Task工具结合Explore代理系统性查找缺失的testid:
使用Task工具:
subagent_type: Explore
prompt: 搜索[功能]相关的UI组件,识别所有缺失data-testid属性的交互元素(按钮、输入框、链接、选择器)。列出文件:行号、元素类型、用途及建议的testid名称。示例输出:
schema-list.tsx:207 - button - "编辑兼容性" - schema-edit-compatibility-btn
schema-list.tsx:279 - button - "创建新Schema" - schema-create-new-btn
schema-details.tsx:160 - button - "编辑兼容性" - schema-details-edit-compatibility-btnAdding Test IDs
添加Test ID
Naming Convention:
- Use kebab-case:
data-testid="feature-action-element" - Be specific: Include feature name + action + element type
- For dynamic items: Use template literals item-delete-${id}`}`
data-testid={\
Examples:
tsx
// ✅ GOOD: Specific button action
<Button data-testid="schema-create-new-btn" onClick={onCreate}>
Create new schema
</Button>
// ✅ GOOD: Form input with context
<Input
data-testid="schema-subject-name-input"
placeholder="Subject name"
/>
// ✅ GOOD: Table row with dynamic ID
<TableRow data-testid={`schema-row-${schema.name}`}>
{schema.name}
</TableRow>
// ✅ GOOD: Delete button in list
<IconButton
data-testid={`schema-delete-btn-${schema.name}`}
icon={<TrashIcon />}
onClick={() => deleteSchema(schema.name)}
/>
// ❌ BAD: Too generic
<Button data-testid="button">Create</Button>
// ❌ BAD: Using CSS classes as identifiers
<Button className="create-btn">Create</Button>Where to Add:
- Primary actions: Create, Save, Delete, Edit, Submit, Cancel buttons
- Navigation: Links to detail pages, breadcrumbs
- Forms: All input fields, selects, checkboxes, radio buttons
- Lists/Tables: Row identifiers, action buttons within rows
- Dialogs/Modals: Open/close buttons, form elements inside
- Search/Filter: Search inputs, filter dropdowns, clear buttons
Process:
- Use Task/Explore to find missing testids in target feature
- Read the component file to understand context
- Add following naming convention
data-testid - Update tests to use new testids
- Run tests to verify selectors work
命名规范:
- 使用短横线分隔(kebab-case):
data-testid="feature-action-element" - 保持具体:包含功能名称 + 操作 + 元素类型
- 动态项:使用模板字符串 item-delete-${id}`}`
data-testid={\
示例:
tsx
// ✅ 推荐:明确的按钮操作
<Button data-testid="schema-create-new-btn" onClick={onCreate}>
创建新Schema
</Button>
// ✅ 推荐:带上下文的表单输入
<Input
data-testid="schema-subject-name-input"
placeholder="Subject名称"
/>
// ✅ 推荐:带动态ID的数据表行
<TableRow data-testid={`schema-row-${schema.name}`}>
{schema.name}
</TableRow>
// ✅ 推荐:列表中的删除按钮
<IconButton
data-testid={`schema-delete-btn-${schema.name}`}
icon={<TrashIcon />}
onClick={() => deleteSchema(schema.name)}
/>
// ❌ 不推荐:过于通用
<Button data-testid="button">创建</Button>
// ❌ 不推荐:使用CSS类作为标识符
<Button className="create-btn">创建</Button>添加位置:
- 核心操作:创建、保存、删除、编辑、提交、取消按钮
- 导航元素:详情页链接、面包屑
- 表单元素:所有输入框、选择器、复选框、单选按钮
- 列表/数据表:行标识符、行内操作按钮
- 对话框/模态框:打开/关闭按钮、内部表单元素
- 搜索/筛选:搜索输入框、筛选下拉框、清除按钮
流程:
- 使用Task/Explore查找目标功能中缺失的testid
- 读取组件文件以理解上下文
- 按照命名规范添加
data-testid - 更新测试以使用新的testid
- 运行测试验证选择器是否生效
Analyzing Test Failures
测试失败分析
Using MCP Playwright Agents
使用MCP Playwright代理
Check Test Status:
typescript
// Use mcp__playwright-test__test_list to see all tests
// Use mcp__playwright-test__test_run to get detailed results
// Use mcp__playwright-test__test_debug to analyze specific failures查看测试状态:
typescript
// 使用mcp__playwright-test__test_list查看所有测试
// 使用mcp__playwright-test__test_run获取详细结果
// 使用mcp__playwright-test__test_debug分析特定失败Reading Playwright Logs
读取Playwright日志
Common failure patterns and fixes:
常见失败模式及修复方案:
1. Element Not Found
1. 元素未找到
Error: locator.click: Target closed
Error: Timeout 30000ms exceeded waiting for locatorAnalysis steps:
- Check if element has correct testid/role
- Verify element is visible (not hidden/collapsed)
- Check for timing issues (element loads async)
- Look for dynamic content that changes selector
Fix:
typescript
// ❌ BAD: Element might not be loaded
await page.getByRole('button', { name: 'Create' }).click();
// ✅ GOOD: Wait for element to be visible
await expect(page.getByRole('button', { name: 'Create' })).toBeVisible();
await page.getByRole('button', { name: 'Create' }).click();
// ✅ BETTER: Add testid for stability
await page.getByTestId('create-button').click();Error: locator.click: Target closed
Error: Timeout 30000ms exceeded waiting for locator分析步骤:
- 检查元素是否有正确的testid/角色
- 验证元素是否可见(未被隐藏/折叠)
- 检查时序问题(元素异步加载)
- 查找是否有动态内容变更了选择器
修复方案:
typescript
// ❌ 不推荐:元素可能未加载完成
await page.getByRole('button', { name: '创建' }).click();
// ✅ 推荐:等待元素可见
await expect(page.getByRole('button', { name: '创建' })).toBeVisible();
await page.getByRole('button', { name: '创建' }).click();
// ✅ 更优:使用稳定的testid
await page.getByTestId('create-button').click();2. Selector Ambiguity
2. 选择器歧义
Error: strict mode violation: locator('button') resolved to 3 elementsAnalysis:
- Multiple elements match the selector
- Need more specific selector or testid
Fix:
typescript
// ❌ BAD: Multiple "Edit" buttons on page
await page.getByRole('button', { name: 'Edit' }).click();
// ✅ GOOD: More specific with testid
await page.getByTestId('schema-edit-compatibility-btn').click();
// ✅ GOOD: Scope within container
await page.getByRole('region', { name: 'Schema Details' })
.getByRole('button', { name: 'Edit' }).click();Error: strict mode violation: locator('button') resolved to 3 elements分析:
- 多个元素匹配选择器
- 需要更具体的选择器或testid
修复方案:
typescript
// ❌ 不推荐:页面上有多个“编辑”按钮
await page.getByRole('button', { name: '编辑' }).click();
// ✅ 推荐:使用更具体的testid
await page.getByTestId('schema-edit-compatibility-btn').click();
// ✅ 推荐:在容器范围内查找
await page.getByRole('region', { name: 'Schema详情' })
.getByRole('button', { name: '编辑' }).click();3. Timing/Race Conditions
3. 时序/竞争条件
Error: expect(locator).toHaveText()
Expected string: "Success"
Received string: "Loading..."Analysis:
- Test assertion ran before UI updated
- Need to wait for specific state
Fix:
typescript
// ❌ BAD: Doesn't wait for state change
await page.getByRole('button', { name: 'Save' }).click();
expect(page.getByText('Success')).toBeVisible();
// ✅ GOOD: Wait for the expected state
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Success')).toBeVisible({ timeout: 5000 });Error: expect(locator).toHaveText()
Expected string: "成功"
Received string: "加载中..."分析:
- 测试断言在UI更新前执行
- 需要等待特定状态
修复方案:
typescript
// ❌ 不推荐:未等待状态变更
await page.getByRole('button', { name: '保存' }).click();
expect(page.getByText('成功')).toBeVisible();
// ✅ 推荐:等待预期状态
await page.getByRole('button', { name: '保存' }).click();
await expect(page.getByText('成功')).toBeVisible({ timeout: 5000 });4. Navigation Issues
4. 导航问题
Error: page.goto: net::ERR_CONNECTION_REFUSEDAnalysis:
- Backend/frontend not running
- Wrong URL or port
Fix:
bash
undefinedError: page.goto: net::ERR_CONNECTION_REFUSED分析:
- 后端/前端未运行
- URL或端口错误
修复方案:
bash
undefinedCheck containers are running
检查容器是否运行
docker ps | grep console-backend
docker ps | grep console-backend
Check container logs
查看容器日志
docker logs <container-id>
docker logs <container-id>
Verify port mapping
验证端口映射
Check testcontainer state file
检查testcontainer状态文件
cat .testcontainers-state.json
undefinedcat .testcontainers-state.json
undefinedSystematic Failure Analysis Workflow
系统性失败分析流程
When tests fail:
-
Get Test Results
Use mcp__playwright-test__test_run or check console output Identify which tests failed and error messages -
Analyze Error Patterns
- Selector not found → Missing/wrong testid or element not visible
- Strict mode violation → Need more specific selector
- Timeout → Element loads async, need waitFor
- Connection refused → Container/service not running
-
Find Missing Test IDs
Use Task tool with Explore agent to find missing testids in the components related to failed tests -
Add Test IDs
- Read component file
- Add to problematic elements
data-testid - Follow naming convention
- Format with biome
-
Update Tests
- Replace brittle selectors with stable testids
- Add proper wait conditions
- Verify with test run
-
Verify Fixes
Run specific test file to verify fix Run full suite to ensure no regressions
当测试失败时:
-
获取测试结果
使用mcp__playwright-test__test_run或查看控制台输出 确定哪些测试失败及错误信息 -
分析错误模式
- 选择器未找到 → testid缺失/错误或元素不可见
- 严格模式冲突 → 需要更具体的选择器
- 超时 → 元素异步加载,需使用waitFor
- 连接被拒绝 → 容器/服务未运行
-
查找缺失的Test ID
使用Task工具结合Explore代理查找与失败测试相关组件中缺失的testid -
添加Test ID
- 读取组件文件
- 为问题元素添加
data-testid - 遵循命名规范
- 使用biome格式化代码
-
更新测试
- 用稳定的testid替换脆弱的选择器
- 添加合适的等待条件
- 运行测试验证
-
验证修复
运行特定测试文件验证修复效果 运行完整测试套件确保无回归
Troubleshooting
故障排除
Container Fails to Start
容器启动失败
bash
undefinedbash
undefinedCheck if frontend build exists
检查前端构建产物是否存在
ls frontend/build/
ls frontend/build/
Check if Docker image built successfully
检查Docker镜像是否构建成功
docker images | grep console-backend
docker images | grep console-backend
Check container logs
查看容器日志
docker logs <container-id>
docker logs <container-id>
Verify Docker network
验证Docker网络
docker network ls | grep testcontainers
undefineddocker network ls | grep testcontainers
undefinedTest Timeout Issues
测试超时问题
typescript
// Increase timeout for slow operations
test('slow operation', async ({ page }) => {
test.setTimeout(60000); // 60 seconds
await page.goto('/slow-page');
await expect(page.getByText('Loaded')).toBeVisible({ timeout: 30000 });
});typescript
// 为慢操作增加超时时间
test('慢操作', async ({ page }) => {
test.setTimeout(60000); // 60秒
await page.goto('/slow-page');
await expect(page.getByText('已加载')).toBeVisible({ timeout: 30000 });
});Port Already in Use
端口已被占用
bash
undefinedbash
undefinedFind and kill process using port 3000
查找并杀死占用3000端口的进程
lsof -ti:3000 | xargs kill -9
lsof -ti:3000 | xargs kill -9
Or use different ports in test config
或在测试配置中使用其他端口
undefinedundefinedQuick Reference
快速参考
Test types:
- E2E tests (): Complete user workflows, browser interactions
*.spec.ts - Integration tests (): Component + API, no browser
*.test.tsx - Unit tests (): Pure logic, utilities
*.test.ts
Commands:
bash
bun run build # Build frontend (REQUIRED first!)
bun run e2e-test # Run OSS E2E tests
bun run e2e-test-enterprise # Run Enterprise E2E tests
bun run e2e-test:ui # Playwright UI mode (debugging)Selector priority:
- - Best for accessibility
getByRole() - - For form inputs
getByLabel() - - For content verification
getByText() - - When semantic selectors aren't clear
getByTestId() - CSS selectors - Avoid if possible
Wait strategies:
- - Navigation complete
waitForURL() - - API call finished
waitForResponse() - with
waitFor()- Element state changedexpect() - Never use fixed unless absolutely necessary
waitForTimeout()
测试类型:
- E2E测试():完整用户流程、浏览器交互
*.spec.ts - 集成测试():组件+API,无浏览器
*.test.tsx - 单元测试():纯逻辑、工具函数
*.test.ts
命令:
bash
bun run build # 构建前端(必须先执行!)
bun run e2e-test # 运行OSS E2E测试
bun run e2e-test-enterprise # 运行企业E2E测试
bun run e2e-test:ui # Playwright UI模式(调试)选择器优先级:
- - 最佳无障碍选择器
getByRole() - - 表单输入框专用
getByLabel() - - 内容验证
getByText() - - 语义化选择器不可用时使用
getByTestId() - CSS选择器 - 尽可能避免
等待策略:
- - 导航完成
waitForURL() - - API调用完成
waitForResponse() - 带的
expect()- 元素状态变更waitFor() - 除非万不得已,否则绝不使用固定的
waitForTimeout()
Output
输出
After completing work:
- Confirm frontend build succeeded
- Verify all E2E tests pass
- Note any new test IDs added to components
- Mention cleanup of test containers
- Report test execution time and coverage
完成工作后:
- 确认前端构建成功
- 验证所有E2E测试通过
- 记录添加到组件中的新Test ID
- 提及测试容器的清理情况
- 报告测试执行时间和覆盖率