feat: wire FileExplorer, DocumentEditor, search and upload into App
This commit is contained in:
parent
90f3784ffa
commit
ad16c1732d
@ -1,7 +1,46 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import FileExplorer from './components/FileExplorer';
|
||||||
|
import DocumentEditor from './components/DocumentEditor';
|
||||||
|
import SearchBar from './components/SearchBar';
|
||||||
|
import SearchResults from './components/SearchResults';
|
||||||
|
import UploadButton from './components/UploadButton';
|
||||||
|
import type { SearchResult, UploadDocumentResponse } from './api/client';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
||||||
|
|
||||||
|
function handleJumpToResult(key: string) {
|
||||||
|
setSelectedKey(key);
|
||||||
|
setSearchQuery('');
|
||||||
|
setSearchResults([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUploaded(result: UploadDocumentResponse) {
|
||||||
|
setSelectedKey(result.key);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
|
<div className="topbar">
|
||||||
<h1>DocHub</h1>
|
<h1>DocHub</h1>
|
||||||
|
<SearchBar onQueryChange={setSearchQuery} onResults={setSearchResults} />
|
||||||
|
<UploadButton path="" onUploaded={handleUploaded} />
|
||||||
|
</div>
|
||||||
|
<div className="body">
|
||||||
|
<FileExplorer onSelectFile={setSelectedKey} selectedKey={selectedKey} />
|
||||||
|
<div className="main" data-testid="main-area">
|
||||||
|
{selectedKey ? (
|
||||||
|
<DocumentEditor documentKey={selectedKey} />
|
||||||
|
) : (
|
||||||
|
<div data-testid="empty-state">未选择文档</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{searchQuery !== '' && (
|
||||||
|
<SearchResults query={searchQuery} results={searchResults} onJumpToResult={handleJumpToResult} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,102 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { test, expect } from 'vitest';
|
import { test, expect, beforeEach, vi } from 'vitest';
|
||||||
import App from '../src/App';
|
import App from '../src/App';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { waitFor } from '@testing-library/react';
|
||||||
|
import { listDocuments, getDocument, search } 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(listDocuments).mockReset();
|
||||||
|
vi.mocked(getDocument).mockReset();
|
||||||
|
vi.mocked(search).mockReset();
|
||||||
|
vi.mocked(listDocuments).mockResolvedValue({ folders: [], files: [] });
|
||||||
|
vi.mocked(getDocument).mockResolvedValue({
|
||||||
|
key: '',
|
||||||
|
content: '',
|
||||||
|
contentType: 'text/markdown',
|
||||||
|
});
|
||||||
|
vi.mocked(search).mockResolvedValue({ results: [] });
|
||||||
|
});
|
||||||
|
|
||||||
test('renders the DocHub heading', () => {
|
test('renders the DocHub heading', () => {
|
||||||
render(<App />);
|
render(<App />);
|
||||||
expect(screen.getByRole('heading', { name: 'DocHub' })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: 'DocHub' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shows an empty state and a file tree when no document is selected', async () => {
|
||||||
|
vi.mocked(listDocuments).mockResolvedValue({
|
||||||
|
folders: [],
|
||||||
|
files: [{ key: '产品文档/架构设计.md', name: '架构设计.md', size: 100, modifiedAt: '2026-08-01T10:00:00Z' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('empty-state')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText('架构设计.md')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selecting a file from the tree loads it into the editor', async () => {
|
||||||
|
vi.mocked(listDocuments).mockResolvedValue({
|
||||||
|
folders: [],
|
||||||
|
files: [{ key: '产品文档/架构设计.md', name: '架构设计.md', size: 100, modifiedAt: '2026-08-01T10:00:00Z' }],
|
||||||
|
});
|
||||||
|
vi.mocked(getDocument).mockResolvedValue({
|
||||||
|
key: '产品文档/架构设计.md',
|
||||||
|
content: '# 架构设计',
|
||||||
|
contentType: 'text/markdown',
|
||||||
|
});
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
const fileRow = await screen.findByText('架构设计.md');
|
||||||
|
await user.click(fileRow);
|
||||||
|
|
||||||
|
const textarea = await screen.findByTestId('monaco-editor-mock');
|
||||||
|
expect(textarea).toHaveValue('# 架构设计');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('searching shows the results overlay, and clicking a result opens the document', async () => {
|
||||||
|
vi.mocked(listDocuments).mockResolvedValue({ folders: [], files: [] });
|
||||||
|
vi.mocked(search).mockResolvedValue({
|
||||||
|
results: [{ key: '产品文档/架构设计.md', title: '架构设计.md', path: '产品文档', snippet: '...<em>检索</em>...' }],
|
||||||
|
});
|
||||||
|
vi.mocked(getDocument).mockResolvedValue({
|
||||||
|
key: '产品文档/架构设计.md',
|
||||||
|
content: '# 架构设计',
|
||||||
|
contentType: 'text/markdown',
|
||||||
|
});
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
const searchInput = screen.getByRole('textbox', { name: '搜索文档' });
|
||||||
|
await user.type(searchInput, '检索');
|
||||||
|
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(screen.getByTestId('search-result-产品文档/架构设计.md')).toBeInTheDocument();
|
||||||
|
},
|
||||||
|
{ timeout: 1000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByTestId('search-result-产品文档/架构设计.md'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByTestId('search-results')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
const textarea = await screen.findByTestId('monaco-editor-mock');
|
||||||
|
expect(textarea).toHaveValue('# 架构设计');
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user