e2ad1331cc
feat(测试): 新增Playwright和Vitest测试配置 feat(测试): 添加测试覆盖率报告生成功能 feat(测试): 实现前后端测试脚本集成 fix(测试): 修复测试密码不匹配问题 fix(测试): 修正URL等待策略 fix(测试): 调整错误消息选择器 refactor(测试): 重构测试目录结构 refactor(测试): 优化测试用例组织方式 docs: 更新测试报告文档 docs: 添加测试覆盖率报告模板 ci: 添加Docker测试环境配置 ci: 实现测试自动化脚本 chore: 更新依赖版本 chore: 添加测试相关配置文件
11 KiB
11 KiB
测试选择器优化指南
概述
本指南提供了在Playwright测试中使用稳定选择器的最佳实践,以提高测试的可靠性和可维护性。
选择器优先级
1. 推荐的选择器(最稳定)
1.1 使用data-testid属性
// ✅ 推荐:使用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:
<template>
<el-button data-testid="submit-button" type="primary">提交</el-button>
<el-input data-testid="username-input" v-model="username" />
</template>
1.2 使用角色和文本
// ✅ 推荐:使用角色和文本
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 使用文本内容
// ✅ 推荐:使用文本内容
await page.getByText('登录').click();
await page.getByText('用户管理').click();
// ❌ 不推荐:使用CSS选择器
await page.click('.login-button');
await page.click('.user-management-link');
2. 可接受的选择器(中等稳定性)
2.1 使用ARIA属性
// ✅ 可接受:使用ARIA属性
await page.getByLabel('用户名').fill('admin');
await page.getByPlaceholder('请输入用户名').fill('admin');
await page.getByAltText('Logo').click();
2.2 使用表单属性
// ✅ 可接受:使用表单属性
await page.getByTitle('提交').click();
await page.getByTestId('username').fill('admin');
3. 不推荐的选择器(稳定性差)
3.1 避免使用CSS类名
// ❌ 不推荐:CSS类名可能变化
await page.click('.el-button--primary');
await page.fill('.el-input__inner', 'admin');
3.2 避免使用复杂的CSS选择器
// ❌ 不推荐:复杂选择器难以维护
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 避免使用索引
// ❌ 不推荐:索引不稳定
await page.click('button:nth-child(2)');
await page.fill('input:nth-of-type(1)', 'admin');
选择器优化示例
示例1:登录页面
优化前
await page.click('.el-button--primary');
await page.fill('.el-input__inner', 'admin');
await page.fill('.el-input__inner', 'admin123');
优化后
// 在前端添加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:用户管理页面
优化前
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');
优化后
// 在前端添加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:表单验证
优化前
const errorMessage = await page.textContent('.el-form-item__error');
const hasError = await page.locator('.el-input.is-error').count() > 0;
优化后
// 在前端添加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
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
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添加指南
添加原则
- 关键交互元素:按钮、链接、输入框
- 表单元素:提交按钮、取消按钮、确认按钮
- 错误消息:表单验证错误、API错误消息
- 重要区域:表格、对话框、侧边栏
命名规范
// 按钮
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组件示例
<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. 使用稳定的等待策略
// ✅ 推荐:使用Playwright内置等待
await page.getByTestId('submit-button').click();
await page.waitForLoadState('networkidle');
// ❌ 不推荐:使用固定等待时间
await page.click('.submit-button');
await page.waitForTimeout(3000);
2. 使用明确的断言
// ✅ 推荐:使用明确的断言
await expect(page.getByTestId('success-message')).toBeVisible();
await expect(page.getByTestId('success-message')).toContainText('操作成功');
// ❌ 不推荐:使用隐式断言
await page.waitForSelector('.success-message');
3. 使用Page Object模式
// ✅ 推荐:使用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. 使用测试辅助工具
// ✅ 推荐:使用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-testid(1-2天)
- 登录页面
- 用户管理页面
- 角色管理页面
- 菜单管理页面
- 系统配置页面
阶段2:更新测试用例(1-2天)
- 更新LoginPage
- 更新UserManagementPage
- 更新RoleManagementPage
- 更新其他Page Objects
阶段3:验证测试稳定性(1天)
- 运行所有测试
- 检查测试通过率
- 修复失败的测试
总结
通过使用稳定的选择器策略,我们可以显著提高测试的可靠性和可维护性:
- ✅ 测试更稳定,不易受UI变化影响
- ✅ 测试更易读,意图更明确
- ✅ 测试更易维护,减少选择器更新
- ✅ 测试更可靠,减少偶发性失败
创建时间:2026-03-24
文档版本:v1.0