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

134 lines
4.5 KiB
TypeScript

import { render, screen } from '@testing-library/react';
import { vi, test, expect, beforeEach } from 'vitest';
import DocumentEditor from '../../src/components/DocumentEditor';
import { getDocument, saveDocument } from '../../src/api/client';
vi.mock('../../src/api/client');
vi.mock('@monaco-editor/react', () => ({
default: ({ value, onChange }: { value: string; onChange: (value: string | undefined) => void }) => (
<textarea
data-testid="monaco-editor-mock"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
),
}));
beforeEach(() => {
vi.mocked(getDocument).mockReset();
vi.mocked(saveDocument).mockReset();
});
test('shows a loading state, then loads and displays document content', async () => {
vi.mocked(getDocument).mockResolvedValue({
key: '产品文档/架构设计.md',
content: '# 架构设计',
contentType: 'text/markdown',
});
render(<DocumentEditor documentKey="产品文档/架构设计.md" />);
expect(screen.getByTestId('document-editor-loading')).toBeInTheDocument();
const textarea = await screen.findByTestId('monaco-editor-mock');
expect(textarea).toHaveValue('# 架构设计');
expect(getDocument).toHaveBeenCalledWith('产品文档/架构设计.md');
expect(screen.getByTestId('save-status')).toHaveTextContent('已归档');
});
import userEvent from '@testing-library/user-event';
import { waitFor } from '@testing-library/react';
test('typing marks the document dirty and saving clears the dirty state', async () => {
vi.mocked(getDocument).mockResolvedValue({
key: '产品文档/架构设计.md',
content: '# 架构设计',
contentType: 'text/markdown',
});
vi.mocked(saveDocument).mockResolvedValue({
key: '产品文档/架构设计.md',
updatedAt: '2026-08-01T10:05:00Z',
});
const user = userEvent.setup();
render(<DocumentEditor documentKey="产品文档/架构设计.md" />);
const textarea = await screen.findByTestId('monaco-editor-mock');
await user.clear(textarea);
await user.type(textarea, '# 新内容');
expect(screen.getByTestId('save-status')).toHaveTextContent('未保存');
const saveButton = screen.getByRole('button', { name: '保存' });
await user.click(saveButton);
await waitFor(() => {
expect(saveDocument).toHaveBeenCalledWith('产品文档/架构设计.md', '# 新内容', 'text/markdown');
});
await waitFor(() => {
expect(screen.getByTestId('save-status')).toHaveTextContent('已归档');
});
});
test('save failure keeps the editor and unsaved content visible, shows an error, and allows retry', async () => {
vi.mocked(getDocument).mockResolvedValue({
key: '产品文档/架构设计.md',
content: '# 架构设计',
contentType: 'text/markdown',
});
vi.mocked(saveDocument).mockRejectedValueOnce(new Error('network error'));
vi.mocked(saveDocument).mockResolvedValueOnce({
key: '产品文档/架构设计.md',
updatedAt: '2026-08-01T10:05:00Z',
});
const user = userEvent.setup();
render(<DocumentEditor documentKey="产品文档/架构设计.md" />);
const textarea = await screen.findByTestId('monaco-editor-mock');
await user.clear(textarea);
await user.type(textarea, '# 未保存的修改');
const saveButton = screen.getByRole('button', { name: '保存' });
await user.click(saveButton);
await waitFor(() => {
expect(screen.getByTestId('save-error')).toBeInTheDocument();
});
expect(screen.getByTestId('document-editor')).toBeInTheDocument();
expect(screen.getByTestId('monaco-editor-mock')).toHaveValue('# 未保存的修改');
expect(screen.getByTestId('save-status')).toHaveTextContent('未保存');
await user.click(saveButton);
await waitFor(() => {
expect(screen.getByTestId('save-status')).toHaveTextContent('已归档');
});
expect(screen.queryByTestId('save-error')).not.toBeInTheDocument();
});
test('calls onDirtyChange when dirty state changes', async () => {
vi.mocked(getDocument).mockResolvedValue({
key: '产品文档/架构设计.md',
content: '# 架构设计',
contentType: 'text/markdown',
});
const onDirtyChange = vi.fn();
const user = userEvent.setup();
render(<DocumentEditor documentKey="产品文档/架构设计.md" onDirtyChange={onDirtyChange} />);
const textarea = await screen.findByTestId('monaco-editor-mock');
await waitFor(() => {
expect(onDirtyChange).toHaveBeenLastCalledWith(false);
});
await user.type(textarea, '追加内容');
await waitFor(() => {
expect(onDirtyChange).toHaveBeenLastCalledWith(true);
});
});