# @d8d/e2e-test-utils > E2E 测试工具集 - 专门用于测试 Radix UI 组件的 Playwright 工具函数 [![TypeScript](https://img.shields.io/badge/TypeScript-5.8+-blue)](https://www.typescriptlang.org/) [![Playwright](https://img.shields.io/badge/Playwright-1.40+-green)](https://playwright.dev/) [![Vitest](https://img.shields.io/badge/Vitest-3.2+-purple)](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/) - 类型安全