# Story 1.2: 实现类型定义和错误处理

Status: done

<!-- 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

- [x] 实现 `src/types.ts` - 共享和特定类型定义 (AC: 1, 2, 4, 5)
  - [x] 导入并重新导出 Playwright `Page` 类型
  - [x] 定义 `BaseOptions` 接口（timeout 等通用配置）
  - [x] 定义 `ErrorContext` 接口（operation, target, expected, actual, available, suggestion）
  - [x] 定义 `AsyncSelectOptions` 接口（extends BaseOptions）
  - [x] 定义 `FileUploadOptions` 接口（extends BaseOptions）
  - [x] 定义 `FormStepOptions` 接口（extends BaseOptions）
  - [x] 定义 `DialogOptions` 接口（extends BaseOptions）
  - [x] 为所有导出类型添加完整 JSDoc 注释
- [x] 实现 `src/errors.ts` - 错误类和错误处理 (AC: 2, 5)
  - [x] 实现 `E2ETestError` 类（extends Error）
  - [x] 实现 `formatErrorMessage` 私有函数（生成友好错误消息）
  - [x] 实现 `throwError` 辅助函数（抛出结构化错误）
  - [x] 为 `E2ETestError` 添加完整 JSDoc 注释
- [x] 实现 `src/constants.ts` - 超时和选择器策略常量 (AC: 3)
  - [x] 定义 `DEFAULT_TIMEOUTS` 常量（static: 2000, async: 5000, networkIdle: 10000）
  - [x] 定义 `SELECTOR_STRATEGIES` 常量（testid → ARIA → text）
  - [x] 为所有常量添加 JSDoc 注释
- [x] 更新 `src/index.ts` 导出新增类型和错误类 (AC: 1, 2, 3)
  - [x] 导出 `types.ts` 中的所有类型
  - [x] 导出 `errors.ts` 中的 `E2ETestError`
  - [x] 导出 `constants.ts` 中的所有常量
- [x] 验证类型检查通过 (AC: 6)
  - [x] 运行 `pnpm typecheck` 确保无类型错误
  - [x] 验证所有类型都有正确的 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 文档创建
- 包含完整的类型定义实现指南、错误处理实现指南、常量定义实现指南
- 为后续工具函数提供类型基础设施
- 包含架构约束、项目标准对齐、前一个故事集成上下文

**实现完成 (2026-01-08):**
- ✅ 完成 `src/types.ts`: 实现了 `BaseOptions`, `ErrorContext`, `AsyncSelectOptions`, `FileUploadOptions`, `FormStepOptions`, `DialogOptions` 类型
- ✅ 完成 `src/errors.ts`: 实现了 `E2ETestError` 类和 `throwError` 辅助函数
- ✅ 完成 `src/constants.ts`: 实现了 `DEFAULT_TIMEOUTS`, `SELECTOR_STRATEGIES` 和 `SelectorStrategy` 类型
- ✅ 所有导出类型都有完整的 JSDoc 注释和示例代码
- ✅ 类型检查通过 (`pnpm typecheck`)
- ✅ 遵循 TypeScript 严格模式，无 `any` 类型

### 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/package.json` - 修复 exports 配置（移除冗余 types 条件）

**相关文件（已在 Story 1.1 中完成，本故事无需修改）：**
- `packages/e2e-test-utils/src/index.ts` - 主导出（已在 Story 1.1 正确配置）
- `packages/e2e-test-utils/tests/unit/index.test.ts` - 单元测试（已在 Story 1.1 创建，测试本故事所有新功能）

**只读参考文件：**
- `_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` - 项目技术栈和规则

## Change Log

### 2026-01-08 - Story 1.2 实现
- ✅ 实现 `src/types.ts`: `BaseOptions`, `ErrorContext`, `AsyncSelectOptions`, `FileUploadOptions`, `FormStepOptions`, `DialogOptions`
- ✅ 实现 `src/errors.ts`: `E2ETestError` 类, `throwError` 辅助函数
- ✅ 实现 `src/constants.ts`: `DEFAULT_TIMEOUTS`, `SELECTOR_STRATEGIES`, `SelectorStrategy` 类型
- ✅ 所有导出都有完整 JSDoc 注释和示例
- ✅ 类型检查通过 (`pnpm typecheck`)

### 2026-01-08 - 代码审查修复
- 🔧 修复 `package.json`: 移除 exports 中冗余的 "types" 条件
- 📝 更新 File List: 澄清 `src/index.ts` 和 `tests/unit/index.test.ts` 在 Story 1.1 中已完成
