Selaa lähdekoodia

feat(area-management-ui): 添加可复用的测试工具包

- 创建 test-utils 导出,提供可复用的测试工具
- 实现 mockAreaClient 工具,简化 areaClient API 的mock
- 提供 mockAreaData 包含预设的地区测试数据
- 添加 TestWrapper 组件包装测试环境
- 更新 package.json 导出配置
- 提供详细的使用示例文档

其他包现在可以通过 @d8d/area-management-ui/test-utils 导入测试工具,
无需重复创建 areaClient 的mock逻辑。

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

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 2 viikkoa sitten
vanhempi
sitoutus
e0b91f6caa

+ 7 - 1
packages/area-management-ui/package.json

@@ -25,10 +25,16 @@
       "types": "./src/api/index.ts",
       "import": "./src/api/index.ts",
       "require": "./src/api/index.ts"
+    },
+    "./test-utils": {
+      "types": "./test/utils/index.ts",
+      "import": "./test/utils/index.ts",
+      "require": "./test/utils/index.ts"
     }
   },
   "files": [
-    "src"
+    "src",
+    "test/utils"
   ],
   "scripts": {
     "build": "unbuild",

+ 43 - 0
packages/area-management-ui/test/utils/TestWrapper.tsx

@@ -0,0 +1,43 @@
+/**
+ * 测试包装组件
+ * 提供测试环境所需的Provider包装
+ */
+
+import React from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { BrowserRouter } from 'react-router';
+
+interface TestWrapperProps {
+  children: React.ReactNode;
+  queryClient?: QueryClient;
+  withRouter?: boolean;
+}
+
+const TestWrapper: React.FC<TestWrapperProps> = ({
+  children,
+  queryClient,
+  withRouter = true,
+}) => {
+  const testQueryClient = queryClient || new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+        gcTime: 0,
+      },
+    },
+  });
+
+  const content = (
+    <QueryClientProvider client={testQueryClient}>
+      {children}
+    </QueryClientProvider>
+  );
+
+  if (withRouter) {
+    return <BrowserRouter>{content}</BrowserRouter>;
+  }
+
+  return content;
+};
+
+export default TestWrapper;

+ 13 - 0
packages/area-management-ui/test/utils/index.ts

@@ -0,0 +1,13 @@
+/**
+ * area-management-ui 测试工具包
+ * 提供可复用的测试工具,供其他包在测试中使用
+ */
+
+export * from './mockAreaData';
+export * from './mockAreaClient';
+
+// 常用测试工具
+export { default as TestWrapper } from './TestWrapper';
+
+// 类型导出
+export type { AreaTestData } from './mockAreaData';

+ 212 - 0
packages/area-management-ui/test/utils/mockAreaClient.ts

