# 测试选择器优化指南 ## 概述 本指南提供了在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 ``` #### 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 // 登录 // // 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 // // // 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 //
用户名不能为空
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 { 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 ``` ## 测试稳定性最佳实践 ### 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-testid(1-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