WuYongJie 75db836d29 feat(3D核心功能): 完成所有必需实现任务
- 文件上传验证: ModelUploader组件增强、上传API错误处理
- GLTF解析器: 增强节点提取和元数据
- 板材编辑: PanelEditor客户端验证 + panels API服务端验证
- 装配步骤编辑: 修复未使用代码,添加预览按钮
- 装配动画播放: AssemblyPlayer步骤导航边界检查优化
- 分享链接: 完整CRUD API(创建、获取、删除),含验证和过期检查

跳过可选测试任务以加快MVP开发
2025-12-22 16:18:13 +08:00

332 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* HomeClient 组件测试
*
* 测试主页客户端组件的功能和性能优化
*
* @created 2025-12-13
*/
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import HomeClient from '@/app/HomeClient';
// Mock 子组件
jest.mock('@/components/ui/Card', () => ({
__esModule: true,
default: ({ children, onClick, ...props }: any) => (
<div data-testid="card" onClick={onClick} {...props}>
{children}
</div>
),
}));
jest.mock('@/components/ui/ProductListItem', () => ({
__esModule: true,
default: ({ product }: any) => (
<div data-testid={`product-${product.id}`}>
{product.name}
</div>
),
}));
jest.mock('@/components/products/UploadProductModal', () => ({
__esModule: true,
default: ({ isOpen, onClose, onSuccess }: any) => (
isOpen ? (
<div data-testid="upload-modal">
<button onClick={onClose}>关闭</button>
<button onClick={onSuccess}>上传成功</button>
</div>
) : null
),
}));
// Mock useDebounce hook
jest.mock('@/hooks/usePerformance', () => ({
useDebounce: (value: any, delay: number) => value, // 测试时不延迟
}));
describe('HomeClient 组件测试', () => {
const mockProducts = [
{
id: '1',
name: '测试产品1',
modelNumber: 'TEST-001',
sku: 'SKU-001',
description: '这是测试产品1的描述',
category: '家具',
createdAt: new Date('2025-01-01'),
_count: {
assemblySteps: 5,
shareLinks: 1,
skus: 2,
},
shareLinks: [{ token: 'token1', viewCount: 10 }],
skus: [
{ id: 'sku1', skuCode: 'SKU-001', price: 100, stock: 50, isActive: true },
],
},
{
id: '2',
name: '测试产品2',
modelNumber: 'TEST-002',
sku: 'SKU-002',
description: '这是测试产品2的描述',
category: '家具',
createdAt: new Date('2025-01-02'),
_count: {
assemblySteps: 3,
shareLinks: 0,
skus: 1,
},
shareLinks: [],
skus: [
{ id: 'sku2', skuCode: 'SKU-002', price: 150, stock: 30, isActive: true },
],
},
];
beforeEach(() => {
jest.clearAllMocks();
});
describe('基本渲染', () => {
it('应该正确渲染所有 Tab 卡片', () => {
render(<HomeClient products={mockProducts} />);
expect(screen.getByText('3D模型管理')).toBeInTheDocument();
expect(screen.getByText('标准五金件')).toBeInTheDocument();
expect(screen.getByText('BOM价格')).toBeInTheDocument();
expect(screen.getByText('装配文档')).toBeInTheDocument();
});
it('应该默认显示模型管理 Tab', () => {
render(<HomeClient products={mockProducts} />);
expect(screen.getByText('产品列表')).toBeInTheDocument();
expect(screen.getByPlaceholderText('搜索产品型号、名称或描述...')).toBeInTheDocument();
});
it('应该渲染产品列表', () => {
render(<HomeClient products={mockProducts} />);
expect(screen.getByTestId('product-1')).toBeInTheDocument();
expect(screen.getByTestId('product-2')).toBeInTheDocument();
});
});
describe('Tab 切换', () => {
it('应该能够切换到标准五金件 Tab', () => {
render(<HomeClient products={mockProducts} />);
const hardwareTab = screen.getByText('标准五金件').closest('[data-testid="card"]');
expect(hardwareTab).toBeInTheDocument();
if (hardwareTab) {
fireEvent.click(hardwareTab);
expect(screen.getByText('标准五金件管理')).toBeInTheDocument();
expect(screen.getByText('功能开发中...')).toBeInTheDocument();
}
});
it('应该能够切换到 BOM 价格 Tab', () => {
render(<HomeClient products={mockProducts} />);
const bomTab = screen.getByText('BOM价格').closest('[data-testid="card"]');
expect(bomTab).toBeInTheDocument();
if (bomTab) {
fireEvent.click(bomTab);
expect(screen.getByText('BOM价格管理')).toBeInTheDocument();
}
});
it('应该能够切换到装配文档 Tab', () => {
render(<HomeClient products={mockProducts} />);
const docsTab = screen.getByText('装配文档').closest('[data-testid="card"]');
expect(docsTab).toBeInTheDocument();
if (docsTab) {
fireEvent.click(docsTab);
expect(screen.getByText('装配文档生成')).toBeInTheDocument();
}
});
it('应该能够切换回模型管理 Tab', () => {
render(<HomeClient products={mockProducts} />);
// 先切换到其他 Tab
const bomTab = screen.getByText('BOM价格').closest('[data-testid="card"]');
if (bomTab) {
fireEvent.click(bomTab);
}
// 再切换回模型管理
const modelsTab = screen.getByText('3D模型管理').closest('[data-testid="card"]');
if (modelsTab) {
fireEvent.click(modelsTab);
expect(screen.getByText('产品列表')).toBeInTheDocument();
}
});
});
describe('搜索功能', () => {
it('应该能够通过产品名称搜索', () => {
render(<HomeClient products={mockProducts} />);
const searchInput = screen.getByPlaceholderText('搜索产品型号、名称或描述...');
fireEvent.change(searchInput, { target: { value: '测试产品1' } });
// 由于 mock 了 useDebounce,搜索会立即生效
expect(screen.getByTestId('product-1')).toBeInTheDocument();
expect(screen.queryByTestId('product-2')).not.toBeInTheDocument();
});
it('应该能够通过 SKU 搜索', () => {
render(<HomeClient products={mockProducts} />);
const searchInput = screen.getByPlaceholderText('搜索产品型号、名称或描述...');
fireEvent.change(searchInput, { target: { value: 'SKU-002' } });
expect(screen.queryByTestId('product-1')).not.toBeInTheDocument();
expect(screen.getByTestId('product-2')).toBeInTheDocument();
});
it('应该能够通过描述搜索', () => {
render(<HomeClient products={mockProducts} />);
const searchInput = screen.getByPlaceholderText('搜索产品型号、名称或描述...');
fireEvent.change(searchInput, { target: { value: '描述' } });
// 两个产品的描述都包含"描述"
expect(screen.getByTestId('product-1')).toBeInTheDocument();
expect(screen.getByTestId('product-2')).toBeInTheDocument();
});
it('应该在没有匹配结果时显示提示', () => {
render(<HomeClient products={mockProducts} />);
const searchInput = screen.getByPlaceholderText('搜索产品型号、名称或描述...');
fireEvent.change(searchInput, { target: { value: '不存在的产品' } });
expect(screen.getByText('未找到匹配的产品')).toBeInTheDocument();
expect(screen.getByText('尝试使用其他关键词搜索')).toBeInTheDocument();
});
it('搜索应该不区分大小写', () => {
render(<HomeClient products={mockProducts} />);
const searchInput = screen.getByPlaceholderText('搜索产品型号、名称或描述...');
fireEvent.change(searchInput, { target: { value: '测试产品1' } });
expect(screen.getByTestId('product-1')).toBeInTheDocument();
expect(screen.queryByTestId('product-2')).not.toBeInTheDocument();
});
});
describe('上传产品功能', () => {
it('应该能够打开上传产品弹窗', () => {
render(<HomeClient products={mockProducts} />);
const uploadButton = screen.getByText('上传产品');
fireEvent.click(uploadButton);
expect(screen.getByTestId('upload-modal')).toBeInTheDocument();
});
it('应该能够关闭上传产品弹窗', () => {
render(<HomeClient products={mockProducts} />);
// 打开弹窗
const uploadButton = screen.getByText('上传产品');
fireEvent.click(uploadButton);
// 关闭弹窗
const closeButton = screen.getByText('关闭');
fireEvent.click(closeButton);
expect(screen.queryByTestId('upload-modal')).not.toBeInTheDocument();
});
it('应该在上传成功后刷新产品列表', async () => {
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({
products: [
...mockProducts,
{
id: '3',
name: '新产品',
modelNumber: 'NEW-001',
sku: 'SKU-003',
description: '新上传的产品',
category: '家具',
createdAt: new Date('2025-01-03'),
_count: { assemblySteps: 0, shareLinks: 0, skus: 1 },
shareLinks: [],
skus: [
{ id: 'sku3', skuCode: 'SKU-003', price: 200, stock: 20, isActive: true },
],
},
],
}),
})
) as jest.Mock;
render(<HomeClient products={mockProducts} />);
// 打开弹窗
const uploadButton = screen.getByText('上传产品');
fireEvent.click(uploadButton);
// 触发上传成功
const successButton = screen.getByText('上传成功');
fireEvent.click(successButton);
// 等待 API 调用完成
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith('/api/products');
});
});
});
describe('空状态', () => {
it('应该在没有产品时显示空状态', () => {
render(<HomeClient products={[]} />);
expect(screen.getByText('暂无产品')).toBeInTheDocument();
expect(screen.getByText('还没有添加任何产品,开始创建您的第一个3D产品吧!')).toBeInTheDocument();
});
});
describe('性能优化', () => {
it('应该使用 useMemo 缓存 tabs 配置', () => {
const { rerender } = render(<HomeClient products={mockProducts} />);
// 重新渲染
rerender(<HomeClient products={mockProducts} />);
// tabs 应该被缓存,不会重新创建
expect(screen.getByText('3D模型管理')).toBeInTheDocument();
});
it('应该使用 useCallback 优化回调函数', () => {
render(<HomeClient products={mockProducts} />);
const uploadButton = screen.getByText('上传产品');
// 多次点击应该使用相同的回调函数
fireEvent.click(uploadButton);
const closeButton = screen.getByText('关闭');
fireEvent.click(closeButton);
fireEvent.click(uploadButton);
expect(screen.getByTestId('upload-modal')).toBeInTheDocument();
});
});
});