- 文件上传验证: ModelUploader组件增强、上传API错误处理 - GLTF解析器: 增强节点提取和元数据 - 板材编辑: PanelEditor客户端验证 + panels API服务端验证 - 装配步骤编辑: 修复未使用代码,添加预览按钮 - 装配动画播放: AssemblyPlayer步骤导航边界检查优化 - 分享链接: 完整CRUD API(创建、获取、删除),含验证和过期检查 跳过可选测试任务以加快MVP开发
332 lines
10 KiB
TypeScript
332 lines
10 KiB
TypeScript
/**
|
||
* 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();
|
||
});
|
||
});
|
||
});
|