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(); }); }); }); });