DocHub/frontend/tests/components/FileExplorer.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

86 lines
3.0 KiB
TypeScript

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi, test, expect, beforeEach } from 'vitest';
import FileExplorer from '../../src/components/FileExplorer';
import { listDocuments } from '../../src/api/client';
vi.mock('../../src/api/client');
beforeEach(() => {
vi.mocked(listDocuments).mockReset();
});
test('renders folders and files returned by listDocuments for the root path', async () => {
vi.mocked(listDocuments).mockResolvedValue({
folders: ['产品文档'],
files: [{ key: '需求说明.md', name: '需求说明.md', size: 100, modifiedAt: '2026-08-01T10:00:00Z' }],
});
render(<FileExplorer onSelectFile={vi.fn()} selectedKey={null} />);
expect(await screen.findByText('产品文档')).toBeInTheDocument();
expect(await screen.findByText('需求说明.md')).toBeInTheDocument();
expect(listDocuments).toHaveBeenCalledWith('');
});
test('clicking a file calls onSelectFile with its key', async () => {
vi.mocked(listDocuments).mockResolvedValue({
folders: [],
files: [{ key: '架构设计.md', name: '架构设计.md', size: 200, modifiedAt: '2026-08-01T10:00:00Z' }],
});
const onSelectFile = vi.fn();
render(<FileExplorer onSelectFile={onSelectFile} selectedKey={null} />);
const fileRow = await screen.findByText('架构设计.md');
fireEvent.click(fileRow);
expect(onSelectFile).toHaveBeenCalledWith('架构设计.md');
});
test('clicking a folder loads and shows its nested contents', async () => {
vi.mocked(listDocuments).mockImplementation((path: string) => {
if (path === '') {
return Promise.resolve({ folders: ['产品文档'], files: [] });
}
if (path === '产品文档') {
return Promise.resolve({
folders: [],
files: [{ key: '产品文档/架构设计.md', name: '架构设计.md', size: 300, modifiedAt: '2026-08-01T10:00:00Z' }],
});
}
return Promise.resolve({ folders: [], files: [] });
});
render(<FileExplorer onSelectFile={vi.fn()} selectedKey={null} />);
const folderRow = await screen.findByText('产品文档');
fireEvent.click(folderRow);
expect(await screen.findByText('架构设计.md')).toBeInTheDocument();
expect(listDocuments).toHaveBeenCalledWith('产品文档');
});
test('shows an error message when listDocuments rejects', async () => {
vi.mocked(listDocuments).mockRejectedValue(new Error('network error'));
render(<FileExplorer onSelectFile={vi.fn()} selectedKey={null} />);
expect(await screen.findByTestId('file-explorer-error')).toBeInTheDocument();
});
test('changing refreshKey re-fetches the root listing', async () => {
vi.mocked(listDocuments).mockResolvedValue({ folders: [], files: [] });
const { rerender } = render(<FileExplorer onSelectFile={vi.fn()} selectedKey={null} refreshKey={0} />);
await waitFor(() => {
expect(listDocuments).toHaveBeenCalledTimes(1);
});
rerender(<FileExplorer onSelectFile={vi.fn()} selectedKey={null} refreshKey={1} />);
await waitFor(() => {
expect(listDocuments).toHaveBeenCalledTimes(2);
});
});