DocHub/frontend/tests/components/UploadButton.test.tsx
Tianyang 7e218b5738 fix: repair build, preserve unsaved edits on save failure, fix search/upload UX gaps
- client.test.ts: use globalThis.fetch instead of global.fetch and drop the
  unused describe import, fixing the broken `npm run build`
- DocumentEditor: separate load-error (replaces view) from save-error (shown
  inline, editor and unsaved content stay mounted so a failed save no longer
  destroys in-progress edits); expose dirty state via onDirtyChange
- App: prompt via window.confirm before switching documents while dirty;
  increment a refreshKey on successful upload so FileExplorer re-fetches the
  tree; track isSearching so the "no results" overlay doesn't flash during
  the search debounce window
- FileExplorer: accept a refreshKey prop to force a root-listing re-fetch
- SearchBar: convert to a controlled component (value prop) so App can clear
  the input after jumping to a search result, fixing input/state desync
- UploadButton: surface upload failures inline instead of swallowing the
  rejected promise, and keep the modal open for retry
2026-08-01 08:59:49 +08:00

81 lines
3.2 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('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 });
});
});