import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import FileSelector from '../../src/components/FileSelector';
// 完整的mock响应对象
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 API客户端
vi.mock('../../src/api/fileClient', () => {
const mockFileClient = {
index: {
$get: vi.fn(() => Promise.resolve(createMockResponse(200, {
data: [],
pagination: { current: 1, pageSize: 50, total: 0 }
}))),
},
':id': {
$get: vi.fn(() => Promise.resolve(createMockResponse(200, {}))),
},
};
const mockFileClientManager = {
get: vi.fn(() => mockFileClient),
};
return {
fileClientManager: mockFileClientManager,
fileClient: mockFileClient,
};
});
// Mock 文件上传组件
vi.mock('../../src/components/MinioUploader', () => ({
default: ({ onUploadSuccess, testId }: { onUploadSuccess?: (key: string, url: string, file: File) => void; testId?: string }) => {
// 提供一个测试辅助方法来触发上传成功
const triggerUpload = () => {
if (onUploadSuccess) {
const testFile = new File(['test'], 'test-upload.jpg', { type: 'image/jpeg' });
onUploadSuccess('test-key', 'http://example.com/test.jpg', testFile);
}
};
// 将触发方法挂载到 DOM 元素上供测试调用
React.useEffect(() => {
if (testId) {
const element = document.querySelector(`[data-testid="${testId}"]`);
if (element) {
(element as any).__triggerUpload = triggerUpload;
}
}
}, [testId, onUploadSuccess]);
return React.createElement('div', { 'data-testid': testId || 'minio-uploader' });
},
}));
describe('FileSelector', () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
vi.clearAllMocks();
});
const renderWithQueryClient = (component: React.ReactElement) => {
return render(
{component}
);
};
const mockFiles = [
{
id: 1,
name: 'test-image.jpg',
type: 'image/jpeg',
size: 1024,
fullUrl: 'http://example.com/test-image.jpg',
uploadTime: '2024-01-01T00:00:00Z',
},
{
id: 2,
name: 'test-document.pdf',
type: 'application/pdf',
size: 2048,
fullUrl: 'http://example.com/test-document.pdf',
uploadTime: '2024-01-01T00:00:00Z',
},
];
it('应该渲染文件选择器', () => {
renderWithQueryClient(
{}} />
);
expect(screen.getByTestId('file-selector-button')).toBeInTheDocument();
});
it('应该打开选择对话框', async () => {
const { fileClient } = await import('../../src/api/fileClient');
(fileClient.index.$get as any).mockResolvedValue(createMockResponse(200, {
data: mockFiles,
pagination: { current: 1, pageSize: 50, total: 2 }
}));
renderWithQueryClient(
{}} />
);
const selectButton = screen.getByTestId('file-selector-button');
fireEvent.click(selectButton);
await waitFor(() => {
expect(screen.getByTestId('file-selector-dialog')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '选择文件' })).toBeInTheDocument();
expect(screen.getByText('上传新文件或从已有文件中选择')).toBeInTheDocument();
});
});
it('应该显示已选文件预览', async () => {
const { fileClient } = await import('../../src/api/fileClient');
(fileClient[':id'].$get as any).mockResolvedValue(createMockResponse(200, mockFiles[0]));
renderWithQueryClient(
{}} showPreview={true} />
);
await waitFor(() => {
expect(screen.getByText('更换文件')).toBeInTheDocument();
});
});
it('应该支持多选模式', async () => {
const { fileClient } = await import('../../src/api/fileClient');
(fileClient.index.$get as any).mockResolvedValue(createMockResponse(200, {
data: mockFiles,
pagination: { current: 1, pageSize: 50, total: 2 }
}));
const onChange = vi.fn();
renderWithQueryClient(
);
await waitFor(() => {
expect(screen.getByText('已选择 2 个文件')).toBeInTheDocument();
});
});
it('应该过滤文件类型', async () => {
const { fileClient } = await import('../../src/api/fileClient');
(fileClient.index.$get as any).mockResolvedValue(createMockResponse(200, {
data: mockFiles,
pagination: { current: 1, pageSize: 50, total: 2 }
}));
renderWithQueryClient(
{}}
filterType="image"
/>
);
const selectButton = screen.getByTestId('file-selector-button');
fireEvent.click(selectButton);
await waitFor(() => {
expect(fileClient.index.$get).toHaveBeenCalledWith({
query: {
page: 1,
pageSize: 50,
keyword: 'image'
}
});
});
});
it('应该处理文件选择确认', async () => {
const { fileClient } = await import('../../src/api/fileClient');
(fileClient.index.$get as any).mockResolvedValue(createMockResponse(200, {
data: mockFiles,
pagination: { current: 1, pageSize: 50, total: 2 }
}));
const onChange = vi.fn();
renderWithQueryClient(
);
const selectButton = screen.getByTestId('file-selector-button');
fireEvent.click(selectButton);
await waitFor(() => {
const fileItems = screen.getAllByText('test-image.jpg');
fireEvent.click(fileItems[0]);
});
const confirmButton = screen.getByText('确认选择');
fireEvent.click(confirmButton);
expect(onChange).toHaveBeenCalledWith(1);
});
describe('uploadOnly 模式', () => {
it('uploadOnly=true 时不应该调用文件列表查询 API', async () => {
const { fileClient } = await import('../../src/api/fileClient');
(fileClient.index.$get as any).mockResolvedValue(createMockResponse(200, {
data: mockFiles,
pagination: { current: 1, pageSize: 50, total: 2 }
}));
renderWithQueryClient(
{}}
uploadOnly={true}
/>
);
const selectButton = screen.getByTestId('file-selector-button');
fireEvent.click(selectButton);
await waitFor(() => {
expect(screen.getByTestId('file-selector-dialog')).toBeInTheDocument();
// 验证描述文本显示为上传模式
expect(screen.getByText('请上传文件')).toBeInTheDocument();
});
// 验证没有调用文件列表 API(因为 uploadOnly=true 禁用了查询)
expect(fileClient.index.$get).not.toHaveBeenCalled();
});
it('uploadOnly 模式下对话框应该只显示上传区域', async () => {
renderWithQueryClient(
{}}
uploadOnly={true}
/>
);
const selectButton = screen.getByTestId('file-selector-button');
fireEvent.click(selectButton);
await waitFor(() => {
expect(screen.getByTestId('file-selector-dialog')).toBeInTheDocument();
// 验证上传组件存在
expect(screen.getByTestId('minio-uploader')).toBeInTheDocument();
});
// 验证不显示确认/取消按钮
expect(screen.queryByText('取消')).not.toBeInTheDocument();
expect(screen.queryByText('确认选择')).not.toBeInTheDocument();
});
it('uploadOnly 模式与 allowMultiple 多选模式兼容', async () => {
const { fileClient } = await import('../../src/api/fileClient');
(fileClient.index.$get as any).mockResolvedValue(createMockResponse(200, {
data: mockFiles,
pagination: { current: 1, pageSize: 50, total: 2 }
}));
const onChange = vi.fn();
renderWithQueryClient(
);
const selectButton = screen.getByTestId('file-selector-button');
fireEvent.click(selectButton);
await waitFor(() => {
expect(screen.getByTestId('file-selector-dialog')).toBeInTheDocument();
expect(screen.getByTestId('minio-uploader')).toBeInTheDocument();
});
// 验证没有调用文件列表 API
expect(fileClient.index.$get).not.toHaveBeenCalled();
});
it('uploadOnly=false 或未设置时,行为与原组件一致', async () => {
const { fileClient } = await import('../../src/api/fileClient');
(fileClient.index.$get as any).mockResolvedValue(createMockResponse(200, {
data: mockFiles,
pagination: { current: 1, pageSize: 50, total: 2 }
}));
renderWithQueryClient(
{}}
uploadOnly={false}
/>
);
const selectButton = screen.getByTestId('file-selector-button');
fireEvent.click(selectButton);
await waitFor(() => {
expect(screen.getByTestId('file-selector-dialog')).toBeInTheDocument();
// 验证显示默认描述文本
expect(screen.getByText('上传新文件或从已有文件中选择')).toBeInTheDocument();
});
// 验证调用了文件列表 API
expect(fileClient.index.$get).toHaveBeenCalledWith({
query: {
page: 1,
pageSize: 50,
}
});
});
it('uploadOnly 模式下不显示文件列表', async () => {
renderWithQueryClient(
{}}
uploadOnly={true}
/>
);
const selectButton = screen.getByTestId('file-selector-button');
fireEvent.click(selectButton);
await waitFor(() => {
expect(screen.getByTestId('file-selector-dialog')).toBeInTheDocument();
});
// 验证不显示现有文件
expect(screen.queryByText('test-image.jpg')).not.toBeInTheDocument();
expect(screen.queryByText('test-document.pdf')).not.toBeInTheDocument();
});
it('uploadOnly 模式下上传成功后自动选择文件并关闭对话框', async () => {
const { fileClient } = await import('../../src/api/fileClient');
// Mock 文件列表 API,返回刚上传的文件
const uploadedFile = {
id: 999,
name: 'test-upload.jpg',
type: 'image/jpeg',
size: 4, // File(['test']) 的 size 是 4
fullUrl: 'http://example.com/test-upload.jpg',
uploadTime: new Date().toISOString(),
};
(fileClient.index.$get as any).mockResolvedValue(createMockResponse(200, {
data: [uploadedFile],
pagination: { current: 1, pageSize: 50, total: 1 }
}));
const onChange = vi.fn();
renderWithQueryClient(
);
// 打开对话框
const selectButton = screen.getByTestId('file-selector-button');
fireEvent.click(selectButton);
await waitFor(() => {
expect(screen.getByTestId('file-selector-dialog')).toBeInTheDocument();
});
// 触发上传成功(通过挂载的测试辅助方法)
const uploaderElement = document.querySelector('[data-testid="photo-upload-0"]');
expect(uploaderElement).toBeInTheDocument();
(uploaderElement as any).__triggerUpload();
// 等待 API 被调用
await waitFor(() => {
expect(fileClient.index.$get).toHaveBeenCalled();
});
// 验证 onChange 被调用,返回正确的 fileId
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(999);
});
// 验证对话框已关闭
await waitFor(() => {
expect(screen.queryByTestId('file-selector-dialog')).not.toBeInTheDocument();
});
});
});
});