@@ -0,0 +1,212 @@
+/**
+ * areaClient 测试mock工具
+ * 用于在测试中模拟areaClient API调用
+ */
+
+import { vi } from 'vitest';
+import { mockAllAreas, filterAreas, getPaginatedData, type AreaTestData } from './mockAreaData';
+
+// 完整的mock响应对象 - 按照用户UI包规范
+export const createMockResponse = (status: number, data?: any) => ({
+  status,
+  ok: status >= 200 && status < 300,
+  body: null,
+  bodyUsed: false,
+  statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
+  headers: new Headers(),
+  url: '',
+  redirected: false,
+  type: 'basic' as ResponseType,
+  json: async () => data || {},
+  text: async () => '',
+  blob: async () => new Blob(),
+  arrayBuffer: async () => new ArrayBuffer(0),
+  formData: async () => new FormData(),
+  clone: function() { return this; }
+});
+
+// 创建mock的areaClient
+export const createMockAreaClient = (customData?: AreaTestData[]) => {
+  const areas = customData || mockAllAreas;
+
+  // 模拟 $get 方法
+  const mockGet = vi.fn(async (options?: { query?: any }) => {
+    const { query = {} } = options || {};
+    const { page = 1, pageSize = 10, filters = '{}', sortBy = 'id', sortOrder = 'ASC' } = query;
+
+    // 解析filters
+    let parsedFilters = {};
+    try {
+      parsedFilters = JSON.parse(filters);
+    } catch {
+      parsedFilters = {};
+    }
+
+    // 筛选数据
+    let filteredAreas = [...areas];
+
+    // 应用筛选条件
+    if (parsedFilters) {
+      filteredAreas = filterAreas(areas, parsedFilters);
+    }
+
+    // 排序
+    filteredAreas.sort((a, b) => {
+      const aValue = a[sortBy as keyof AreaTestData];
+      const bValue = b[sortBy as keyof AreaTestData];
+
+      if (typeof aValue === 'number' && typeof bValue === 'number') {
+        return sortOrder === 'ASC' ? aValue - bValue : bValue - aValue;
+      }
+
+      if (typeof aValue === 'string' && typeof bValue === 'string') {
+        return sortOrder === 'ASC'
+          ? aValue.localeCompare(bValue)
+          : bValue.localeCompare(aValue);
+      }
+
+      return 0;
+    });
+
+    // 分页
+    const paginated = getPaginatedData(filteredAreas, page, pageSize);
+
+    return Promise.resolve(createMockResponse(200, paginated));
+  });
+
+  // 模拟 $post 方法
+  const mockPost = vi.fn(async (options?: { json?: any }) => {
+    const { json = {} } = options || {};
+    const newId = Math.max(...areas.map(a => a.id)) + 1;
+    const newArea: AreaTestData = {
+      id: newId,
+      name: json.name || '新地区',
+      level: json.level || 1,
+      parentId: json.parentId,
+      isDisabled: json.isDisabled || 0,
+      sort: json.sort || 0,
+    };
+
+    areas.push(newArea);
+
+    return Promise.resolve(createMockResponse(201, newArea));
+  });
+
+  // 模拟 $put 方法
+  const mockPut = vi.fn(async (options?: { param?: any; json?: any }) => {
+    const { param = {}, json = {} } = options || {};
+    const id = param.id ? parseInt(param.id) : 0;
+
+    const index = areas.findIndex(area => area.id === id);
+    if (index === -1) {
+      return Promise.resolve(createMockResponse(404, { error: '地区不存在' }));
+    }
+
+    areas[index] = { ...areas[index], ...json };
+
+    return Promise.resolve(createMockResponse(200, areas[index]));
+  });
+
+  // 模拟 $delete 方法
+  const mockDelete = vi.fn(async (options?: { param?: any }) => {
+    const { param = {} } = options || {};
+    const id = param.id ? parseInt(param.id) : 0;
+
+    const index = areas.findIndex(area => area.id === id);
+    if (index === -1) {
+      return Promise.resolve(createMockResponse(404, { error: '地区不存在' }));
+    }
+
+    areas.splice(index, 1);
+
+    return Promise.resolve(createMockResponse(204));
+  });
+
+  return {
+    index: {
+      $get: mockGet,
+      $post: mockPost,
+    },
+    ':id': {
+      $put: mockPut,
+      $delete: mockDelete,
+    },
+  };
+};
+
+// 创建mock的areaClientManager
+export const createMockAreaClientManager = (customData?: AreaTestData[]) => {
+  const mockClient = createMockAreaClient(customData);
+
+  return {
+    getInstance: vi.fn(() => ({
+      get: vi.fn(() => mockClient),
+      reset: vi.fn(),
+    })),
+    get: vi.fn(() => mockClient),
+    reset: vi.fn(),
+  };
+};
+
+// 完整的mock配置(用于vi.mock)
+export const createAreaClientMock = (customData?: AreaTestData[]) => {
+  const mockClient = createMockAreaClient(customData);
+  const mockManager = createMockAreaClientManager(customData);
+
+  return {
+    areaClient: mockClient,
+    areaClientManager: mockManager,
+  };
+};
+
+// 预设的mock配置(快速使用)
+export const presetMocks = {
+  // 基本成功mock
+  success: () => createAreaClientMock(),
+
+  // 空数据mock
+  empty: () => createAreaClientMock([]),
+
+  // 错误mock
+  error: (errorType: 'network' | 'server' | 'notFound' = 'server') => {
+    const mockClient = {
+      index: {
+        $get: vi.fn(() => {
+          if (errorType === 'network') {
+            return Promise.reject(new Error('Network Error'));
+          }
+          return Promise.resolve(createMockResponse(500, { error: '服务器错误' }));
+        }),
+        $post: vi.fn(() => Promise.resolve(createMockResponse(500, { error: '创建失败' }))),
+      },
+      ':id': {
+        $put: vi.fn(() => {
+          if (errorType === 'notFound') {
+            return Promise.resolve(createMockResponse(404, { error: '地区不存在' }));
+          }
+          return Promise.resolve(createMockResponse(500, { error: '更新失败' }));
+        }),
+        $delete: vi.fn(() => {
+          if (errorType === 'notFound') {
+            return Promise.resolve(createMockResponse(404, { error: '地区不存在' }));
+          }
+          return Promise.resolve(createMockResponse(500, { error: '删除失败' }));
+        }),
+      },
+    };
+
+    const mockManager = {
+      getInstance: vi.fn(() => ({
+        get: vi.fn(() => mockClient),
+        reset: vi.fn(),
+      })),
+      get: vi.fn(() => mockClient),
+      reset: vi.fn(),
+    };
+
+    return {
+      areaClient: mockClient,
+      areaClientManager: mockManager,
+    };
+  },
+};

