yourname 16fe25d45a docs(e2e): 创建 Story 11.3 - Platform 列表显示验证测试 5 dias atrás
..
docs 16fe25d45a docs(e2e): 创建 Story 11.3 - Platform 列表显示验证测试 5 dias atrás
src 16fe25d45a docs(e2e): 创建 Story 11.3 - Platform 列表显示验证测试 5 dias atrás
tests 16fe25d45a docs(e2e): 创建 Story 11.3 - Platform 列表显示验证测试 5 dias atrás
PACKAGING.md 16fe25d45a docs(e2e): 创建 Story 11.3 - Platform 列表显示验证测试 5 dias atrás
README.md 16fe25d45a docs(e2e): 创建 Story 11.3 - Platform 列表显示验证测试 5 dias atrás
eslint.config.js 16fe25d45a docs(e2e): 创建 Story 11.3 - Platform 列表显示验证测试 5 dias atrás
package.json 16fe25d45a docs(e2e): 创建 Story 11.3 - Platform 列表显示验证测试 5 dias atrás
tsconfig.json 16fe25d45a docs(e2e): 创建 Story 11.3 - Platform 列表显示验证测试 5 dias atrás
vitest.config.ts 16fe25d45a docs(e2e): 创建 Story 11.3 - Platform 列表显示验证测试 5 dias atrás

README.md

@d8d/e2e-test-utils

E2E 测试工具集 - 专门用于测试 Radix UI 组件的 Playwright 工具函数

TypeScript Playwright Vitest

📋 简介

@d8d/e2e-test-utils 是一个专为 Radix UI 组件设计的 E2E 测试工具集。它提供了一组强大且易于使用的函数,帮助你更高效地编写和维护端到端测试。

核心特性

  • 🎯 Radix UI 专用 - 针对无障碍组件优化的选择器策略
  • 🔒 TypeScript 全支持 - 完整的类型定义和 JSDoc 注释
  • 📦 零运行时依赖 - 仅依赖 Playwright 作为 peer dependency
  • 🧪 开箱即用的测试数据 - 包含常用测试场景的 fixtures
  • 🌳 Tree-shakeable - 按需导入,只打包使用的代码
  • 严格模式 - 启用 TypeScript 严格类型检查

📦 安装

Monorepo 项目(Workspace)

pnpm add -D @d8d/e2e-test-utils@workspace:*

独立项目

pnpm add -D @d8d/e2e-test-utils

Peer Dependencies

确保你的项目已安装 @playwright/test

pnpm add -D @playwright/test

TypeScript 配置

确保你的 tsconfig.json 包含以下配置以获得最佳类型支持:

{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  }
}

如果你使用 Node.js 16+,也可以使用 node16nodenext 模块解析。

🚀 快速入门

Select 工具使用

import { test, expect } from '@playwright/test';
import { selectRadixOption, selectRadixOptionAsync } from '@d8d/e2e-test-utils';

test('选择静态下拉框选项', async ({ page }) => {
  await page.goto('/form');

  // 选择静态下拉框(如残疾类型、性别等枚举类型)
  await selectRadixOption(page, '残疾类型', '视力残疾');
  await selectRadixOption(page, '性别', '男');
});

test('选择异步加载的下拉框选项', async ({ page }) => {
  await page.goto('/form');

  // 选择异步加载的下拉框(如省份、城市、银行等动态数据)
  await selectRadixOptionAsync(page, '省份', '广东省');
  await selectRadixOptionAsync(page, '城市', '深圳市');
});

选择器策略

工具函数按以下优先级查找下拉框触发器:

  1. data-testid="标签-trigger" - 推荐,最稳定
  2. aria-label="标签" + role="combobox" - 无障碍属性
  3. text="标签" - 文本匹配(兜底)

推荐做法:在 Radix Select 组件上添加 data-testid 属性以获得最佳稳定性。

<RadixSelect.Root>
  <RadixSelect.Trigger data-testid="省份-trigger">
    <RadixSelect.Value placeholder="选择省份" />
  </RadixSelect.Trigger>
  {/* ... */}
</RadixSelect.Root>

基础使用

import { test, expect } from '@playwright/test';
import { BaseOptions, E2ETestError, DEFAULT_TIMEOUTS } from '@d8d/e2e-test-utils';

test('示例测试', async ({ page }) => {
  // 使用工具函数进行测试
  const options: BaseOptions = {
    timeout: DEFAULT_TIMEOUTS.static
  };

  // 测试逻辑...
});

使用类型定义

import type { BaseOptions, ErrorContext } from '@d8d/e2e-test-utils';

// 定义自定义选项
const options: BaseOptions = {
  timeout: 5000
};

// 构建错误上下文
const errorContext: ErrorContext = {
  operation: 'selectOption',
  target: 'dropdown',
  expected: 'Option A',
  actual: 'Option B',
  available: ['Option A', 'Option B', 'Option C'],
  suggestion: '检查选项值是否正确'
};

