Przeglądaj źródła

fix: 修复统一广告管理UI包测试配置和规范文档补充

修复内容:
- tsconfig.json: 添加 jsx: "react-jsx" 和 lib: ["ES2022", "DOM", "DOM.Iterable"] 配置
- tests/setup.ts: 修复 ResizeObserver mock 使用 class 模式以支持 Radix UI 的 react-use-size
- tests/integration/*.tsx: 添加 React 导入,改用 renderWithProviders 函数模式
- docs/architecture/ui-package-standards.md: 补充 ResizeObserver mock 规范说明

测试结果: 广告类型管理测试 7/7 全部通过

🤖 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 2 tygodni temu
rodzic
commit
854c735f66

+ 15 - 0
docs/architecture/ui-package-standards.md

@@ -493,8 +493,23 @@ vi.mock('sonner', () => ({
 
 // Mock scrollIntoView for Radix UI components
 Element.prototype.scrollIntoView = vi.fn();
+
+// Mock ResizeObserver (必须使用 class 模式)
+// 注意:@radix-ui/react-use-size 等组件需要 ResizeObserver 是构造函数
+global.ResizeObserver = class MockResizeObserver {
+  constructor(callback: ResizeObserverCallback) {
+    (this as any).callback = callback;
+  }
+  observe() {}
+  unobserve() {}
+  disconnect() {}
+};
 ```
 
+**重要说明**:
+- **必须使用 class 模式**:`@radix-ui/react-use-size` 等 Radix UI 组件使用 `new ResizeObserver()`,因此必须 mock 为构造函数
+- **不要使用函数模式**:`vi.fn().mockImplementation(() => ({...}))` 返回的是对象而非构造函数,会导致 `TypeError: ... is not a constructor` 错误
+
 **Select组件test ID规范**:为Radix UI Select组件的选项添加test ID,避免文本查找冲突。
 
 ```typescript

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

@@ -388,6 +388,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) |
 
 ## Dev Agent Record
 
@@ -398,6 +399,9 @@ Claude Opus 4.5 (model ID: claude-opus-4-5-20251101)
 - RPC 客户端路径问题:发现后端模块在故事 010.003 中已修复路径规范(从 `/api/v1/admin/...` 改为相对路径 `/`)
 - Hono RPC 客户端解析:发现路由使用 `index` 属性作为根路径的别名,调用方式为 `client.index.$get`
 - param 类型问题:Hono RPC 客户端的 `:id` 参数期望 string 类型,需要将 number 转换为 string
+- **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`
 
 ### Completion Notes List
 1. **包结构**:完整创建 `packages/unified-advertisement-management-ui` 包
@@ -408,8 +412,11 @@ Claude Opus 4.5 (model ID: claude-opus-4-5-20251101)
    - `UnifiedAdvertisementTypeManagement`:广告类型管理组件
    - `UnifiedAdvertisementTypeSelector`:广告类型选择器组件
 5. **表单设计**:使用条件渲染两个独立的 Form 组件(`isCreateForm ? <Form {...createForm}> : <Form {...updateForm}>`)
-6. **测试**:创建了集成测试文件
+6. **测试**:创建了集成测试文件,使用 `renderWithProviders` 模式
 7. **路径修复**:根据故事 010.003 的修复,使用 `client.index.$get` 调用方式
+8. **配置修复**:修复 tsconfig.json 添加 JSX 和 DOM 库配置
+9. **测试修复**:添加 React 导入,改用 `renderWithProviders` 函数模式,修复 ResizeObserver mock 为 class 模式
+10. **规范更新**:在 `docs/architecture/ui-package-standards.md` 中补充 ResizeObserver mock 规范
 
 ### File List
 **新增文件**:
@@ -429,5 +436,9 @@ Claude Opus 4.5 (model ID: claude-opus-4-5-20251101)
 - `packages/unified-advertisement-management-ui/tests/integration/unified-advertisement-management.integration.test.tsx`
 - `packages/unified-advertisement-management-ui/tests/integration/unified-advertisement-type-management.integration.test.tsx`
 
+**修改文件**:
+- `docs/architecture/ui-package-standards.md` - 补充 ResizeObserver mock 规范
+- `docs/stories/010.002.story.md` - 更新 Dev Agent Record
+
 ## QA Results
 _待QA代理填写_

+ 26 - 21
packages/unified-advertisement-management-ui/tests/integration/unified-advertisement-management.integration.test.tsx

@@ -1,3 +1,4 @@
+import React from 'react';
 import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { render, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
@@ -40,23 +41,27 @@ vi.mock('@d8d/shared-ui-components/components/admin/DataTablePagination', () =>
   )
 }));
 
-const createTestQueryClient = () => new QueryClient({
-  defaultOptions: {
-    queries: {
-      retry: false,
-      gcTime: 0
-    },
-    mutations: {
-      retry: false
+const createTestQueryClient = () =>
+  new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+        gcTime: 0
+      },
+      mutations: {
+        retry: false
+      }
     }
-  }
-});
+  });
 
-const wrapper = ({ children }: { children: React.ReactNode }) => (
-  <QueryClientProvider client={createTestQueryClient()}>
-    {children}
-  </QueryClientProvider>
-});
+const renderWithProviders = (component: React.ReactElement) => {
+  const queryClient = createTestQueryClient();
+  return render(
+    <QueryClientProvider client={queryClient}>
+      {component as any}
+    </QueryClientProvider>
+  );
+};
 
 describe('UnifiedAdvertisementManagement - 集成测试', () => {
   const mockGetClient = vi.mocked(unifiedAdvertisementClientManager.get);
@@ -132,7 +137,7 @@ describe('UnifiedAdvertisementManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementManagement />);
 
       await waitFor(() => {
         expect(screen.getByText('统一广告管理')).toBeInTheDocument();
@@ -177,7 +182,7 @@ describe('UnifiedAdvertisementManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementManagement />);
 
       await waitFor(() => {
         expect(screen.getByText('暂无广告数据')).toBeInTheDocument();
@@ -230,7 +235,7 @@ describe('UnifiedAdvertisementManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementManagement />);
 
       // 点击创建按钮
       await user.click(screen.getByTestId('create-unified-advertisement-button'));
@@ -332,7 +337,7 @@ describe('UnifiedAdvertisementManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementManagement />);
 
       // 等待列表加载
       await waitFor(() => {
@@ -433,7 +438,7 @@ describe('UnifiedAdvertisementManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementManagement />);
 
       // 等待列表加载
       await waitFor(() => {
@@ -519,7 +524,7 @@ describe('UnifiedAdvertisementManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementManagement />);
 
       // 输入搜索关键词
       const searchInput = screen.getByTestId('search-input');

+ 27 - 22
packages/unified-advertisement-management-ui/tests/integration/unified-advertisement-type-management.integration.test.tsx

@@ -1,3 +1,4 @@
+import React from 'react';
 import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { render, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
@@ -23,23 +24,27 @@ vi.mock('@d8d/shared-ui-components/components/admin/DataTablePagination', () =>
   )
 }));
 
-const createTestQueryClient = () => new QueryClient({
-  defaultOptions: {
-    queries: {
-      retry: false,
-      gcTime: 0
-    },
-    mutations: {
-      retry: false
+const createTestQueryClient = () =>
+  new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+        gcTime: 0
+      },
+      mutations: {
+        retry: false
+      }
     }
-  }
-});
+  });
 
-const wrapper = ({ children }: { children: React.ReactNode }) => (
-  <QueryClientProvider client={createTestQueryClient()}>
-    {children}
-  </QueryClientProvider>
-});
+const renderWithProviders = (component: React.ReactElement) => {
+  const queryClient = createTestQueryClient();
+  return render(
+    <QueryClientProvider client={queryClient}>
+      {component as any}
+    </QueryClientProvider>
+  );
+};
 
 describe('UnifiedAdvertisementTypeManagement - 集成测试', () => {
   const mockGetClient = vi.mocked(unifiedAdvertisementTypeClientManager.get);
@@ -85,7 +90,7 @@ describe('UnifiedAdvertisementTypeManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementTypeManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementTypeManagement />);
 
       await waitFor(() => {
         expect(screen.getByText('统一广告类型管理')).toBeInTheDocument();
@@ -118,7 +123,7 @@ describe('UnifiedAdvertisementTypeManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementTypeManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementTypeManagement />);
 
       await waitFor(() => {
         expect(screen.getByText('暂无广告类型数据')).toBeInTheDocument();
@@ -154,7 +159,7 @@ describe('UnifiedAdvertisementTypeManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementTypeManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementTypeManagement />);
 
       // 点击创建按钮
       await user.click(screen.getByTestId('create-unified-advertisement-type-button'));
@@ -225,7 +230,7 @@ describe('UnifiedAdvertisementTypeManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementTypeManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementTypeManagement />);
 
       // 等待列表加载
       await waitFor(() => {
@@ -304,7 +309,7 @@ describe('UnifiedAdvertisementTypeManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementTypeManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementTypeManagement />);
 
       // 等待列表加载
       await waitFor(() => {
@@ -368,7 +373,7 @@ describe('UnifiedAdvertisementTypeManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementTypeManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementTypeManagement />);
 
       // 输入搜索关键词
       const searchInput = screen.getByTestId('search-input');
@@ -416,7 +421,7 @@ describe('UnifiedAdvertisementTypeManagement - 集成测试', () => {
         }
       } as any);
 
-      render(<UnifiedAdvertisementTypeManagement />, { wrapper });
+      renderWithProviders(<UnifiedAdvertisementTypeManagement />);
 
       // 点击创建按钮
       await user.click(screen.getByTestId('create-unified-advertisement-type-button'));

+ 10 - 6
packages/unified-advertisement-management-ui/tests/setup.ts

@@ -21,9 +21,13 @@ global.IntersectionObserver = vi.fn().mockImplementation(() => ({
   disconnect: vi.fn()
 })) as any;
 
-// Mock ResizeObserver
-global.ResizeObserver = vi.fn().mockImplementation(() => ({
-  observe: vi.fn(),
-  unobserve: vi.fn(),
-  disconnect: vi.fn()
-})) as any;
+// Mock ResizeObserver (使用 class 模式以支持 Radix UI 的 react-use-size)
+global.ResizeObserver = class MockResizeObserver {
+  constructor(callback: ResizeObserverCallback) {
+    // Store callback for testing
+    (this as any).callback = callback;
+  }
+  observe() {}
+  unobserve() {}
+  disconnect() {}
+};

+ 3 - 2
packages/unified-advertisement-management-ui/tsconfig.json

@@ -3,10 +3,11 @@
   "compilerOptions": {
     "composite": true,
     "outDir": "./dist",
-    "rootDir": "./src",
     "declaration": true,
     "declarationMap": true,
-    "sourceMap": true
+    "sourceMap": true,
+    "jsx": "react-jsx",
+    "lib": ["ES2022", "DOM", "DOM.Iterable"]
   },
   "include": [
     "src/**/*"