+ 83 - 0
packages/area-management-ui/test/utils/mockAreaData.ts

@@ -0,0 +1,83 @@
+/**
+ * 地区测试数据
+ * 用于在测试中模拟地区数据
+ */
+
+export interface AreaTestData {
+  id: number;
+  name: string;
+  level: number;
+  parentId?: number;
+  isDisabled?: number;
+  sort?: number;
+}
+
+// 省份数据(level: 1)
+export const mockProvinces: AreaTestData[] = [
+  { id: 1, name: '北京市', level: 1, isDisabled: 0, sort: 1 },
+  { id: 2, name: '上海市', level: 1, isDisabled: 0, sort: 2 },
+  { id: 3, name: '广东省', level: 1, isDisabled: 0, sort: 3 },
+  { id: 4, name: '江苏省', level: 1, isDisabled: 0, sort: 4 },
+  { id: 5, name: '浙江省', level: 1, isDisabled: 0, sort: 5 },
+];
+
+// 城市数据(level: 2)
+export const mockCities: AreaTestData[] = [
+  { id: 11, name: '北京市', level: 2, parentId: 1, isDisabled: 0, sort: 1 },
+  { id: 12, name: '上海市', level: 2, parentId: 2, isDisabled: 0, sort: 1 },
+  { id: 13, name: '广州市', level: 2, parentId: 3, isDisabled: 0, sort: 1 },
+  { id: 14, name: '深圳市', level: 2, parentId: 3, isDisabled: 0, sort: 2 },
+  { id: 15, name: '南京市', level: 2, parentId: 4, isDisabled: 0, sort: 1 },
+  { id: 16, name: '苏州市', level: 2, parentId: 4, isDisabled: 0, sort: 2 },
+  { id: 17, name: '杭州市', level: 2, parentId: 5, isDisabled: 0, sort: 1 },
+  { id: 18, name: '宁波市', level: 2, parentId: 5, isDisabled: 0, sort: 2 },
+];
+
+// 区县数据(level: 3)
+export const mockDistricts: AreaTestData[] = [
+  { id: 101, name: '东城区', level: 3, parentId: 11, isDisabled: 0, sort: 1 },
+  { id: 102, name: '西城区', level: 3, parentId: 11, isDisabled: 0, sort: 2 },
+  { id: 103, name: '黄浦区', level: 3, parentId: 12, isDisabled: 0, sort: 1 },
+  { id: 104, name: '徐汇区', level: 3, parentId: 12, isDisabled: 0, sort: 2 },
+  { id: 105, name: '天河区', level: 3, parentId: 13, isDisabled: 0, sort: 1 },
+  { id: 106, name: '越秀区', level: 3, parentId: 13, isDisabled: 0, sort: 2 },
+  { id: 107, name: '福田区', level: 3, parentId: 14, isDisabled: 0, sort: 1 },
+  { id: 108, name: '南山区', level: 3, parentId: 14, isDisabled: 0, sort: 2 },
+];
+
+// 所有地区数据
+export const mockAllAreas: AreaTestData[] = [
+  ...mockProvinces,
+  ...mockCities,
+  ...mockDistricts,
+];
+
+// 根据条件筛选地区数据
+export const filterAreas = (
+  areas: AreaTestData[],
+  filters?: { level?: number; parentId?: number; isDisabled?: number }
+): AreaTestData[] => {
+  return areas.filter(area => {
+    if (filters?.level !== undefined && area.level !== filters.level) return false;
+    if (filters?.parentId !== undefined && area.parentId !== filters.parentId) return false;
+    if (filters?.isDisabled !== undefined && area.isDisabled !== filters.isDisabled) return false;
+    return true;
+  });
+};
+
+// 获取分页数据
+export const getPaginatedData = (
+  areas: AreaTestData[],
+  page: number = 1,
+  pageSize: number = 10
+) => {
+  const start = (page - 1) * pageSize;
+  const end = start + pageSize;
+  return {
+    data: areas.slice(start, end),
+    total: areas.length,
+    page,
+    pageSize,
+    totalPages: Math.ceil(areas.length / pageSize),
+  };
+};

