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

11 KiB
Raw Blame History

测试选择器优化指南

概述

本指南提供了在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添加指南

添加原则

  1. 关键交互元素:按钮、链接、输入框
  2. 表单元素:提交按钮、取消按钮、确认按钮
  3. 错误消息:表单验证错误、API错误消息
  4. 重要区域:表格、对话框、侧边栏

命名规范

// 按钮
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-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