Adds a folder picker (webkitdirectory) and drag-and-drop directory support (File System Entries API), preserving relative paths via a new optional relative_path field on the upload endpoint.
143 lines
5.4 KiB
TypeScript
143 lines
5.4 KiB
TypeScript
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { vi, test, expect, beforeEach } from 'vitest';
|
|
import UploadButton from '../../src/components/UploadButton';
|
|
import { uploadDocument } from '../../src/api/client';
|
|
|
|
vi.mock('../../src/api/client');
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(uploadDocument).mockReset();
|
|
});
|
|
|
|
test('opens the upload modal when the button is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
render(<UploadButton path="产品文档" onUploaded={vi.fn()} />);
|
|
|
|
expect(screen.queryByTestId('upload-modal')).not.toBeInTheDocument();
|
|
|
|
await user.click(screen.getByRole('button', { name: '上传文档' }));
|
|
|
|
expect(screen.getByTestId('upload-modal')).toBeInTheDocument();
|
|
});
|
|
|
|
test('selecting a file through the hidden input uploads it', async () => {
|
|
vi.mocked(uploadDocument).mockResolvedValue({ key: '产品文档/新文档.md', name: '新文档.md', size: 42 });
|
|
const onUploaded = vi.fn();
|
|
const user = userEvent.setup();
|
|
|
|
render(<UploadButton path="产品文档" onUploaded={onUploaded} />);
|
|
await user.click(screen.getByRole('button', { name: '上传文档' }));
|
|
|
|
const file = new File(['# 新文档'], '新文档.md', { type: 'text/markdown' });
|
|
const input = screen.getByTestId('upload-file-input');
|
|
await user.upload(input, file);
|
|
|
|
expect(uploadDocument).toHaveBeenCalledWith(file, '产品文档');
|
|
await waitFor(() => {
|
|
expect(onUploaded).toHaveBeenCalledWith({ key: '产品文档/新文档.md', name: '新文档.md', size: 42 });
|
|
});
|
|
});
|
|
|
|
test('a failed upload shows an error message and keeps the modal open for retry', async () => {
|
|
vi.mocked(uploadDocument).mockRejectedValue(new Error('Failed to upload document: 400'));
|
|
const onUploaded = vi.fn();
|
|
const user = userEvent.setup();
|
|
|
|
render(<UploadButton path="产品文档" onUploaded={onUploaded} />);
|
|
await user.click(screen.getByRole('button', { name: '上传文档' }));
|
|
|
|
const file = new File(['data'], '恶意程序.exe', { type: 'application/octet-stream' });
|
|
const input = screen.getByTestId('upload-file-input');
|
|
await user.upload(input, file);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('upload-error')).toHaveTextContent('Failed to upload document: 400');
|
|
});
|
|
expect(screen.getByTestId('upload-modal')).toBeInTheDocument();
|
|
expect(onUploaded).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('selecting a folder through the folder input uploads each file with its relative path', async () => {
|
|
vi.mocked(uploadDocument).mockResolvedValue({ key: '产品文档/子目录/新文档.md', name: '新文档.md', size: 8 });
|
|
const onUploaded = vi.fn();
|
|
const user = userEvent.setup();
|
|
|
|
render(<UploadButton path="产品文档" onUploaded={onUploaded} />);
|
|
await user.click(screen.getByRole('button', { name: '上传文档' }));
|
|
|
|
const file = new File(['content'], '新文档.md', { type: 'text/markdown' });
|
|
Object.defineProperty(file, 'webkitRelativePath', { value: '子目录/新文档.md', configurable: true });
|
|
const input = screen.getByTestId('upload-folder-input');
|
|
await user.upload(input, file);
|
|
|
|
expect(uploadDocument).toHaveBeenCalledWith(file, '产品文档', '子目录/新文档.md');
|
|
await waitFor(() => {
|
|
expect(onUploaded).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
test('dropping a file onto the dropzone uploads it', async () => {
|
|
vi.mocked(uploadDocument).mockResolvedValue({ key: '产品文档/拖拽文档.md', name: '拖拽文档.md', size: 10 });
|
|
const onUploaded = vi.fn();
|
|
const user = userEvent.setup();
|
|
|
|
render(<UploadButton path="产品文档" onUploaded={onUploaded} />);
|
|
await user.click(screen.getByRole('button', { name: '上传文档' }));
|
|
|
|
const file = new File(['内容'], '拖拽文档.md', { type: 'text/markdown' });
|
|
const dropzone = screen.getByTestId('dropzone');
|
|
|
|
fireEvent.drop(dropzone, {
|
|
dataTransfer: { files: [file] },
|
|
});
|
|
|
|
expect(uploadDocument).toHaveBeenCalledWith(file, '产品文档');
|
|
await waitFor(() => {
|
|
expect(onUploaded).toHaveBeenCalledWith({ key: '产品文档/拖拽文档.md', name: '拖拽文档.md', size: 10 });
|
|
});
|
|
});
|
|
|
|
test('dropping a folder onto the dropzone walks its entries and uploads each file with its relative path', async () => {
|
|
vi.mocked(uploadDocument).mockResolvedValue({ key: '产品文档/子目录/文件.md', name: '文件.md', size: 5 });
|
|
const onUploaded = vi.fn();
|
|
const user = userEvent.setup();
|
|
const file = new File(['# hi'], '文件.md', { type: 'text/markdown' });
|
|
|
|
const fileEntry = {
|
|
isFile: true,
|
|
isDirectory: false,
|
|
fullPath: '/子目录/文件.md',
|
|
file: (success: (f: File) => void) => success(file),
|
|
};
|
|
let readCount = 0;
|
|
const dirReader = {
|
|
readEntries: (success: (entries: unknown[]) => void) => {
|
|
readCount += 1;
|
|
success(readCount === 1 ? [fileEntry] : []);
|
|
},
|
|
};
|
|
const dirEntry = {
|
|
isFile: false,
|
|
isDirectory: true,
|
|
fullPath: '/子目录',
|
|
createReader: () => dirReader,
|
|
};
|
|
const item = { webkitGetAsEntry: () => dirEntry };
|
|
|
|
render(<UploadButton path="产品文档" onUploaded={onUploaded} />);
|
|
await user.click(screen.getByRole('button', { name: '上传文档' }));
|
|
const dropzone = screen.getByTestId('dropzone');
|
|
|
|
fireEvent.drop(dropzone, {
|
|
dataTransfer: { items: [item], files: [] },
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(uploadDocument).toHaveBeenCalledWith(file, '产品文档', '子目录/文件.md');
|
|
});
|
|
await waitFor(() => {
|
|
expect(onUploaded).toHaveBeenCalled();
|
|
});
|
|
});
|