使用错误类

import { E2ETestError } from '@d8d/e2e-test-utils';

test('错误处理示例', async ({ page }) => {
  const selectElement = await page.$('[data-testid="my-select"]');

  if (!selectElement) {
    throw new E2ETestError({
      operation: 'findSelect',
      target: '[data-testid="my-select"]',
      suggestion: '确认 data-testid 属性是否正确设置'
    });
  }
});

使用测试数据

注意:测试数据文件(fixtures)仅在开发环境可用,不会随 npm 包发布。

Monorepo 开发环境:

import testUsers from '@d8d/e2e-test-utils/tests/fixtures/data/test-users.json' assert { type: 'json' };

test('用户登录', async ({ page }) => {
  const user = testUsers.users[0];

  await page.goto('/login');
  await page.fill('[name="email"]', user.email);
  await page.fill('[name="password"]', 'test_password');
  await page.click('button[type="submit"]');

  await expect(page).toHaveURL('/dashboard');
});

独立项目:

在您的项目中创建测试数据文件:

mkdir -p tests/fixtures/data
cat > tests/fixtures/data/test-users.json << 'EOF'
{
  "users": [
    { "name": "Test User", "email": "test@example.com" }
  ]
}
EOF

然后导入使用:

import testUsers from './fixtures/data/test-users.json' assert { type: 'json' };

📚 API 文档

类型定义

BaseOptions

基础配置选项,所有工具函数配置对象的基类。

interface BaseOptions {
  /** 超时时间(毫秒)*/
  timeout?: number;
}

ErrorContext

错误上下文信息,用于结构化错误报告。

interface ErrorContext {
  /** 操作类型(如 'selectRadixOption')*/
  operation: string;
  /** 目标(如下拉框标签)*/
  target: string;
  /** 期望值 */
  expected?: string;
  /** 实际值 */
  actual?: string;
  /** 可用选项列表 */
  available?: string[];
  /** 修复建议 */
  suggestion?: string;
}

错误类

E2ETestError

E2E 测试专用错误类,提供结构化的错误上下文信息。

class E2ETestError extends Error {
  constructor(
    public readonly context: ErrorContext,
    message?: string
  )
}

示例:

throw new E2ETestError({
  operation: 'selectOption',
  target: 'Role Selector',
  expected: 'Admin',
  actual: 'User',
  available: ['Admin', 'User', 'Guest'],
  suggestion: '确认选项值是否正确拼写'
});
// 输出:
// ❌ selectOption failed
// Target: Role Selector
// Expected: Admin
// Actual: User
// Available: Admin, User, Guest
// 💡 确认选项值是否正确拼写

常量

DEFAULT_TIMEOUTS

默认超时配置(毫秒)。

const DEFAULT_TIMEOUTS = {
  /** 静态选项超时 */
  static: 2000,
  /** 异步选项超时 */
  async: 5000,
  /** 网络空闲超时 */
  networkIdle: 10000
} as const;

使用示例:

import { DEFAULT_TIMEOUTS } from '@d8d/e2e-test-utils';

await page.waitForTimeout(DEFAULT_TIMEOUTS.static);

SELECTOR_STRATEGIES

支持的选择器策略列表。

const SELECTOR_STRATEGIES = [
  'data-testid',
  'aria-label + role',
  'text content + role'
] as const;

策略优先级:

  1. data-testid - 最高优先级,最稳定
  2. aria-label + role - 遵循无障碍标准
  3. text content + role - 兜底方案

Radix UI Select 工具

selectRadixOption()

选择静态枚举型 Radix UI Select 选项。适用于选项在页面加载时已存在于 DOM 中的下拉框。

函数签名:

function selectRadixOption(
  page: Page,
  label: string,
  value: string
): Promise<void>

参数:

参数 类型 必填 说明
page Page Playwright Page 对象
label string 下拉框触发器的标签文本(data-testid/aria-label/文本内容)
value string 要选择的选项值

使用场景:

  • 枚举类型选择(残疾类型、性别、婚姻状况等)
  • 静态配置选项
  • 选项在页面加载时已存在 DOM 中

示例:

import { selectRadixOption } from '@d8d/e2e-test-utils';

test('填写残疾人信息', async ({ page }) => {
  await page.goto('/disabled-person-form');

  // 选择残疾类型
  await selectRadixOption(page, '残疾类型', '视力残疾');

  // 选择性别
  await selectRadixOption(page, '性别', '男');

  // 选择婚姻状况
  await selectRadixOption(page, '婚姻状况', '未婚');
});

selectRadixOptionAsync()

选择异步加载的 Radix UI Select 选项。适用于选项需要从 API 动态加载的下拉框。

函数签名:

function selectRadixOptionAsync(
  page: Page,
  label: string,
  value: string,
  options?: AsyncSelectOptions
): Promise<void>

参数:

