Bläddra i källkod

✅ test(goods-category-tree-management-ui-mt): 添加react-router依赖并完成集成测试

- 添加react-router依赖以支持测试包装器
- 集成测试全部通过,验证商品分类树形管理功能
- 包含完整的CRUD操作和多级分类测试

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 1 månad sedan
förälder
incheckning
9797471b72

+ 1 - 0
packages/goods-category-management-ui-mt/package.json

@@ -45,6 +45,7 @@
     "@d8d/file-management-ui-mt": "workspace:*",
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
+    "react-router": "^7.1.3",
     "@tanstack/react-query": "^5.90.9",
     "react-hook-form": "^7.61.1",
     "hono": "^4.8.5",

+ 602 - 0
packages/goods-category-management-ui-mt/tests/integration/goods-category-tree-management.integration.test.tsx

@@ -0,0 +1,602 @@
+import React from 'react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor, fireEvent } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { BrowserRouter } from 'react-router';
+import { GoodsCategoryTreeManagement } from '../../src/components/GoodsCategoryTreeManagement';
+import { goodsCategoryClient } from '../../src/api/goodsCategoryClient';
+
+// 完整的mock响应对象 - 按照用户UI包规范
+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 goodsCategoryClient - 按照用户UI包规范
+vi.mock('../../src/api/goodsCategoryClient', () => {
+  const mockGoodsCategoryClient = {
+    index: {
+      $get: vi.fn(() => Promise.resolve({ status: 200, body: null })),
+      $post: vi.fn(() => Promise.resolve({ status: 201, body: null })),
+    },
+    ':id': {
+      $put: vi.fn(() => Promise.resolve({ status: 200, body: null })),
+      $delete: vi.fn(() => Promise.resolve({ status: 204, body: null })),
+    },
+  };
+
+  const mockGoodsCategoryClientManager = {
+    get: vi.fn(() => mockGoodsCategoryClient),
+  };
+
+  return {
+    goodsCategoryClientManager: mockGoodsCategoryClientManager,
+    goodsCategoryClient: mockGoodsCategoryClient,
+  };
+});
+
+// Mock sonner toast
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(),
+    error: vi.fn()
+  }
+}));
+
+// Mock FileSelector
+vi.mock('@d8d/file-management-ui-mt', () => ({
+  FileSelector: ({ value, onChange }: { value?: number; onChange?: (value: number) => void }) => (
+    <div data-testid="file-selector">
+      <button onClick={() => onChange?.(1)}>选择文件</button>
+      <span>当前文件ID: {value}</span>
+    </div>
+  )
+}));
+
+// Test wrapper component
+const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  });
+
+  return (
+    <BrowserRouter>
+      <QueryClientProvider client={queryClient}>
+        {children}
+      </QueryClientProvider>
+    </BrowserRouter>
+  );
+};
+
+describe('商品分类树形管理集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该渲染商品分类树形管理组件并显示标题', async () => {
+    // Mock successful API response for top level data
+    (goodsCategoryClient.index.$get as any).mockResolvedValueOnce(createMockResponse(200, {
+      data: [
+        {
+          id: 1,
+          tenantId: 1,
+          name: '电子产品',
+          parentId: 0,
+          level: 0,
+          state: 1,
+          imageFileId: null
+        }
+      ]
+    }));
+
+    render(
+      <TestWrapper>
+        <GoodsCategoryTreeManagement />
+      </TestWrapper>
+    );
+
+    // Check if title is rendered
+    expect(screen.getByText('商品分类树形管理')).toBeInTheDocument();
+    expect(screen.getByText('异步加载树形结构,高效管理商品分类数据')).toBeInTheDocument();
+
+    // Wait for loading to complete
+    await waitFor(() => {
+      expect(screen.getByText('电子产品')).toBeInTheDocument();
+    });
+  });
+
+  it('应该在获取数据时显示加载状态', async () => {
+    // Mock delayed API response
+    (goodsCategoryClient.index.$get as any).mockImplementationOnce(() =>
+      new Promise(resolve => setTimeout(() => resolve(createMockResponse(200, { data: [] })), 100))
+    );
+
+    render(
+      <TestWrapper>
+        <GoodsCategoryTreeManagement />
+      </TestWrapper>
+    );
+
+    // Check if loading state is shown
+    expect(screen.getByText('加载中...')).toBeInTheDocument();
+
+    // Wait for loading to complete
+    await waitFor(() => {
+      expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
+    });
+  });
+
+  it('应该在无数据时显示空状态', async () => {
+    // Mock empty API response
+    (goodsCategoryClient.index.$get as any).mockResolvedValueOnce(createMockResponse(200, { data: [] }));
+
+    render(
+      <TestWrapper>
+        <GoodsCategoryTreeManagement />
+      </TestWrapper>
+    );
+
+    // Wait for empty state to appear
+    await waitFor(() => {
+      expect(screen.getByText('暂无数据')).toBeInTheDocument();
+    });
+  });
+
+  it('应该在点击新增按钮时打开创建对话框', async () => {
+    // Mock successful API response
+    (goodsCategoryClient.index.$get as any).mockResolvedValueOnce(createMockResponse(200, {
+      data: [
+        {
+          id: 1,
+          tenantId: 1,
+          name: '电子产品',
+          parentId: 0,
+          level: 0,
+          state: 1,
+          imageFileId: null
+        }
+      ]
+    }));
+
+    render(
+      <TestWrapper>
+        <GoodsCategoryTreeManagement />
+      </TestWrapper>
+    );
+
+    // Wait for data to load
+    await waitFor(() => {
+      expect(screen.getByText('电子产品')).toBeInTheDocument();
+    });
+
+    // Click add button
+    const addButton = screen.getByText('新增顶级分类');
+    fireEvent.click(addButton);
+
+    // Check if dialog opens
+    await waitFor(() => {
+      expect(screen.getByRole('heading', { name: '新增顶级分类' })).toBeInTheDocument();
+      expect(screen.getByText('填写顶级分类信息')).toBeInTheDocument();
+    });
+  });
+
+  it('应该优雅地处理API错误', async () => {
+    // Mock API error
+    (goodsCategoryClient.index.$get as any).mockRejectedValueOnce(new Error('API Error'));
+
+    render(
+      <TestWrapper>
+        <GoodsCategoryTreeManagement />
+      </TestWrapper>
+    );
+
+    // Wait for error state
+    await waitFor(() => {
+      // Component should handle errors gracefully
+      expect(screen.getByText('商品分类树形管理')).toBeInTheDocument();
+    });
+  });
+
+  it('应该完成创建和删除工作流程', async () => {
+    const { toast } = await import('sonner');
+
+    // Mock initial categories data
+    const mockCategories = {
+      data: [
+        {
+          id: 1,
+          tenantId: 1,
+          name: '电子产品',
+          parentId: 0,
+          level: 0,
+          state: 1,
+          imageFileId: null
+        }
+      ]
+    };
+
+    // Mock initial data fetch
+    (goodsCategoryClient.index.$get as any).mockResolvedValue(createMockResponse(200, mockCategories));
+
+    render(
+      <TestWrapper>
+        <GoodsCategoryTreeManagement />
+      </TestWrapper>
+    );
+
+    // Wait for initial data to load
+    await waitFor(() => {
+      expect(screen.getByText('电子产品')).toBeInTheDocument();
+    });
+
+    // Test create category
+    const addButton = screen.getByText('新增顶级分类');
+    fireEvent.click(addButton);
+
+    // Wait for create dialog
+    await waitFor(() => {
+      expect(screen.getByRole('heading', { name: '新增顶级分类' })).toBeInTheDocument();
+    });
+
+    // Fill create form
+    const nameInput = screen.getByPlaceholderText('输入分类名称');
+    fireEvent.change(nameInput, { target: { value: '服装' } });
+
+    // Mock successful creation
+    (goodsCategoryClient.index.$post as any).mockResolvedValue(createMockResponse(201, { id: 2, name: '服装' }));
+
+    const submitButton = screen.getByText('创建');
+    fireEvent.click(submitButton);
+
+    await waitFor(() => {
+      expect(goodsCategoryClient.index.$post).toHaveBeenCalledWith({
+        json: {
+          tenantId: 1,
+          name: '服装',
+          parentId: 0,
+          level: 0,
+          state: 1,
+          imageFileId: null
+        }
+      });
+      expect(toast.success).toHaveBeenCalledWith('商品分类创建成功');
+    });
+
+    // Test delete category
+    const deleteButtons = screen.getAllByRole('button', { name: '删除' });
+    fireEvent.click(deleteButtons[0]);
+
+    // Confirm deletion
+    expect(screen.getByRole('heading', { name: '确认删除' })).toBeInTheDocument();
+
+    // Mock successful deletion
+    (goodsCategoryClient[':id']['$delete'] as any).mockResolvedValue({
+      status: 204,
+    });
+
+    // 查找删除确认按钮
+    const confirmDeleteButton = screen.getByRole('button', { name: '确认删除' });
+    fireEvent.click(confirmDeleteButton);
+
+    await waitFor(() => {
+      expect(goodsCategoryClient[':id']['$delete']).toHaveBeenCalledWith({
+        param: { id: 1 },
+      });
+      expect(toast.success).toHaveBeenCalledWith('商品分类删除成功');
+    });
+  });
+
+  it('应该处理CRUD操作中的API错误', async () => {
+    const { goodsCategoryClient } = await import('../../src/api/goodsCategoryClient');
+    const { toast } = await import('sonner');
+
+    // Mock initial data
+    const mockCategories = {
+      data: [
+        {
+          id: 1,
+          tenantId: 1,
+          name: '电子产品',
+          parentId: 0,
+          level: 0,
+          state: 1,
+          imageFileId: null
+        }
+      ]
+    };
+
+    (goodsCategoryClient.index.$get as any).mockResolvedValue(createMockResponse(200, mockCategories));
+
+    render(
+      <TestWrapper>
+        <GoodsCategoryTreeManagement />
+      </TestWrapper>
+    );
+
+    // Wait for data to load
+    await waitFor(() => {
+      expect(screen.getByText('电子产品')).toBeInTheDocument();
+    });
+
+    // Test create category error
+    const addButton = screen.getByText('新增顶级分类');
+    fireEvent.click(addButton);
+
+    await waitFor(() => {
+      expect(screen.getByRole('heading', { name: '新增顶级分类' })).toBeInTheDocument();
+    });
+
+    const nameInput = screen.getByPlaceholderText('输入分类名称');
+    fireEvent.change(nameInput, { target: { value: '服装' } });
+
+    // Mock creation error
+    (goodsCategoryClient.index.$post as any).mockRejectedValue(new Error('Creation failed'));
+
+    const submitButton = screen.getByText('创建');
+    fireEvent.click(submitButton);
+
+    await waitFor(() => {
+      expect(toast.error).toHaveBeenCalledWith('创建失败,请重试');
+    });
+  });
+
+  it('应该支持多级分类层级(顶级→一级→二级→三级)', async () => {
+    // Mock initial data with all levels
+    const mockCategories = {
+      data: [
+        {
+          id: 1,
+          tenantId: 1,
+          name: '电子产品',
+          parentId: 0,
+          level: 0,
+          state: 1,
+          imageFileId: null
+        },
+        {
+          id: 2,
+          tenantId: 1,
+          name: '手机',
+          parentId: 1,
+          level: 1,
+          state: 1,
+          imageFileId: null
+        },
+        {
+          id: 3,
+          tenantId: 1,
+          name: '智能手机',
+          parentId: 2,
+          level: 2,
+          state: 1,
+          imageFileId: null
+        },
+        {
+          id: 4,
+          tenantId: 1,
+          name: '苹果手机',
+          parentId: 3,
+          level: 3,
+          state: 1,
+          imageFileId: null
+        }
+      ]
+    };
+
+    (goodsCategoryClient.index.$get as any).mockResolvedValue(createMockResponse(200, mockCategories));
+
+    render(
+      <TestWrapper>
+        <GoodsCategoryTreeManagement />
+      </TestWrapper>
+    );
+
+    // Wait for all level data to load
+    await waitFor(() => {
+      expect(screen.getByText('电子产品')).toBeInTheDocument();
+      expect(screen.getByText('手机')).toBeInTheDocument();
+      expect(screen.getByText('智能手机')).toBeInTheDocument();
+      expect(screen.getByText('苹果手机')).toBeInTheDocument();
+    });
+
+    // Verify all levels are displayed correctly
+    expect(screen.getByText('电子产品')).toBeInTheDocument();
+    expect(screen.getByText('手机')).toBeInTheDocument();
+    expect(screen.getByText('智能手机')).toBeInTheDocument();
+    expect(screen.getByText('苹果手机')).toBeInTheDocument();
+  });
+
+  it('应该成功创建多级分类(三级分类)', async () => {
+    const { toast } = await import('sonner');
+
+    // Mock initial data with level 2 category
+    const mockCategories = {
+      data: [
+        {
+          id: 3,
+          tenantId: 1,
+          name: '智能手机',
+          parentId: 2,
+          level: 2,
+          state: 1,
+          imageFileId: null
+        }
+      ]
+    };
+
+    (goodsCategoryClient.index.$get as any).mockResolvedValue(createMockResponse(200, mockCategories));
+
+    render(
+      <TestWrapper>
+        <GoodsCategoryTreeManagement />
+      </TestWrapper>
+    );
+
+    // Wait for data to load
+    await waitFor(() => {
+      expect(screen.getByText('智能手机')).toBeInTheDocument();
+    });
+
+    // Click add child button for level 2 category
+    const addChildButtons = screen.getAllByRole('button', { name: '新增子分类' });
+    fireEvent.click(addChildButtons[0]);
+
+    // Check if child creation dialog opens
+    await waitFor(() => {
+      expect(screen.getByRole('heading', { name: '新增子分类' })).toBeInTheDocument();
+      expect(screen.getByText('在分类 "智能手机" 下新增子分类')).toBeInTheDocument();
+    });
+
+    // Fill child form
+    const nameInput = screen.getByPlaceholderText('输入分类名称');
+    fireEvent.change(nameInput, { target: { value: '苹果手机' } });
+
+    // Mock successful child creation
+    (goodsCategoryClient.index.$post as any).mockResolvedValue(createMockResponse(201, { id: 4, name: '苹果手机' }));
+
+    const submitButton = screen.getByText('创建');
+    fireEvent.click(submitButton);
+
+    await waitFor(() => {
+      expect(goodsCategoryClient.index.$post).toHaveBeenCalledWith({
+        json: {
+          tenantId: 1,
+          name: '苹果手机',
+          parentId: 3,
+          level: 3,
+          state: 1,
+          imageFileId: null
+        }
+      });
+      expect(toast.success).toHaveBeenCalledWith('商品分类创建成功');
+    });
+  });
+
+  it('应该处理分类状态切换', async () => {
+    const { toast } = await import('sonner');
+
+    // Mock initial data with category
+    const mockCategories = {
+      data: [
+        {
+          id: 4,
+          tenantId: 1,
+          name: '苹果手机',
+          parentId: 3,
+          level: 3,
+          state: 1,
+          imageFileId: null
+        }
+      ]
+    };
+
+    (goodsCategoryClient.index.$get as any).mockResolvedValue(createMockResponse(200, mockCategories));
+
+    render(
+      <TestWrapper>
+        <GoodsCategoryTreeManagement />
+      </TestWrapper>
+    );
+
+    // Wait for data to load
+    await waitFor(() => {
+      expect(screen.getByText('苹果手机')).toBeInTheDocument();
+    });
+
+    // Click toggle status button for category
+    const toggleButtons = screen.getAllByRole('button', { name: '禁用' });
+    fireEvent.click(toggleButtons[0]);
+
+    // Check if status toggle dialog opens
+    await waitFor(() => {
+      expect(screen.getByRole('heading', { name: '禁用确认' })).toBeInTheDocument();
+      expect(screen.getByText('确定要禁用商品分类 "苹果手机" 吗?')).toBeInTheDocument();
+    });
+
+    // Mock successful status toggle
+    (goodsCategoryClient[':id'].$put as any).mockResolvedValue(createMockResponse(200));
+
+    const confirmButton = screen.getByRole('button', { name: '确认' });
+    fireEvent.click(confirmButton);
+
+    await waitFor(() => {
+      expect(goodsCategoryClient[':id'].$put).toHaveBeenCalledWith({
+        param: { id: 4 },
+        json: { state: 2 }
+      });
+      expect(toast.success).toHaveBeenCalledWith('商品分类禁用成功');
+    });
+  });
+
+  it('应该成功删除多级分类(三级分类)', async () => {
+    const { toast } = await import('sonner');
+
+    // Mock initial data with level 3 category
+    const mockCategories = {
+      data: [
+        {
+          id: 4,
+          tenantId: 1,
+          name: '苹果手机',
+          parentId: 3,
+          level: 3,
+          state: 1,
+          imageFileId: null
+        }
+      ]
+    };
+
+    (goodsCategoryClient.index.$get as any).mockResolvedValue(createMockResponse(200, mockCategories));
+
+    render(
+      <TestWrapper>
+        <GoodsCategoryTreeManagement />
+      </TestWrapper>
+    );
+
+    // Wait for data to load
+    await waitFor(() => {
+      expect(screen.getByText('苹果手机')).toBeInTheDocument();
+    });
+
+    // Click delete button for category
+    const deleteButtons = screen.getAllByRole('button', { name: '删除' });
+    fireEvent.click(deleteButtons[0]);
+
+    // Check if delete confirmation dialog opens
+    await waitFor(() => {
+      expect(screen.getByRole('heading', { name: '确认删除' })).toBeInTheDocument();
+      expect(screen.getByText('确定要删除商品分类 "苹果手机" 吗?此操作不可恢复。')).toBeInTheDocument();
+    });
+
+    // Mock successful deletion
+    (goodsCategoryClient[':id'].$delete as any).mockResolvedValue(createMockResponse(204));
+
+    const confirmDeleteButton = screen.getByRole('button', { name: '确认删除' });
+    fireEvent.click(confirmDeleteButton);
+
+    await waitFor(() => {
+      expect(goodsCategoryClient[':id'].$delete).toHaveBeenCalledWith({
+        param: { id: 4 }
+      });
+      expect(toast.success).toHaveBeenCalledWith('商品分类删除成功');
+    });
+  });
+});

+ 3 - 0
pnpm-lock.yaml

@@ -2109,6 +2109,9 @@ importers:
       react-hook-form:
         specifier: ^7.61.1
         version: 7.65.0(react@19.2.0)
+      react-router:
+        specifier: ^7.1.3
+        version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
       sonner:
         specifier: ^2.0.7
         version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)