# @d8d/e2e-test-utils
> E2E 测试工具集 - 专门用于测试 Radix UI 组件的 Playwright 工具函数
[](https://www.typescriptlang.org/)
[](https://playwright.dev/)
[](https://vitest.dev/)
## 📋 简介
`@d8d/e2e-test-utils` 是一个专为 Radix UI 组件设计的 E2E 测试工具集。它提供了一组强大且易于使用的函数,帮助你更高效地编写和维护端到端测试。
### 核心特性
- 🎯 **Radix UI 专用** - 针对无障碍组件优化的选择器策略
- 🔒 **TypeScript 全支持** - 完整的类型定义和 JSDoc 注释
- 📦 **零运行时依赖** - 仅依赖 Playwright 作为 peer dependency
- 🧪 **开箱即用的测试数据** - 包含常用测试场景的 fixtures
- 🌳 **Tree-shakeable** - 按需导入,只打包使用的代码
- ⚡ **严格模式** - 启用 TypeScript 严格类型检查
## 📦 安装
### Monorepo 项目(Workspace)
```bash
pnpm add -D @d8d/e2e-test-utils@workspace:*
```
### 独立项目
```bash
pnpm add -D @d8d/e2e-test-utils
```
### Peer Dependencies
确保你的项目已安装 `@playwright/test`:
```bash
pnpm add -D @playwright/test
```
### TypeScript 配置
确保你的 `tsconfig.json` 包含以下配置以获得最佳类型支持:
```json
{
"compilerOptions": {
"moduleResolution": "bundler",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
}
}
```
如果你使用 Node.js 16+,也可以使用 `node16` 或 `nodenext` 模块解析。
## 🚀 快速入门
### Select 工具使用
```typescript
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` 属性以获得最佳稳定性。
```tsx
{/* ... */}
```
### 基础使用
```typescript
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
};
// 测试逻辑...
});
```
### 使用类型定义
```typescript
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: '检查选项值是否正确'
};
```
### 使用错误类
```typescript
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 开发环境:**
```typescript
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');
});
```
**独立项目:**
在您的项目中创建测试数据文件:
```bash
mkdir -p tests/fixtures/data
cat > tests/fixtures/data/test-users.json << 'EOF'
{
"users": [
{ "name": "Test User", "email": "test@example.com" }
]
}
EOF
```
然后导入使用:
```typescript
import testUsers from './fixtures/data/test-users.json' assert { type: 'json' };
```
## 📚 API 文档
### 类型定义
#### `BaseOptions`
基础配置选项,所有工具函数配置对象的基类。
```typescript
interface BaseOptions {
/** 超时时间(毫秒)*/
timeout?: number;
}
```
#### `ErrorContext`
错误上下文信息,用于结构化错误报告。
```typescript
interface ErrorContext {
/** 操作类型(如 'selectRadixOption')*/
operation: string;
/** 目标(如下拉框标签)*/
target: string;
/** 期望值 */
expected?: string;
/** 实际值 */
actual?: string;
/** 可用选项列表 */
available?: string[];
/** 修复建议 */
suggestion?: string;
}
```
### 错误类
#### `E2ETestError`
E2E 测试专用错误类,提供结构化的错误上下文信息。
```typescript
class E2ETestError extends Error {
constructor(
public readonly context: ErrorContext,
message?: string
)
}
```
**示例:**
```typescript
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`
默认超时配置(毫秒)。
```typescript
const DEFAULT_TIMEOUTS = {
/** 静态选项超时 */
static: 2000,
/** 异步选项超时 */
async: 5000,
/** 网络空闲超时 */
networkIdle: 10000
} as const;
```
**使用示例:**
```typescript
import { DEFAULT_TIMEOUTS } from '@d8d/e2e-test-utils';
await page.waitForTimeout(DEFAULT_TIMEOUTS.static);
```
#### `SELECTOR_STRATEGIES`
支持的选择器策略列表。
```typescript
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 中的下拉框。
**函数签名:**
```typescript
function selectRadixOption(
page: Page,
label: string,
value: string
): Promise
```
**参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `page` | `Page` | ✅ | Playwright Page 对象 |
| `label` | `string` | ✅ | 下拉框触发器的标签文本(data-testid/aria-label/文本内容) |
| `value` | `string` | ✅ | 要选择的选项值 |
**使用场景:**
- 枚举类型选择(残疾类型、性别、婚姻状况等)
- 静态配置选项
- 选项在页面加载时已存在 DOM 中
**示例:**
```typescript
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 动态加载的下拉框。
**函数签名:**
```typescript
function selectRadixOptionAsync(
page: Page,
label: string,
value: string,
options?: AsyncSelectOptions
): Promise
```
**参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `page` | `Page` | ✅ | Playwright Page 对象 |
| `label` | `string` | ✅ | 下拉框触发器的标签文本 |
| `value` | `string` | ✅ | 要选择的选项值 |
| `options` | `AsyncSelectOptions` | ❌ | 可选配置对象 |
| `options.timeout` | `number` | ❌ | 超时时间(毫秒),默认 5000 |
| `options.waitForOption` | `boolean` | ❌ | 是否等待选项加载,默认 true |
| `options.waitForNetworkIdle` | `boolean` | ❌ | 是否等待网络空闲,默认 true |
**使用场景:**
- 动态数据选择(省份、城市、银行等)
- 选项从 API 加载
- 需要等待网络请求完成
**示例:**
```typescript
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()`
- 🤔 不确定时,可以使用异步版本(会有额外等待,但更稳定)
**实际案例对比:**
```typescript
// 静态 Select - 残疾类型(枚举)
await selectRadixOption(page, '残疾类型', '视力残疾');
// ↓ 点击 → 立即查找选项 → 选择
// 异步 Select - 省份(API 加载)
await selectRadixOptionAsync(page, '省份', '广东省');
// ↓ 点击 → 等待网络请求 → 等待选项出现 → 选择
```
## 🧪 测试
### 运行测试
```bash
# 运行所有单元测试
pnpm test:unit
# 运行测试并生成覆盖率报告
pnpm test:coverage
# 监听模式(开发时使用)
pnpm test
```
### 测试结构
```
tests/
├── fixtures/ # 测试资源
│ ├── data/ # 测试数据(JSON)
│ └── images/ # 测试图片
├── unit/ # 单元测试(Vitest)
├── integration/ # 集成测试(Playwright)
└── stability/ # 稳定性测试
```
### 使用 Fixtures
#### 数据文件
```typescript
// 导入测试用户数据
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
```
### 可用脚本
```bash
# 类型检查
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
## 🔗 相关资源
- [Radix UI](https://www.radix-ui.com/) - 无障碍 UI 组件库
- [Playwright](https://playwright.dev/) - E2E 测试框架
- [TypeScript](https://www.typescriptlang.org/) - 类型安全