|
@@ -0,0 +1,509 @@
|
|
|
|
|
+# Story 1.3: 实现静态 Select 工具函数
|
|
|
|
|
+
|
|
|
|
|
+Status: ready-for-dev
|
|
|
|
|
+
|
|
|
|
|
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
|
|
+
|
|
|
|
|
+## Story
|
|
|
|
|
+
|
|
|
|
|
+作为测试开发者,
|
|
|
|
|
+我想要使用 `selectRadixOption()` 函数选择静态枚举型下拉框,
|
|
|
|
|
+以便无需理解 Radix UI DOM 结构就能编写测试。
|
|
|
|
|
+
|
|
|
|
|
+## Acceptance Criteria
|
|
|
|
|
+
|
|
|
|
|
+**Given** 类型定义和错误处理已创建(Story 1.1、1.2 完成)
|
|
|
|
|
+**When** 实现 `src/radix-select.ts` 中的 `selectRadixOption()` 函数
|
|
|
|
|
+**Then** 函数签名:`selectRadixOption(page: Page, label: string, value: string): Promise<void>`
|
|
|
|
|
+**And** 选择器策略:`data-testid` → aria-label + role → text content
|
|
|
|
|
+**And** 自动处理点击触发器、等待选项列表、点击选项
|
|
|
|
|
+**And** 错误时抛出 `E2ETestError`,包含标签、期望值、可用选项
|
|
|
|
|
+**And** 操作在 2 秒内完成(NFR8)
|
|
|
|
|
+**And** 所有导出函数有完整的 JSDoc 注释
|
|
|
|
|
+
|
|
|
|
|
+## Tasks / Subtasks
|
|
|
|
|
+
|
|
|
|
|
+- [ ] 实现 `src/radix-select.ts` - 静态 Select 工具函数 (AC: 1, 2, 3, 4)
|
|
|
|
|
+ - [ ] 实现 `selectRadixOption(page, label, value)` 主函数
|
|
|
|
|
+ - [ ] 实现触发器查找逻辑(按优先级:testid → ARIA → text)
|
|
|
|
|
+ - [ ] 实现选项查找逻辑(data-value → text content)
|
|
|
|
|
+ - [ ] 实现点击触发器和选项的交互流程
|
|
|
|
|
+ - [ ] 添加错误处理和友好错误消息
|
|
|
|
|
+ - [ ] 为所有导出函数添加完整 JSDoc 注释
|
|
|
|
|
+- [ ] 更新 `src/index.ts` 导出新增函数 (AC: 6)
|
|
|
|
|
+- [ ] 类型检查通过验证 (AC: 7)
|
|
|
|
|
+ - [ ] 运行 `pnpm typecheck` 确保无类型错误
|
|
|
|
|
+
|
|
|
|
|
+## Dev Notes
|
|
|
|
|
+
|
|
|
|
|
+### Epic 1 背景
|
|
|
|
|
+
|
|
|
|
|
+**Epic 1 目标:** 测试开发者可以安装 `@d8d/e2e-test-utils` 包,立即使用 Select 工具测试 Radix UI Select 组件。
|
|
|
|
|
+
|
|
|
|
|
+**本故事在 Epic 中的位置:** 第三个故事,实现核心的静态 Select 工具函数。这是第一个实际可用的工具函数,也是后续异步 Select 函数的基础。
|
|
|
|
|
+
|
|
|
|
|
+### 架构约束和模式
|
|
|
|
|
+
|
|
|
|
|
+**从架构文档中必须遵循的决策:**
|
|
|
|
|
+
|
|
|
|
|
+**选择器策略:混合策略(testid → ARIA → 文本)**
|
|
|
|
|
+- 优先级 1: `data-testid="${label}-trigger"` - 最稳定,推荐
|
|
|
|
|
+- 优先级 2: `aria-label="${label}"` + `role="combobox"` - 无障碍属性
|
|
|
|
|
+- 优先级 3: `text="${label}"` - 文本匹配兜底
|
|
|
|
|
+
|
|
|
|
|
+**API 设计模式:3个必需参数 + 可选配置对象**
|
|
|
|
|
+- 必需参数:`page`, `label`, `value`
|
|
|
|
|
+- 静态 Select 不需要配置对象(保持简洁)
|
|
|
|
|
+- 异步 Select 需要配置对象(在 Story 1.4 实现)
|
|
|
|
|
+
|
|
|
|
|
+**错误处理策略:结构化错误类 + 友好消息**
|
|
|
|
|
+- 使用 `E2ETestError` 类(已在 Story 1.2 实现)
|
|
|
|
|
+- 包含 `ErrorContext` 信息(operation, target, expected, available, suggestion)
|
|
|
|
|
+- 错误消息格式:❌ 操作失败、上下文信息、💡 修复建议
|
|
|
|
|
+
|
|
|
|
|
+### 技术实现要求
|
|
|
|
|
+
|
|
|
|
|
+**`src/radix-select.ts` - 静态 Select 工具函数实现指南:**
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+import type { Page } from '@playwright/test';
|
|
|
|
|
+import { throwError } from './errors';
|
|
|
|
|
+import { DEFAULT_TIMEOUTS } from './constants';
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 选择 Radix UI 下拉框的静态选项
|
|
|
|
|
+ *
|
|
|
|
|
+ * @description
|
|
|
|
|
+ * 自动处理 Radix UI Select 的 DOM 结构和交互流程,
|
|
|
|
|
+ * 无需手动理解组件的复杂结构。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 支持的选择器策略(按优先级):
|
|
|
|
|
+ * 1. `data-testid="${label}-trigger"` - 推荐,最稳定
|
|
|
|
|
+ * 2. `aria-label="${label}"` + `role="combobox"` - 无障碍属性
|
|
|
|
|
+ * 3. `text="${label}"` - 文本匹配(兜底)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param page - Playwright Page 对象
|
|
|
|
|
+ * @param label - 下拉框的标签文本(用于定位触发器)
|
|
|
|
|
+ * @param value - 要选择的选项值
|
|
|
|
|
+ * @throws {E2ETestError} 当触发器或选项未找到时
|
|
|
|
|
+ *
|
|
|
|
|
+ * @example
|
|
|
|
|
+ * ```ts
|
|
|
|
|
+ * // 选择残疾类型
|
|
|
|
|
+ * await selectRadixOption(page, '残疾类型', '视力残疾');
|
|
|
|
|
+ *
|
|
|
|
|
+ * // 选择性别
|
|
|
|
|
+ * await selectRadixOption(page, '性别', '男');
|
|
|
|
|
+ *
|
|
|
|
|
+ * // 选择残疾等级
|
|
|
|
|
+ * await selectRadixOption(page, '残疾等级', '一级');
|
|
|
|
|
+ * ```
|
|
|
|
|
+ */
|
|
|
|
|
+export async function selectRadixOption(
|
|
|
|
|
+ page: Page,
|
|
|
|
|
+ label: string,
|
|
|
|
|
+ value: string
|
|
|
|
|
+): Promise<void> {
|
|
|
|
|
+ // TODO: 实现触发器查找逻辑
|
|
|
|
|
+ // TODO: 实现选项查找和点击逻辑
|
|
|
|
|
+ // TODO: 添加错误处理
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**触发器查找逻辑实现:**
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+/**
|
|
|
|
|
+ * 查找 Radix UI Select 触发器
|
|
|
|
|
+ *
|
|
|
|
|
+ * @internal
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param page - Playwright Page 对象
|
|
|
|
|
+ * @param label - 下拉框标签
|
|
|
|
|
+ * @returns 触发器元素
|
|
|
|
|
+ * @throws {E2ETestError} 当触发器未找到时
|
|
|
|
|
+ */
|
|
|
|
|
+async function findTrigger(page: Page, label: string) {
|
|
|
|
|
+ const timeout = DEFAULT_TIMEOUTS.static;
|
|
|
|
|
+
|
|
|
|
|
+ // 策略 1: data-testid
|
|
|
|
|
+ const testIdSelector = `[data-testid="${label}-trigger"]`;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const trigger = await page.waitForSelector(testIdSelector, { timeout });
|
|
|
|
|
+ if (trigger) return trigger;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // 继续尝试下一个策略
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 策略 2: aria-label + role
|
|
|
|
|
+ try {
|
|
|
|
|
+ const ariaSelector = `[aria-label="${label}"][role="combobox"]`;
|
|
|
|
|
+ const trigger = await page.waitForSelector(ariaSelector, { timeout });
|
|
|
|
|
+ if (trigger) return trigger;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // 继续尝试下一个策略
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 策略 3: text content
|
|
|
|
|
+ try {
|
|
|
|
|
+ const trigger = await page.waitForSelector(`text="${label}"`, { timeout });
|
|
|
|
|
+ if (trigger) return trigger;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // 所有策略都失败
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 未找到触发器
|
|
|
|
|
+ throwError({
|
|
|
|
|
+ operation: 'selectRadixOption',
|
|
|
|
|
+ target: label,
|
|
|
|
|
+ expected: value,
|
|
|
|
|
+ suggestion: '检查下拉框标签是否正确,或添加 data-testid 属性'
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**选项查找和点击逻辑实现:**
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+/**
|
|
|
|
|
+ * 查找并点击 Radix UI Select 选项
|
|
|
|
|
+ *
|
|
|
|
|
+ * @internal
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param page - Playwright Page 对象
|
|
|
|
|
+ * @param value - 选项值
|
|
|
|
|
+ * @param availableOptions - 可用选项列表(用于错误提示)
|
|
|
|
|
+ * @throws {E2ETestError} 当选项未找到时
|
|
|
|
|
+ */
|
|
|
|
|
+async function findAndClickOption(
|
|
|
|
|
+ page: Page,
|
|
|
|
|
+ value: string,
|
|
|
|
|
+ availableOptions: string[]
|
|
|
|
|
+) {
|
|
|
|
|
+ const timeout = DEFAULT_TIMEOUTS.static;
|
|
|
|
|
+
|
|
|
|
|
+ // 策略 1: data-value
|
|
|
|
|
+ const dataValueSelector = `[role="option"][data-value="${value}"]`;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const option = await page.waitForSelector(dataValueSelector, { timeout });
|
|
|
|
|
+ if (option) {
|
|
|
|
|
+ await option.click();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // 继续尝试下一个策略
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 策略 2: text content
|
|
|
|
|
+ const textSelector = `[role="option"]:has-text("${value}")`;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const option = await page.waitForSelector(textSelector, { timeout });
|
|
|
|
|
+ if (option) {
|
|
|
|
|
+ await option.click();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // 所有策略都失败
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 未找到选项
|
|
|
|
|
+ throwError({
|
|
|
|
|
+ operation: 'selectRadixOption',
|
|
|
|
|
+ target: `选项 "${value}"`,
|
|
|
|
|
+ available: availableOptions,
|
|
|
|
|
+ suggestion: '检查选项值是否正确,或确认选项已加载到 DOM 中'
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**主函数完整实现:**
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+export async function selectRadixOption(
|
|
|
|
|
+ page: Page,
|
|
|
|
|
+ label: string,
|
|
|
|
|
+ value: string
|
|
|
|
|
+): Promise<void> {
|
|
|
|
|
+ // 1. 查找触发器
|
|
|
|
|
+ const trigger = await findTrigger(page, label);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 点击触发器展开选项列表
|
|
|
|
|
+ await trigger.click();
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 等待选项列表出现
|
|
|
|
|
+ await page.waitForSelector('[role="listbox"]', {
|
|
|
|
|
+ timeout: DEFAULT_TIMEOUTS.static
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 获取可用选项列表(用于错误提示)
|
|
|
|
|
+ const availableOptions = await page.evaluate(() => {
|
|
|
|
|
+ const options = document.querySelectorAll('[role="option"]');
|
|
|
|
|
+ return Array.from(options).map(opt => opt.textContent || '');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 查找并点击选项
|
|
|
|
|
+ await findAndClickOption(page, value, availableOptions);
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 等待下拉框关闭(可选,确保操作完成)
|
|
|
|
|
+ // 不需要显式等待,Playwright auto-waiting 会处理
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**`src/index.ts` - 更新主导出文件:**
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// ... 现有导出 ...
|
|
|
|
|
+
|
|
|
|
|
+// Radix UI Select 工具
|
|
|
|
|
+export { selectRadixOption } from './radix-select';
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 与前一个故事的集成
|
|
|
|
|
+
|
|
|
|
|
+**Story 1.1 已完成的工作:**
|
|
|
|
|
+- ✅ 包结构已创建:`packages/e2e-test-utils/`
|
|
|
|
|
+- ✅ 配置文件已就绪:`package.json`, `tsconfig.json`, `vitest.config.ts`
|
|
|
|
|
+- ✅ 目录结构已创建:`tests/unit/`, `tests/fixtures/`
|
|
|
|
|
+
|
|
|
|
|
+**Story 1.2 已完成的工作:**
|
|
|
|
|
+- ✅ 类型定义完整:`src/types.ts` 包含所有共享类型
|
|
|
|
|
+- ✅ 错误处理完整:`src/errors.ts` 包含 `E2ETestError` 和 `throwError`
|
|
|
|
|
+- ✅ 常量定义完整:`src/constants.ts` 包含 `DEFAULT_TIMEOUTS` 和 `SELECTOR_STRATEGIES`
|
|
|
|
|
+
|
|
|
|
|
+**本故事需要做的:**
|
|
|
|
|
+- 创建 `src/radix-select.ts` 实现静态 Select 工具函数
|
|
|
|
|
+- 更新 `src/index.ts` 导出新函数
|
|
|
|
|
+- 运行 `pnpm typecheck` 验证类型检查通过
|
|
|
|
|
+
|
|
|
|
|
+### DOM 结构理解
|
|
|
|
|
+
|
|
|
|
|
+**Radix UI Select 的典型 DOM 结构:**
|
|
|
|
|
+
|
|
|
|
|
+```html
|
|
|
|
|
+<!-- 触发器(点击展开下拉列表) -->
|
|
|
|
|
+<button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ role="combobox"
|
|
|
|
|
+ aria-label="残疾类型"
|
|
|
|
|
+ data-radix-select-trigger=""
|
|
|
|
|
+ data-state="closed"
|
|
|
|
|
+>
|
|
|
|
|
+ <span>残疾类型</span>
|
|
|
|
|
+ <svg><!-- 下箭头图标 --></svg>
|
|
|
|
|
+</button>
|
|
|
|
|
+
|
|
|
|
|
+<!-- 选项列表容器(点击后展开) -->
|
|
|
|
|
+<div role="listbox" data-radix-select-content="">
|
|
|
|
|
+ <!-- 单个选项 -->
|
|
|
|
|
+ <div
|
|
|
|
|
+ role="option"
|
|
|
|
|
+ data-value="blind"
|
|
|
|
|
+ data-state="unchecked"
|
|
|
|
|
+ aria-selected="false"
|
|
|
|
|
+ >
|
|
|
|
|
+ 视力残疾
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ role="option"
|
|
|
|
|
+ data-value="hearing"
|
|
|
|
|
+ data-state="unchecked"
|
|
|
|
|
+ aria-selected="false"
|
|
|
|
|
+ >
|
|
|
|
|
+ 听力残疾
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ role="option"
|
|
|
|
|
+ data-value="physical"
|
|
|
|
|
+ data-state="unchecked"
|
|
|
|
|
+ aria-selected="false"
|
|
|
|
|
+ >
|
|
|
|
|
+ 肢体残疾
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ role="option"
|
|
|
|
|
+ data-value="intellectual"
|
|
|
|
|
+ data-state="unchecked"
|
|
|
|
|
+ aria-selected="false"
|
|
|
|
|
+ >
|
|
|
|
|
+ 智力残疾
|
|
|
|
|
+ </div>
|
|
|
|
|
+</div>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**关键 DOM 属性:**
|
|
|
|
|
+- 触发器:`role="combobox"`, `data-radix-select-trigger`
|
|
|
|
|
+- 选项列表:`role="listbox"`, `data-radix-select-content`
|
|
|
|
|
+- 单个选项:`role="option"`, `data-value`(选项值)
|
|
|
|
|
+
|
|
|
|
|
+### Radix UI 选择器策略实现细节
|
|
|
|
|
+
|
|
|
|
|
+**从 E2E Radix 测试标准文档中提取的实现细节:**
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 触发器选择器优先级
|
|
|
|
|
+const TRIGGER_SELECTORS = [
|
|
|
|
|
+ `[data-testid="${label}-trigger"]`, // 最高优先级
|
|
|
|
|
+ `text="${label}"`, // 文本匹配
|
|
|
|
|
+ `[role="combobox"]` // 兜底
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+// 选项选择器优先级
|
|
|
|
|
+const OPTION_SELECTORS = [
|
|
|
|
|
+ `[role="option"][data-value="${value}"]`, // data-value + role
|
|
|
|
|
+ `[role="option"]:has-text("${value}")` // 文本匹配兜底
|
|
|
|
|
+];
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**推荐在应用代码中添加 data-testid:**
|
|
|
|
|
+```html
|
|
|
|
|
+<!-- 推荐方式:添加 data-testid -->
|
|
|
|
|
+<button
|
|
|
|
|
+ data-testid="残疾类型-trigger"
|
|
|
|
|
+ role="combobox"
|
|
|
|
|
+ aria-label="残疾类型"
|
|
|
|
|
+>
|
|
|
|
|
+ 残疾类型
|
|
|
|
|
+</button>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 项目标准对齐
|
|
|
|
|
+
|
|
|
|
|
+**与项目标准对齐:**
|
|
|
|
|
+- 遵循 `docs/standards/testing-standards.md` 中的测试规范
|
|
|
|
|
+- 遵循 `docs/standards/e2e-radix-testing.md` 中的 Radix UI E2E 测试标准
|
|
|
|
|
+- 遵循 `docs/standards/coding-standards.md` 中的编码标准
|
|
|
|
|
+
|
|
|
|
|
+**TypeScript 配置:**
|
|
|
|
|
+- 严格模式已启用(`strict: true`),禁止 `any` 类型
|
|
|
|
|
+- 所有函数参数和返回值必须有明确类型注解
|
|
|
|
|
+
|
|
|
|
|
+**JSDoc 注释标准:**
|
|
|
|
|
+- 所有导出函数必须有完整 JSDoc
|
|
|
|
|
+- 使用 `@param`, `@throws`, `@example` 标签
|
|
|
|
|
+- 添加实际使用示例代码
|
|
|
|
|
+
|
|
|
|
|
+**命名约定:**
|
|
|
|
|
+- 函数:camelCase,动词+名词(如 `selectRadixOption`)
|
|
|
|
|
+- 内部函数:camelCase,动词开头(如 `findTrigger`, `findAndClickOption`)
|
|
|
|
|
+- 私有函数使用 `@internal` JSDoc 标记
|
|
|
|
|
+
|
|
|
|
|
+### 性能约束
|
|
|
|
|
+
|
|
|
|
|
+**从 NFR 提取的性能要求:**
|
|
|
|
|
+- **NFR8**: 静态 Select 选择操作应在 2 秒内完成(目标 < 1s)
|
|
|
|
|
+- **NFR10**: 工具函数本身的开销不超过 100ms(不包括 Playwright 操作时间)
|
|
|
|
|
+- **NFR12**: 静态选项使用合理的默认超时(2 秒),避免不必要的等待
|
|
|
|
|
+- **NFR14**: 工具函数使用 Playwright 的 auto-waiting 机制,减少显式等待的需要
|
|
|
|
|
+
|
|
|
|
|
+**实现时需要考虑:**
|
|
|
|
|
+- 使用 `DEFAULT_TIMEOUTS.static` (2000ms) 而不是硬编码
|
|
|
|
|
+- 优先使用 Playwright 的 `waitForSelector` 而不是 `waitForTimeout`
|
|
|
|
|
+- 不添加额外的显式等待(让 auto-waiting 处理)
|
|
|
|
|
+
|
|
|
|
|
+### 文件结构约束
|
|
|
|
|
+
|
|
|
|
|
+**必须遵循的文件结构:**
|
|
|
|
|
+```
|
|
|
|
|
+packages/e2e-test-utils/src/
|
|
|
|
|
+├── index.ts # 主导出(需要更新)
|
|
|
|
|
+├── types.ts # 共享类型定义(已完成)
|
|
|
|
|
+├── errors.ts # 错误类(已完成)
|
|
|
|
|
+├── constants.ts # 常量定义(已完成)
|
|
|
|
|
+└── radix-select.ts # Radix UI Select 工具(本故事创建)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**禁止事项(Anti-Patterns):**
|
|
|
|
|
+- ❌ 使用 `any` 类型
|
|
|
|
|
+- ❌ 硬编码超时值(必须使用 `DEFAULT_TIMEOUTS.static`)
|
|
|
|
|
+- ❌ 抛出原生 `Error`(必须使用 `throwError` 辅助函数)
|
|
|
|
|
+- ❌ 缺少 JSDoc 注释
|
|
|
|
|
+- ❌ 使用 `waitForTimeout`(应该依赖 auto-waiting)
|
|
|
|
|
+- ❌ 只实现单一选择器策略(必须按优先级尝试多种策略)
|
|
|
|
|
+
|
|
|
|
|
+### 测试要求
|
|
|
|
|
+
|
|
|
|
|
+**单元测试(在 Story 1.6 中实现):**
|
|
|
|
|
+本故事创建的 `selectRadixOption` 函数将在 Story 1.6 中进行单元测试。
|
|
|
|
|
+
|
|
|
|
|
+**当前验证方法:**
|
|
|
|
|
+- 类型检查:`pnpm typecheck`
|
|
|
|
|
+- JSDoc 验证:手动检查所有导出都有完整注释
|
|
|
|
|
+
|
|
|
|
|
+### 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/web-ui-testing-standards.md` 中的 Web UI 测试规范
|
|
|
|
|
+- 遵循 `docs/standards/e2e-radix-testing.md` 中的 Radix UI E2E 测试标准(核心标准文档)
|
|
|
|
|
+
|
|
|
|
|
+### References
|
|
|
|
|
+
|
|
|
|
|
+**PRD 来源:**
|
|
|
|
|
+- [PRD - E2E测试工具包](_bmad-output/planning-artifacts/prd.md) - 项目需求概述
|
|
|
|
|
+- [PRD - Radix UI Select 测试需求](_bmad-output/planning-artifacts/epics.md#radix-ui-select-测试支持-fr1-fr6) - FR1-FR6 需求
|
|
|
|
|
+
|
|
|
|
|
+**Architecture 来源:**
|
|
|
|
|
+- [Architecture - 包结构策略](_bmad-output/planning-artifacts/architecture.md#package-structure) - 按功能分组
|
|
|
|
|
+- [Architecture - API 设计模式](_bmad-output/planning-artifacts/architecture.md#api-design-pattern) - 3参数+配置对象
|
|
|
|
|
+- [Architecture - 选择器策略](_bmad-output/planning-artifacts/architecture.md#selector-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) - 核心测试标准文档,包含 DOM 结构和选择器策略
|
|
|
|
|
+- [Project Context](_bmad-output/project-context.md) - 项目技术栈和规则
|
|
|
|
|
+
|
|
|
|
|
+**Epic 来源:**
|
|
|
|
|
+- [Epic 1 - Story 1.3](_bmad-output/planning-artifacts/epics.md#story-13-实现静态-select-工具函数) - 原始用户故事和验收标准
|
|
|
|
|
+
|
|
|
|
|
+**前一个故事:**
|
|
|
|
|
+- [Story 1.1 - 创建包基础结构和配置](_bmad-output/implementation-artifacts/1-1-create-package-structure.md) - 包基础设施
|
|
|
|
|
+- [Story 1.2 - 实现类型定义和错误处理](_bmad-output/implementation-artifacts/1-2-implement-types-errors.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 测试标准文档创建
|
|
|
|
|
+- 包含完整的实现指南:触发器查找、选项查找、交互流程
|
|
|
|
|
+- 包含 DOM 结构理解和选择器策略详细说明
|
|
|
|
|
+- 为后续异步 Select 函数(Story 1.4)提供基础
|
|
|
|
|
+
|
|
|
|
|
+### File List
|
|
|
|
|
+
|
|
|
|
|
+**本故事需要创建/修改的文件:**
|
|
|
|
|
+- `packages/e2e-test-utils/src/radix-select.ts` - 静态 Select 工具函数(新建)
|
|
|
|
|
+- `packages/e2e-test-utils/src/index.ts` - 更新导出(修改)
|
|
|
|
|
+
|
|
|
|
|
+**相关文件(已在 Story 1.1、1.2 中完成,本故事使用):**
|
|
|
|
|
+- `packages/e2e-test-utils/src/types.ts` - 共享类型定义
|
|
|
|
|
+- `packages/e2e-test-utils/src/errors.ts` - 错误类和错误处理
|
|
|
|
|
+- `packages/e2e-test-utils/src/constants.ts` - 超时和选择器策略常量
|
|
|
|
|
+
|
|
|
|
|
+**只读参考文件:**
|
|
|
|
|
+- `_bmad-output/implementation-artifacts/1-1-create-package-structure.md` - 前一个故事
|
|
|
|
|
+- `_bmad-output/implementation-artifacts/1-2-implement-types-errors.md` - 前一个故事
|
|
|
|
|
+- `_bmad-output/planning-artifacts/epics.md` - Epic 和故事定义
|
|
|
|
|
+- `_bmad-output/planning-artifacts/architecture.md` - 架构决策和模式
|
|
|
|
|
+- `docs/standards/e2e-radix-testing.md` - E2E Radix UI 测试标准
|
|
|
|
|
+
|
|
|
|
|
+## Change Log
|
|
|
|
|
+
|
|
|
|
|
+### 2026-01-08 - Story 1.3 创建
|
|
|
|
|
+- ✅ 故事文档创建完成
|
|
|
|
|
+- ✅ 包含完整的实现指南
|
|
|
|
|
+- ✅ 包含 DOM 结构和选择器策略详细说明
|
|
|
|
|
+- ✅ 状态设置为 ready-for-dev
|