参数 类型 必填 说明
page Page Playwright Page 对象
label string 下拉框触发器的标签文本
value string 要选择的选项值
options AsyncSelectOptions 可选配置对象
options.timeout number 超时时间(毫秒),默认 5000
options.waitForOption boolean 是否等待选项加载,默认 true
options.waitForNetworkIdle boolean 是否等待网络空闲,默认 true

使用场景:

  • 动态数据选择(省份、城市、银行等)
  • 选项从 API 加载
  • 需要等待网络请求完成

示例:

import { selectRadixOptionAsync } from '@d8d/e2e-test-utils';

test('填写地址信息', async ({ page }) => {
  await page.goto('/address-form');

  // 选择省份(使用默认配置)
  await selectRadixOptionAsync(page, '省份', '广东省');

  // 选择城市(自定义超时时间)
  await selectRadixOptionAsync(page, '城市', '深圳市', {
    timeout: 10000
  });

  // 选择银行(禁用网络空闲等待,适用于网络不稳定环境)
  await selectRadixOptionAsync(page, '开户银行', '中国工商银行', {
    waitForNetworkIdle: false
  });
});

静态 Select vs 异步 Select

特性 静态 Select (selectRadixOption) 异步 Select (selectRadixOptionAsync)
选项加载时机 页面加载时已存在 DOM 中 点击触发器后 API 加载
使用场景 枚举类型(残疾类型、性别等) 动态数据(省份、城市、银行等)
等待策略 立即查找选项 等待网络请求 + 选项出现
默认超时 2000ms 5000ms
配置对象 AsyncSelectOptions
网络空闲等待 不需要 默认启用
函数签名 selectRadixOption(page, label, value) selectRadixOptionAsync(page, label, value, options?)

选择建议:

  • ✅ 如果下拉框选项在页面加载时已存在 → 使用 selectRadixOption()
  • ✅ 如果下拉框选项需要从 API 动态加载 → 使用 selectRadixOptionAsync()
  • 🤔 不确定时,可以使用异步版本(会有额外等待,但更稳定)

实际案例对比:

// 静态 Select - 残疾类型(枚举)
await selectRadixOption(page, '残疾类型', '视力残疾');
// ↓ 点击 → 立即查找选项 → 选择

// 异步 Select - 省份(API 加载)
await selectRadixOptionAsync(page, '省份', '广东省');
// ↓ 点击 → 等待网络请求 → 等待选项出现 → 选择

🧪 测试

运行测试

# 运行所有单元测试
pnpm test:unit

# 运行测试并生成覆盖率报告
pnpm test:coverage

# 监听模式(开发时使用)
pnpm test

测试结构

tests/
├── fixtures/           # 测试资源
│   ├── data/          # 测试数据(JSON)
│   └── images/        # 测试图片
├── unit/              # 单元测试(Vitest)
├── integration/       # 集成测试(Playwright)
└── stability/         # 稳定性测试

使用 Fixtures

数据文件

// 导入测试用户数据
import testUsers from '@d8d/e2e-test-utils/tests/fixtures/data/test-users.json' assert { type: 'json' };

const user = testUsers.users[0];
console.log(user.name, user.email);

图片文件

图片文件用于测试文件上传功能。请参考 tests/fixtures/images/README.md 了解如何添加测试图片。

🛠️ 开发

项目结构

packages/e2e-test-utils/
├── src/
│   ├── index.ts              # 主导出,tree-shakeable
│   ├── types.ts              # 共享类型定义
│   ├── errors.ts             # 错误类和错误处理
│   ├── constants.ts          # 常量定义
│   ├── radix-select.ts       # Radix UI Select 工具
│   ├── file-upload.ts        # 文件上传工具(规划中)
│   ├── form-helper.ts        # 表单辅助函数(规划中)
│   ├── dialog.ts             # 对话框操作(规划中)
│   └── dynamic-list.ts       # 动态列表管理(规划中)
├── tests/
│   ├── fixtures/             # 测试资源
│   ├── unit/                 # Vitest 单元测试
│   ├── integration/          # Playwright 集成测试
│   └── stability/            # 稳定性测试
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── README.md

可用脚本

# 类型检查
pnpm typecheck

# 构建包
pnpm build

# 开发模式(监听文件变化)
pnpm dev

# 运行测试
pnpm test:unit

# 生成覆盖率报告
pnpm test:coverage

🤝 贡献

欢迎贡献!请遵循以下步骤:

  1. Fork 本仓库
  2. 创建特性分支 (git checkout -b feature/amazing-feature)
  3. 提交更改 (git commit -m 'Add some amazing feature')
  4. 推送到分支 (git push origin feature/amazing-feature)
  5. 开启 Pull Request

代码规范

  • 使用 TypeScript 严格模式
  • 所有导出的函数/类型必须有完整的 JSDoc 注释
  • 单元测试覆盖率 ≥ 80%
  • 遵循项目的 ESLint 和 Prettier 配置

📝 License

MIT

🔗 相关资源