| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- 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(
- <QueryClientProvider client={queryClient}>
- {component}
- </QueryClientProvider>
- );
- };
- 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(
- <FileSelector value={null} onChange={() => {}} />
- );
- 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(
- <FileSelector value={null} onChange={() => {}} />
- );
- 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(
- <FileSelector value={1} onChange={() => {}} 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(
- <FileSelector
- value={[1, 2]}
- onChange={onChange}
- allowMultiple={true}
- showPreview={true}
- />
- );
- 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(
- <FileSelector
- value={null}
- onChange={() => {}}
- 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(
- <FileSelector value={null} onChange={onChange} />
- );
- 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(
- <FileSelector
- value={null}
- onChange={() => {}}
- 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(
- <FileSelector
- value={null}
- onChange={() => {}}
- 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(
- <FileSelector
- value={null}
- onChange={onChange}
- uploadOnly={true}
- allowMultiple={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();
- });
- // 验证没有调用文件列表 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(
- <FileSelector
- value={null}
- onChange={() => {}}
- 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(
- <FileSelector
- value={null}
- onChange={() => {}}
- 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(
- <FileSelector
- value={null}
- onChange={onChange}
- uploadOnly={true}
- testId="photo-upload-0"
- />
- );
- // 打开对话框
- 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();
- });
- });
- });
- });
|