+ 120 - 0
packages/area-management-ui/test/utils/usage-example.ts

@@ -0,0 +1,120 @@
+/**
+ * 测试工具使用示例
+ * 展示如何在其他包中使用 area-management-ui 的测试工具
+ */
+
+import { vi } from 'vitest';
+
+// 示例1:基本使用
+export const example1 = `
+// 在残疾人管理UI包的测试中
+import { createAreaClientMock, mockAreaData } from '@d8d/area-management-ui/test-utils';
+
+// Mock areaClient API
+vi.mock('@d8d/area-management-ui/api', () => createAreaClientMock());
+
+// 或者使用预设数据
+vi.mock('@d8d/area-management-ui/api', () => createAreaClientMock(mockAreaData.mockAllAreas));
+`;
+
+// 示例2:使用预设mock
+export const example2 = `
+import { presetMocks } from '@d8d/area-management-ui/test-utils';
+
+// 使用成功mock
+vi.mock('@d8d/area-management-ui/api', () => presetMocks.success());
+
+// 使用空数据mock
+vi.mock('@d8d/area-management-ui/api', () => presetMocks.empty());
+
+// 使用错误mock
+vi.mock('@d8d/area-management-ui/api', () => presetMocks.error('network'));
+`;
+
+// 示例3:完整测试示例
+export const example3 = `
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { createAreaClientMock, TestWrapper } from '@d8d/area-management-ui/test-utils';
+import YourComponent from './YourComponent';
+
+// Mock areaClient
+vi.mock('@d8d/area-management-ui/api', () => createAreaClientMock());
+
+describe('YourComponent', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该正确渲染', () => {
+    render(
+      <TestWrapper>
+        <YourComponent />
+      </TestWrapper>
+    );
+
+    expect(screen.getByText('Some content')).toBeInTheDocument();
+  });
+});
+`;
+
+// 示例4:在残疾人管理UI包中的实际使用
+export const disabilityManagementExample = `
+// 文件:allin-packages/disability-person-management-ui/tests/integration/disability-person.integration.test.tsx
+
+import { createAreaClientMock } from '@d8d/area-management-ui/test-utils';
+
+// 替换原有的复杂mock
+// 原来的:
+// vi.mock('@d8d/area-management-ui/components', () => ({
+//   AreaSelect: vi.fn(...),
+//   AreaSelectForm: vi.fn(...),
+// }));
+
+// 现在的:
+vi.mock('@d8d/area-management-ui/api', () => createAreaClientMock());
+
+// 注意:现在我们不需要mock UI组件了!
+// 可以直接使用真实的 AreaSelect 和 AreaSelectForm 组件
+// 因为它们会使用我们mock的API
+`;
+
+// 示例5:在强权管理UI包中的使用
+export const orderManagementExample = `
+// 文件:allin-packages/order-management-ui/tests/integration/order.integration.test.tsx
+
+import { createAreaClientMock } from '@d8d/area-management-ui/test-utils';
+
+// 替换原有的mock
+// 原来的:
+// vi.mock('@d8d/area-management-ui', () => ({
+//   AreaSelect: vi.fn(({ value, onChange }) => (...)),
+// }));
+
+// 现在的:
+vi.mock('@d8d/area-management-ui/api', () => createAreaClientMock());
+
+// 现在可以直接使用真实的 AreaSelect 组件
+// 组件会从mock的API获取数据
+`;
+
+// 优势总结
+export const advantages = `
+## 使用测试工具的优势
+
+1. **一致性**:所有包使用相同的mock逻辑
+2. **维护性**:mock逻辑集中在一处,易于更新
+3. **真实性**:使用真实组件,测试更接近实际使用
+4. **简化测试**:减少重复的mock代码
+5. **类型安全**:提供完整的TypeScript类型支持
+6. **灵活性**:支持自定义数据和错误场景
+
+## 迁移步骤
+
+1. 安装或更新 area-management-ui 包
+2. 导入测试工具:import { createAreaClientMock } from '@d8d/area-management-ui/test-utils'
+3. 替换原有的 areaClient mock
+4. 移除对 UI 组件的 mock(AreaSelect, AreaSelectForm)
+5. 运行测试,确保一切正常
+`;