Selaa lähdekoodia

feat(e2e-test-utils): 创建 Story 1.2 类型定义和错误处理

- 创建完整的类型定义实现指南(BaseOptions, ErrorContext, AsyncSelectOptions 等)
- 创建错误处理实现指南(E2ETestError, throwError)
- 创建常量定义实现指南(DEFAULT_TIMEOUTS, SELECTOR_STRATEGIES)
- 包含架构约束、项目标准对齐、前一个故事集成上下文
- 更新 sprint-status 状态为 ready-for-dev

🤖 Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 1 viikko sitten
vanhempi
sitoutus
a339c6e4dd

+ 462 - 0
_bmad-output/implementation-artifacts/1-2-implement-types-errors.md

@@ -0,0 +1,462 @@
+# Story 1.2: 实现类型定义和错误处理
+
+Status: ready-for-dev
+
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
+
+## Story
+
+作为测试开发者,
+我想要工具包有完整的类型支持和统一的错误处理,
+以便获得类型安全和清晰的错误提示。
+
+## Acceptance Criteria
+
+**Given** 包结构已创建(Story 1.1 完成)
+**When** 实现 `src/types.ts`, `src/errors.ts`, `src/constants.ts`
+**Then** `types.ts` 导出 `BaseOptions`, `AsyncSelectOptions`, `FileUploadOptions`, `FormStepOptions`, `DialogOptions` 等共享和特定类型
+**And** `errors.ts` 导出 `E2ETestError` 类和 `ErrorContext` 接口
+**And** `constants.ts` 定义 `DEFAULT_TIMEOUTS`(static: 2000, async: 5000, networkIdle: 10000)
+**And** 所有类型使用 TypeScript 严格模式,无 `any` 类型
+**And** 所有导出类型都有完整的 JSDoc 注释
+**And** 类型检查通过(`pnpm typecheck`)
+
+## Tasks / Subtasks
+
+- [ ] 实现 `src/types.ts` - 共享和特定类型定义 (AC: 1, 2, 4, 5)
+  - [ ] 导入并重新导出 Playwright `Page` 类型
+  - [ ] 定义 `BaseOptions` 接口(timeout 等通用配置)
+  - [ ] 定义 `ErrorContext` 接口(operation, target, expected, actual, available, suggestion)
+  - [ ] 定义 `AsyncSelectOptions` 接口(extends BaseOptions)
+  - [ ] 定义 `FileUploadOptions` 接口(extends BaseOptions)
+  - [ ] 定义 `FormStepOptions` 接口(extends BaseOptions)
+  - [ ] 定义 `DialogOptions` 接口(extends BaseOptions)
+  - [ ] 为所有导出类型添加完整 JSDoc 注释
+- [ ] 实现 `src/errors.ts` - 错误类和错误处理 (AC: 2, 5)
+  - [ ] 实现 `E2ETestError` 类(extends Error)
+  - [ ] 实现 `formatErrorMessage` 私有函数(生成友好错误消息)
+  - [ ] 实现 `throwError` 辅助函数(抛出结构化错误)
+  - [ ] 为 `E2ETestError` 添加完整 JSDoc 注释
+- [ ] 实现 `src/constants.ts` - 超时和选择器策略常量 (AC: 3)
+  - [ ] 定义 `DEFAULT_TIMEOUTS` 常量(static: 2000, async: 5000, networkIdle: 10000)
+  - [ ] 定义 `SELECTOR_STRATEGIES` 常量(testid → ARIA → text)
+  - [ ] 为所有常量添加 JSDoc 注释
+- [ ] 更新 `src/index.ts` 导出新增类型和错误类 (AC: 1, 2, 3)
+  - [ ] 导出 `types.ts` 中的所有类型
+  - [ ] 导出 `errors.ts` 中的 `E2ETestError`
+  - [ ] 导出 `constants.ts` 中的所有常量
+- [ ] 验证类型检查通过 (AC: 6)
+  - [ ] 运行 `pnpm typecheck` 确保无类型错误
+  - [ ] 验证所有类型都有正确的 JSDoc 注释
+
+## Dev Notes
+
+### Epic 1 背景
+
+**Epic 1 目标:** 测试开发者可以安装 `@d8d/e2e-test-utils` 包,立即使用 Select 工具测试 Radix UI Select 组件。
+
+**本故事在 Epic 中的位置:** 第二个故事,在包结构(Story 1.1)建立后,实现类型定义和错误处理基础设施,为所有后续工具函数提供共享类型和统一的错误处理。
+
+### 架构约束和模式
+
+**从架构文档中必须遵循的决策:**
+
+**类型系统策略:分层类型(共享+特定)**
+- `types.ts` 存放共享类型(`BaseOptions`, `ErrorContext`)
+- 各模块文件存放特定类型(如 `AsyncSelectOptions` 在 `radix-select.ts` 中)
+- 所有配置对象继承 `BaseOptions`
+
+**错误处理策略:结构化错误类 + 友好消息**
+- 使用 `E2ETestError` 类(extends Error)
+- 包含结构化的 `ErrorContext` 信息
+- 错误消息格式:❌ 操作失败、上下文信息、💡 修复建议
+
+**常量定义策略:**
+- 超时使用 `DEFAULT_TIMEOUTS` 常量,不硬编码
+- 选择器策略使用 `SELECTOR_STRATEGIES` 常量
+
+### 技术实现要求
+
+**`src/types.ts` - 类型定义实现指南:**
+
+```typescript
+import type { Page } from '@playwright/test';
+
+/**
+ * 基础配置选项,所有配置对象的基类
+ *
+ * @example
+ * ```ts
+ * interface MyOptions extends BaseOptions {
+ *   customOption?: string;
+ * }
+ * ```
+ */
+export interface BaseOptions {
+  /** 超时时间(毫秒)*/
+  timeout?: number;
+}
+
+/**
+ * 错误上下文接口,用于提供结构化的错误信息
+ */
+export interface ErrorContext {
+  /** 操作类型(如 'selectRadixOption')*/
+  operation: string;
+  /** 目标(如下拉框标签)*/
+  target: string;
+  /** 期望值 */
+  expected?: string;
+  /** 实际值 */
+  actual?: string;
+  /** 可用选项列表 */
+  available?: string[];
+  /** 修复建议 */
+  suggestion?: string;
+}
+
+/**
+ * 异步 Select 选项配置
+ *
+ * @example
+ * ```ts
+ * await selectRadixOptionAsync(page, '省份', '广东省', {
+ *   timeout: 10000,
+ *   waitForOption: true
+ * });
+ * ```
+ */
+export interface AsyncSelectOptions extends BaseOptions {
+  /** 是否等待选项加载完成(默认:true)*/
+  waitForOption?: boolean;
+  /** 等待网络空闲后再操作(默认:false)*/
+  waitForNetworkIdle?: boolean;
+}
+
+/**
+ * 文件上传选项配置
+ */
+export interface FileUploadOptions extends BaseOptions {
+  /** 验证文件是否成功上传(默认:true)*/
+  verifyUpload?: boolean;
+}
+
+/**
+ * 表单步骤配置
+ */
+export interface FormStepOptions extends BaseOptions {
+  /** 是否在填写前滚动到元素(默认:true)*/
+  scrollToElement?: boolean;
+}
+
+/**
+ * 对话框操作配置
+ */
+export interface DialogOptions extends BaseOptions {
+  /** 对话框关闭后的等待时间(毫秒,默认:500)*/
+  closeWaitTime?: number;
+}
+
+// 重新导出 Playwright 类型以便使用者使用
+export type { Page } from '@playwright/test';
+```
+
+**`src/errors.ts` - 错误处理实现指南:**
+
+```typescript
+import type { ErrorContext } from './types';
+
+/**
+ * E2E 测试专用错误类
+ *
+ * 提供结构化的错误上下文信息,帮助开发者快速定位问题。
+ *
+ * @example
+ * ```ts
+ * throw new E2ETestError({
+ *   operation: 'selectRadixOption',
+ *   target: '残疾类型',
+ *   expected: '视力残疾',
+ *   available: ['听力残疾', '言语残疾', '肢体残疾']
+ * });
+ * ```
+ */
+export class E2ETestError extends Error {
+  /**
+   * 创建一个 E2E 测试错误实例
+   *
+   * @param context - 结构化的错误上下文
+   * @param message - 自定义错误消息(可选,默认自动生成)
+   */
+  constructor(
+    public readonly context: ErrorContext,
+    message?: string
+  ) {
+    super(message || formatErrorMessage(context));
+    this.name = 'E2ETestError';
+  }
+}
+
+/**
+ * 格式化错误消息为友好格式
+ *
+ * @internal
+ */
+function formatErrorMessage(context: ErrorContext): string {
+  const parts: string[] = [
+    `❌ ${context.operation} failed`,
+    `Target: ${context.target}`
+  ];
+
+  if (context.expected) {
+    parts.push(`Expected: ${context.expected}`);
+  }
+
+  if (context.actual) {
+    parts.push(`Actual: ${context.actual}`);
+  }
+
+  if (context.available && context.available.length > 0) {
+    parts.push(`Available: ${context.available.join(', ')}`);
+  }
+
+  if (context.suggestion) {
+    parts.push(`\n💡 ${context.suggestion}`);
+  }
+
+  return parts.join('\n');
+}
+
+/**
+ * 抛出 E2E 测试错误的辅助函数
+ *
+ * @param context - 错误上下文
+ * @throws {E2ETestError} 永远抛出错误
+ *
+ * @example
+ * ```ts
+ * if (!optionFound) {
+ *   throwError({
+ *     operation: 'selectRadixOption',
+ *     target: label,
+ *     expected: value,
+ *     available: availableOptions,
+ *     suggestion: '检查选项值是否正确'
+ *   });
+ * }
+ * ```
+ */
+export function throwError(context: ErrorContext): never {
+  throw new E2ETestError(context);
+}
+```
+
+**`src/constants.ts` - 常量定义实现指南:**
+
+```typescript
+/**
+ * 默认超时配置(毫秒)
+ *
+ * @example
+ * ```ts
+ * const timeout = options.timeout ?? DEFAULT_TIMEOUTS.static;
+ * ```
+ */
+export const DEFAULT_TIMEOUTS = {
+  /** 静态选项超时(2秒)*/
+  static: 2000,
+  /** 异步选项超时(5秒)*/
+  async: 5000,
+  /** 网络空闲超时(10秒)*/
+  networkIdle: 10000
+} as const;
+
+/**
+ * 选择器策略常量
+ *
+ * 定义选择器查找的优先级顺序,用于应对 Radix UI 版本升级。
+ *
+ * @example
+ * ```ts
+ * // 按优先级尝试选择器
+ * for (const strategy of SELECTOR_STRATEGIES) {
+ *   const element = await findByStrategy(strategy, label);
+ *   if (element) return element;
+ * }
+ * ```
+ */
+export const SELECTOR_STRATEGIES = [
+  'data-testid',
+  'aria-label + role',
+  'text content + role'
+] as const;
+
+/**
+ * 选择器策略类型
+ */
+export type SelectorStrategy = typeof SELECTOR_STRATEGIES[number];
+```
+
+**`src/index.ts` - 更新主导出文件:**
+
+```typescript
+// 导出类型定义
+export * from './types';
+
+// 导出错误类
+export * from './errors';
+
+// 导出常量
+export * from './constants';
+
+// Radix UI Select 工具(后续故事实现)
+// export * from './radix-select';
+```
+
+### 项目标准对齐
+
+**与 project-context.md 对齐:**
+
+1. **TypeScript 配置**:
+   - 严格模式已启用(`strict: true`),禁止 `any` 类型
+   - 所有类型定义必须有明确类型注解
+
+2. **JSDoc 注释标准**:
+   - 所有导出类型、函数、类必须有完整 JSDoc
+   - 使用 `@param`, `@throws`, `@example` 标签
+   - 添加使用示例代码
+
+3. **命名约定**:
+   - 接口:PascalCase,`Options` 后缀(如 `BaseOptions`)
+   - 常量:UPPER_SNAKE_CASE(如 `DEFAULT_TIMEOUTS`)
+   - 类:PascalCase,无前缀(如 `E2ETestError`)
+   - 函数:camelCase(如 `throwError`)
+
+### 与前一个故事的集成
+
+**Story 1.1 已完成的工作:**
+- ✅ 包结构已创建:`packages/e2e-test-utils/`
+- ✅ 配置文件已就绪:`package.json`, `tsconfig.json`, `vitest.config.ts`
+- ✅ 占位文件已创建:`src/types.ts`, `src/errors.ts`, `src/constants.ts`
+- ✅ TypeScript 严格模式已启用
+- ✅ 目录结构已创建:`tests/unit/`, `tests/fixtures/`
+
+**本故事需要做的:**
+- 将占位文件替换为完整实现
+- 确保所有类型定义与占位接口兼容
+- 运行 `pnpm typecheck` 验证类型检查通过
+
+### 类型检查验证
+
+**运行命令:**
+```bash
+cd packages/e2e-test-utils
+pnpm typecheck
+```
+
+**预期结果:**
+- 无类型错误输出
+- 所有 JSDoc 注释正确识别
+- 导出类型可以被正确推断
+
+### 文件结构约束
+
+**必须遵循的文件结构:**
+```
+packages/e2e-test-utils/src/
+├── index.ts         # 主导出(更新,导出新类型和错误类)
+├── types.ts         # 共享类型定义(完整实现)
+├── errors.ts        # 错误类和错误处理(完整实现)
+└── constants.ts     # 常量定义(完整实现)
+```
+
+**禁止事项(Anti-Patterns):**
+- ❌ 使用 `any` 类型
+- ❌ 硬编码超时值(必须使用 `DEFAULT_TIMEOUTS`)
+- ❌ 抛出原生 `Error`(必须使用 `E2ETestError`)
+- ❌ 缺少 JSDoc 注释
+- ❌ 配置对象不继承 `BaseOptions`
+
+### 测试要求
+
+**单元测试(在 Story 1.6 中实现):**
+本故事创建的类型和错误类将在 Story 1.6 中进行单元测试。
+
+**当前验证方法:**
+- 类型检查:`pnpm typecheck`
+- JSDoc 验证:手动检查所有导出都有完整注释
+
+### 性能约束
+
+**从 NFR 提取的性能要求:**
+- NFR8: 静态 Select 选择操作应在 2 秒内完成
+- NFR9: 异步 Select 选择操作应在 5 秒内完成(默认超时)
+- NFR10: 工具函数本身的开销不超过 100ms
+
+**本故事相关的性能约束:**
+- 类型定义必须是零运行时开销(TypeScript 编译后完全移除)
+- 错误消息格式化不应超过 10ms
+
+### Project Structure Notes
+
+**对齐项目 Monorepo 架构:**
+- 包位于 `packages/e2e-test-utils/`
+- 使用 workspace 协议安装:`@d8d/e2e-test-utils@workspace:*`
+- 与现有 `@d8d/shared-test-util`(后端集成测试)分离
+
+**与项目标准对齐:**
+- 遵循 `docs/standards/testing-standards.md` 中的测试规范
+- 遵循 `docs/standards/coding-standards.md` 中的编码标准
+- 遵循 `docs/standards/e2e-radix-testing.md` 中的 Radix UI E2E 测试标准
+
+### References
+
+**PRD 来源:**
+- [PRD - E2E测试工具包](_bmad-output/planning-artifacts/prd.md) - 项目需求概述
+- [PRD - 类型系统需求](_bmad-output/planning-artifacts/epics.md#从-architecture-文档提取的技术需求) - 分层类型设计
+
+**Architecture 来源:**
+- [Architecture - 类型系统策略](_bmad-output/planning-artifacts/architecture.md#type-system-strategy) - 分层类型决策
+- [Architecture - 错误处理策略](_bmad-output/planning-artifacts/architecture.md#error-handling-strategy) - 结构化错误类
+- [Architecture - 实现模式](_bmad-output/planning-artifacts/architecture.md#implementation-patterns--consistency-rules) - 命名和格式约定
+- [Architecture - 项目结构](_bmad-output/planning-artifacts/architecture.md#project-structure--boundaries) - 完整目录结构
+
+**标准文档来源:**
+- [E2E Radix UI 测试标准](docs/standards/e2e-radix-testing.md) - 核心测试标准文档
+- [Project Context](_bmad-output/project-context.md) - 项目技术栈和规则
+
+**Epic 来源:**
+- [Epic 1 - Story 1.2](_bmad-output/planning-artifacts/epics.md#story-12-实现类型定义和错误处理) - 原始用户故事和验收标准
+
+**前一个故事:**
+- [Story 1.1 - 创建包基础结构和配置](_bmad-output/implementation-artifacts/1-1-create-package-structure.md) - 包基础设施
+
+## Dev Agent Record
+
+### Agent Model Used
+
+Claude (d8d-model) via create-story workflow
+
+### Debug Log References
+
+### Completion Notes List
+
+- 故事创建时间: 2026-01-08
+- 基于 PRD、Architecture、E2E Radix 测试标准、Project Context 文档创建
+- 包含完整的类型定义实现指南、错误处理实现指南、常量定义实现指南
+- 为后续工具函数提供类型基础设施
+- 包含架构约束、项目标准对齐、前一个故事集成上下文
+
+### File List
+
+**本故事将创建/修改的文件:**
+- `packages/e2e-test-utils/src/types.ts` - 完整实现共享和特定类型定义
+- `packages/e2e-test-utils/src/errors.ts` - 完整实现错误类和错误处理
+- `packages/e2e-test-utils/src/constants.ts` - 完整实现超时和选择器策略常量
+- `packages/e2e-test-utils/src/index.ts` - 更新主导出,导出新增类型和错误类
+
+**相关文件(只读,用于参考):**
+- `_bmad-output/implementation-artifacts/1-1-create-package-structure.md` - 前一个故事
+- `_bmad-output/planning-artifacts/epics.md` - Epic 和故事定义
+- `_bmad-output/planning-artifacts/architecture.md` - 架构决策和模式
+- `_bmad-output/project-context.md` - 项目技术栈和规则

+ 1 - 1
_bmad-output/implementation-artifacts/sprint-status.yaml

@@ -43,7 +43,7 @@ development_status:
   # Epic 1: 测试工具包基础框架与 Select 支持
   epic-1: in-progress
   1-1-create-package-structure: done
-  1-2-implement-types-errors: backlog
+  1-2-implement-types-errors: ready-for-dev
   1-3-static-select-tool: backlog
   1-4-async-select-tool: backlog
   1-5-main-export-docs: backlog