Files
novalon-manage-system/novalon-manage-web/e2e/SELECTOR_OPTIMIZATION_GUIDE.md
T
张翔 e2ad1331cc feat: 添加测试框架和覆盖率报告功能
feat(测试): 新增Playwright和Vitest测试配置
feat(测试): 添加测试覆盖率报告生成功能
feat(测试): 实现前后端测试脚本集成

fix(测试): 修复测试密码不匹配问题
fix(测试): 修正URL等待策略
fix(测试): 调整错误消息选择器

refactor(测试): 重构测试目录结构
refactor(测试): 优化测试用例组织方式

docs: 更新测试报告文档
docs: 添加测试覆盖率报告模板

ci: 添加Docker测试环境配置
ci: 实现测试自动化脚本

chore: 更新依赖版本
chore: 添加测试相关配置文件
2026-03-25 09:03:37 +08:00

438 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 测试选择器优化指南
## 概述
本指南提供了在Playwright测试中使用稳定选择器的最佳实践,以提高测试的可靠性和可维护性。
## 选择器优先级
### 1. 推荐的选择器(最稳定)
#### 1.1 使用data-testid属性
```typescript
// ✅ 推荐:使用data-testid
await page.getByTestId('submit-button').click();
await page.getByTestId('username-input').fill('admin');
// ❌ 不推荐:使用CSS类名
await page.click('.btn-primary');
await page.fill('.username-input', 'admin');
```
**在前端添加data-testid**
```vue
<template>
<el-button data-testid="submit-button" type="primary">提交</el-button>
<el-input data-testid="username-input" v-model="username" />
</template>
```
#### 1.2 使用角色和文本
```typescript
// ✅ 推荐:使用角色和文本
await page.getByRole('button', { name: '提交' }).click();
await page.getByRole('textbox', { name: '用户名' }).fill('admin');
// ❌ 不推荐:使用CSS选择器
await page.click('button[type="submit"]');
await page.fill('input[placeholder="用户名"]', 'admin');
```
#### 1.3 使用文本内容
```typescript
// ✅ 推荐:使用文本内容
await page.getByText('登录').click();
await page.getByText('用户管理').click();
// ❌ 不推荐:使用CSS选择器
await page.click('.login-button');
await page.click('.user-management-link');
```
### 2. 可接受的选择器(中等稳定性)
#### 2.1 使用ARIA属性
```typescript
// ✅ 可接受:使用ARIA属性
await page.getByLabel('用户名').fill('admin');
await page.getByPlaceholder('请输入用户名').fill('admin');
await page.getByAltText('Logo').click();
```
#### 2.2 使用表单属性
```typescript
// ✅ 可接受:使用表单属性
await page.getByTitle('提交').click();
await page.getByTestId('username').fill('admin');
```
### 3. 不推荐的选择器(稳定性差)
#### 3.1 避免使用CSS类名
```typescript
// ❌ 不推荐:CSS类名可能变化
await page.click('.el-button--primary');
await page.fill('.el-input__inner', 'admin');
```
#### 3.2 避免使用复杂的CSS选择器
```typescript
// ❌ 不推荐:复杂选择器难以维护
await page.click('.el-form > .el-form-item > .el-form-item__content > .el-button');
await page.fill('div.el-input > div.el-input__wrapper > input', 'admin');
```
#### 3.3 避免使用索引
```typescript
// ❌ 不推荐:索引不稳定
await page.click('button:nth-child(2)');
await page.fill('input:nth-of-type(1)', 'admin');
```
## 选择器优化示例
### 示例1:登录页面
#### 优化前
```typescript
await page.click('.el-button--primary');
await page.fill('.el-input__inner', 'admin');
await page.fill('.el-input__inner', 'admin123');
```
#### 优化后
```typescript
// 在前端添加data-testid
// <el-button data-testid="login-button">登录</el-button>
// <el-input data-testid="username-input" placeholder="用户名" />
// <el-input data-testid="password-input" type="password" placeholder="密码" />
await page.getByTestId('login-button').click();
await page.getByTestId('username-input').fill('admin');
await page.getByTestId('password-input').fill('admin123');
```
### 示例2:用户管理页面
#### 优化前
```typescript
await page.click('.el-table__body tr:first-child .edit-button');
await page.click('.el-dialog__footer button[type="submit"]');
await page.click('.el-message-box__btns .el-button--primary');
```
#### 优化后
```typescript
// 在前端添加data-testid
// <button data-testid="edit-user-button">编辑</button>
// <button data-testid="submit-form-button">提交</button>
// <button data-testid="confirm-delete-button">确认</button>
await page.getByTestId('edit-user-button').click();
await page.getByTestId('submit-form-button').click();
await page.getByTestId('confirm-delete-button').click();
```
### 示例3:表单验证
#### 优化前
```typescript
const errorMessage = await page.textContent('.el-form-item__error');
const hasError = await page.locator('.el-input.is-error').count() > 0;
```
#### 优化后
```typescript
// 在前端添加data-testid
// <div data-testid="username-error" class="el-form-item__error">用户名不能为空</div>
const errorMessage = await page.getByTestId('username-error').textContent();
const hasError = await page.getByTestId('username-error').isVisible();
```
## Page Object优化
### 优化前的Page Object
```typescript
export class UserManagementPage {
readonly page: Page;
readonly table: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.table = page.locator('.el-table');
this.submitButton = page.locator('.el-dialog__footer button[type="submit"]');
}
async clickEditUser(rowNumber: number) {
await this.table.locator(`tbody tr:nth-child(${rowNumber}) .edit-button`).click();
}
async submitForm() {
await this.submitButton.click();
}
}
```
### 优化后的Page Object
```typescript
export class UserManagementPage {
readonly page: Page;
readonly table: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.table = page.getByTestId('user-table');
this.submitButton = page.getByTestId('submit-form-button');
}
async clickEditUser(rowNumber: number) {
await this.table.getByTestId(`edit-user-button-${rowNumber}`).click();
}
async submitForm() {
await this.submitButton.click();
}
async getErrorMessage(fieldName: string): Promise<string> {
return await this.page.getByTestId(`${fieldName}-error`).textContent();
}
}
```
## 前端data-testid添加指南
### 添加原则
1. **关键交互元素**:按钮、链接、输入框
2. **表单元素**:提交按钮、取消按钮、确认按钮
3. **错误消息**:表单验证错误、API错误消息
4. **重要区域**:表格、对话框、侧边栏
### 命名规范
```typescript
// 按钮
data-testid="submit-button"
data-testid="cancel-button"
data-testid="delete-button"
// 输入框
data-testid="username-input"
data-testid="password-input"
data-testid="email-input"
// 表格
data-testid="user-table"
data-testid="role-table"
// 表格行
data-testid="user-row-1"
data-testid="user-row-2"
// 表格操作按钮
data-testid="edit-user-button-1"
data-testid="delete-user-button-1"
// 错误消息
data-testid="username-error"
data-testid="password-error"
data-testid="api-error-message"
// 对话框
data-testid="user-form-dialog"
data-testid="confirm-delete-dialog"
// 菜单
data-testid="user-management-menu"
data-testid="role-management-menu"
```
### Vue组件示例
```vue
<template>
<div class="user-management">
<!-- 表格 -->
<el-table
:data="users"
data-testid="user-table"
>
<el-table-column prop="username" label="用户名" />
<el-table-column label="操作">
<template #default="{ row, $index }">
<el-button
data-testid="edit-user-button-${$index}"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
data-testid="delete-user-button-${$index}"
type="danger"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 创建用户对话框 -->
<el-dialog
v-model="dialogVisible"
data-testid="user-form-dialog"
>
<el-form :model="form" data-testid="user-form">
<el-form-item label="用户名" data-testid="username-form-item">
<el-input
v-model="form.username"
data-testid="username-input"
placeholder="请输入用户名"
/>
<div
v-if="errors.username"
data-testid="username-error"
class="el-form-item__error"
>
{{ errors.username }}
</div>
</el-form-item>
<el-form-item label="密码" data-testid="password-form-item">
<el-input
v-model="form.password"
type="password"
data-testid="password-input"
placeholder="请输入密码"
/>
<div
v-if="errors.password"
data-testid="password-error"
class="el-form-item__error"
>
{{ errors.password }}
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button
data-testid="cancel-button"
@click="dialogVisible = false"
>
取消
</el-button>
<el-button
data-testid="submit-button"
type="primary"
@click="handleSubmit"
>
提交
</el-button>
</template>
</el-dialog>
</div>
</template>
```
## 测试稳定性最佳实践
### 1. 使用稳定的等待策略
```typescript
// ✅ 推荐:使用Playwright内置等待
await page.getByTestId('submit-button').click();
await page.waitForLoadState('networkidle');
// ❌ 不推荐:使用固定等待时间
await page.click('.submit-button');
await page.waitForTimeout(3000);
```
### 2. 使用明确的断言
```typescript
// ✅ 推荐:使用明确的断言
await expect(page.getByTestId('success-message')).toBeVisible();
await expect(page.getByTestId('success-message')).toContainText('操作成功');
// ❌ 不推荐:使用隐式断言
await page.waitForSelector('.success-message');
```
### 3. 使用Page Object模式
```typescript
// ✅ 推荐:使用Page Object
const userPage = new UserManagementPage(page);
await userPage.clickEditUser(1);
await userPage.submitForm();
// ❌ 不推荐:直接操作页面元素
await page.click('.edit-button');
await page.click('.submit-button');
```
### 4. 使用测试辅助工具
```typescript
// ✅ 推荐:使用TestHelper
await TestHelper.waitForElementVisible(page, 'user-form-dialog');
await TestHelper.waitForSuccessMessage(page);
await TestHelper.waitForErrorMessage(page);
// ❌ 不推荐:手动实现等待
await page.waitForSelector('.user-form-dialog');
await page.waitForSelector('.el-message--success');
```
## 迁移计划
### 阶段1:添加data-testid1-2天)
1. 登录页面
2. 用户管理页面
3. 角色管理页面
4. 菜单管理页面
5. 系统配置页面
### 阶段2:更新测试用例(1-2天)
1. 更新LoginPage
2. 更新UserManagementPage
3. 更新RoleManagementPage
4. 更新其他Page Objects
### 阶段3:验证测试稳定性(1天)
1. 运行所有测试
2. 检查测试通过率
3. 修复失败的测试
## 总结
通过使用稳定的选择器策略,我们可以显著提高测试的可靠性和可维护性:
- ✅ 测试更稳定,不易受UI变化影响
- ✅ 测试更易读,意图更明确
- ✅ 测试更易维护,减少选择器更新
- ✅ 测试更可靠,减少偶发性失败
---
**创建时间**2026-03-24
**文档版本**v1.0