Parcourir la source

fix: 修复统一广告管理UI包Select组件测试问题 (Story 010.002)

- 添加pointer events mock (hasPointerCapture, releasePointerCapture, setPointerCapture)
- 在创建广告测试中添加选择广告类型的步骤
- 更新UI包开发规范,补充pointer events mock说明

🤖 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 il y a 2 semaines
Parent
commit
cdc678b166

+ 11 - 2
docs/architecture/ui-package-standards.md

@@ -3,6 +3,7 @@
 ## 版本信息
 | 版本 | 日期 | 描述 | 作者 |
 |------|------|------|------|
+| 1.2 | 2026-01-03 | 添加Radix UI Select组件pointer events mock规范(基于故事010.002经验) | James (Claude Code) |
 | 1.1 | 2025-12-04 | 添加Radix UI组件测试环境修复规范(基于故事008.007经验) | James |
 | 1.0 | 2025-12-03 | 基于史诗008经验创建UI包规范 | Claude Code |
 
@@ -469,10 +470,10 @@ data-testid="edit-channel-button-1"
 data-testid="delete-confirm-dialog-title"
 ```
 
-#### 4. Radix UI组件测试环境修复(基于故事008.007经验)
+#### 4. Radix UI组件测试环境修复(基于故事008.007、010.002经验)
 **规范**:在测试环境中使用Radix UI组件(特别是Select、DropdownMenu等)时,必须添加必要的DOM API mock。
 
-**问题**:Radix UI组件在测试环境中可能缺少某些DOM API(如`scrollIntoView`),导致测试失败。
+**问题**:Radix UI组件在测试环境中可能缺少某些DOM API(如`scrollIntoView`、`hasPointerCapture`),导致测试失败或产生未处理错误
 
 **解决方案**:在测试setup文件中添加必要的mock。
 
@@ -494,6 +495,12 @@ vi.mock('sonner', () => ({
 // Mock scrollIntoView for Radix UI components
 Element.prototype.scrollIntoView = vi.fn();
 
+// Mock pointer events for Radix UI Select component
+// Select组件使用 userEvent.click() 时会触发 pointer events
+Element.prototype.hasPointerCapture = vi.fn(() => true) as any;
+Element.prototype.releasePointerCapture = vi.fn() as any;
+Element.prototype.setPointerCapture = vi.fn() as any;
+
 // Mock ResizeObserver (必须使用 class 模式)
 // 注意:@radix-ui/react-use-size 等组件需要 ResizeObserver 是构造函数
 global.ResizeObserver = class MockResizeObserver {
@@ -509,6 +516,8 @@ global.ResizeObserver = class MockResizeObserver {
 **重要说明**:
 - **必须使用 class 模式**:`@radix-ui/react-use-size` 等 Radix UI 组件使用 `new ResizeObserver()`,因此必须 mock 为构造函数
 - **不要使用函数模式**:`vi.fn().mockImplementation(() => ({...}))` 返回的是对象而非构造函数,会导致 `TypeError: ... is not a constructor` 错误
+- **Pointer events mock**:使用 `userEvent.click()` 测试 Select 组件时必须 mock pointer events,否则会报 `TypeError: target.hasPointerCapture is not a function` 错误
+- **推荐使用 userEvent**:相比 `fireEvent`,`userEvent.click()` 更真实地模拟用户交互,触发完整的浏览器事件流
 
 **Select组件test ID规范**:为Radix UI Select组件的选项添加test ID,避免文本查找冲突。
 

+ 9 - 1
docs/stories/010.002.story.md

@@ -389,6 +389,7 @@ pnpm typecheck
 | 2026-01-03 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
 | 2026-01-03 | 1.1 | 完成所有开发任务 | James (Claude Code) |
 | 2026-01-03 | 1.2 | 修复测试配置和规范文档补充 | James (Claude Code) |
+| 2026-01-03 | 1.3 | 修复Select组件测试(添加typeId选择步骤和pointer events mock) | James (Claude Code) |
 
 ## Dev Agent Record
 
@@ -402,6 +403,8 @@ Claude Opus 4.5 (model ID: claude-opus-4-5-20251101)
 - **tsconfig.json 配置缺失**:缺少 `jsx: "react-jsx"` 和 `lib: ["ES2022", "DOM", "DOM.Iterable"]` 配置
 - **测试语法错误**:测试文件缺少 `React` 导入,且 wrapper 函数定义方式导致 esbuild 解析错误
 - **ResizeObserver mock 模式错误**:使用 `vi.fn().mockImplementation()` 返回对象而非构造函数,导致 Radix UI 的 `@radix-ui/react-use-size` 报错 `TypeError: ... is not a constructor`
+- **Select组件测试失败根因**:创建广告测试中未选择必填的 `typeId` 字段,导致表单验证失败,API 调用从未触发
+- **Pointer events mock 缺失**:使用 `userEvent.click()` 测试 Radix UI Select 组件时,必须 mock `hasPointerCapture`、`releasePointerCapture`、`setPointerCapture`,否则报 `TypeError: target.hasPointerCapture is not a function`
 
 ### Completion Notes List
 1. **包结构**:完整创建 `packages/unified-advertisement-management-ui` 包
@@ -417,6 +420,9 @@ Claude Opus 4.5 (model ID: claude-opus-4-5-20251101)
 8. **配置修复**:修复 tsconfig.json 添加 JSX 和 DOM 库配置
 9. **测试修复**:添加 React 导入,改用 `renderWithProviders` 函数模式,修复 ResizeObserver mock 为 class 模式
 10. **规范更新**:在 `docs/architecture/ui-package-standards.md` 中补充 ResizeObserver mock 规范
+11. **Select测试修复**:在创建广告测试中添加选择广告类型的步骤(`type-selector-trigger` → `type-selector-item-1`)
+12. **Pointer events mock**:在测试 setup 中添加 `hasPointerCapture`、`releasePointerCapture`、`setPointerCapture` mock
+13. **规范更新**:在 `docs/architecture/ui-package-standards.md` 中补充 pointer events mock 规范,强调使用 `userEvent` 而非 `fireEvent`
 
 ### File List
 **新增文件**:
@@ -437,8 +443,10 @@ Claude Opus 4.5 (model ID: claude-opus-4-5-20251101)
 - `packages/unified-advertisement-management-ui/tests/integration/unified-advertisement-type-management.integration.test.tsx`
 
 **修改文件**:
-- `docs/architecture/ui-package-standards.md` - 补充 ResizeObserver mock 规范
+- `docs/architecture/ui-package-standards.md` - 补充 ResizeObserver mock 规范,添加 pointer events mock 规范
 - `docs/stories/010.002.story.md` - 更新 Dev Agent Record
+- `packages/unified-advertisement-management-ui/tests/setup.ts` - 添加 pointer events mock(hasPointerCapture、releasePointerCapture、setPointerCapture)
+- `packages/unified-advertisement-management-ui/tests/integration/unified-advertisement-management.integration.test.tsx` - 在创建广告测试中添加选择广告类型的步骤
 
 ## QA Results
 _待QA代理填写_

+ 4 - 0
packages/unified-advertisement-management-ui/tests/integration/unified-advertisement-management.integration.test.tsx

@@ -253,6 +253,10 @@ describe('UnifiedAdvertisementManagement - 集成测试', () => {
       const codeInput = screen.getByTestId('code-input');
       await user.type(codeInput, 'new_ad');
 
+      // 选择广告类型(必填字段)
+      await user.click(screen.getByTestId('type-selector-trigger'));
+      await user.click(screen.getByTestId('type-selector-item-1'));
+
       // 提交表单
       const submitButton = screen.getByTestId('create-submit-button');
       await user.click(submitButton);

+ 5 - 0
packages/unified-advertisement-management-ui/tests/setup.ts

@@ -14,6 +14,11 @@ vi.mock('sonner', () => ({
 // Mock scrollIntoView for Radix UI components
 Element.prototype.scrollIntoView = vi.fn();
 
+// Mock pointer events for Radix UI Select component
+Element.prototype.hasPointerCapture = vi.fn(() => true) as any;
+Element.prototype.releasePointerCapture = vi.fn() as any;
+Element.prototype.setPointerCapture = vi.fn() as any;
+
 // Mock IntersectionObserver
 global.IntersectionObserver = vi.fn().mockImplementation(() => ({
   observe: vi.fn(),