import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { render, screen, waitFor, fireEvent, act } from '@testing-library/react' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import Knowledge from '@/pages/Knowledge' // ── Mock data ────────────────────────────────────────────────── const mockCategories = [ { id: 'cat-001', name: '技术文档', description: '技术相关文档', parent_id: null, icon: '📚', sort_order: 0, item_count: 5, children: [], created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', }, { id: 'cat-002', name: '行业知识', description: '行业相关知识', parent_id: null, icon: '🏭', sort_order: 1, item_count: 3, children: [], created_at: '2026-01-15T00:00:00Z', updated_at: '2026-01-15T00:00:00Z', }, ] const mockItems = { items: [ { id: 'item-001', category_id: 'cat-001', title: 'API 认证指南', content: 'JWT 认证流程说明...', keywords: ['认证', 'JWT'], related_questions: [], priority: 5, status: 'active', version: 2, source: 'manual', tags: ['api', 'auth'], created_by: 'admin', created_at: '2026-02-01T00:00:00Z', updated_at: '2026-03-01T00:00:00Z', }, { id: 'item-002', category_id: 'cat-002', title: '玩具市场趋势 2026', content: '2026 年玩具行业趋势分析...', keywords: ['市场', '趋势'], related_questions: [], priority: 3, status: 'active', version: 1, source: 'import', tags: ['market'], created_by: 'admin', created_at: '2026-03-01T00:00:00Z', updated_at: '2026-03-01T00:00:00Z', }, ], total: 2, page: 1, page_size: 20, } const mockOverview = { total_items: 8, active_items: 6, total_categories: 2, weekly_new_items: 1, total_references: 45, avg_reference_per_item: 5.6, hit_rate: 0.78, injection_rate: 0.65, positive_feedback_rate: 0.92, stale_items_count: 1, } const mockTrends = { trends: [{ date: '2026-04-01', new_items: 2, references: 10, queries: 25 }] } const mockTopItems = { items: [] } const mockQuality = { metrics: [] } const mockGaps = { gaps: [] } const server = setupServer() beforeEach(() => { server.listen({ onUnhandledRequest: 'bypass' }) }) afterEach(() => { server.close() }) function renderWithProviders(ui: React.ReactElement) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }) return render( {ui} , ) } function setupKnowledgeHandlers(overrides: Record = {}) { server.use( http.get('*/api/v1/knowledge/categories', () => { return HttpResponse.json(overrides.categories ?? mockCategories) }), http.get('*/api/v1/knowledge/items', () => { return HttpResponse.json(overrides.items ?? mockItems) }), http.get('*/api/v1/knowledge/analytics/overview', () => { return HttpResponse.json(overrides.overview ?? mockOverview) }), http.get('*/api/v1/knowledge/analytics/trends', () => { return HttpResponse.json(overrides.trends ?? mockTrends) }), http.get('*/api/v1/knowledge/analytics/top-items', () => { return HttpResponse.json(overrides.topItems ?? mockTopItems) }), http.get('*/api/v1/knowledge/analytics/quality', () => { return HttpResponse.json(overrides.quality ?? mockQuality) }), http.get('*/api/v1/knowledge/analytics/gaps', () => { return HttpResponse.json(overrides.gaps ?? mockGaps) }), ) } describe('Knowledge', () => { // ── Tab structure ───────────────────────────────────────────── it('renders all tab labels', () => { setupKnowledgeHandlers() renderWithProviders() expect(screen.getByText('知识条目')).toBeInTheDocument() expect(screen.getByText('分类管理')).toBeInTheDocument() expect(screen.getByText('搜索')).toBeInTheDocument() expect(screen.getByText('分析看板')).toBeInTheDocument() }) // ── Items Tab (default) ────────────────────────────────────── it('displays items in default tab', async () => { setupKnowledgeHandlers() renderWithProviders() await waitFor(() => { expect(screen.getByText('API 认证指南')).toBeInTheDocument() }) expect(screen.getByText('玩具市场趋势 2026')).toBeInTheDocument() }) it('displays item status with Chinese labels', async () => { setupKnowledgeHandlers() renderWithProviders() await waitFor(() => { // status "active" is displayed as "活跃" via statusLabels mapping const activeLabels = screen.getAllByText('活跃') expect(activeLabels.length).toBeGreaterThanOrEqual(2) }) }) it('displays item version column', async () => { setupKnowledgeHandlers() renderWithProviders() await waitFor(() => { expect(screen.getByText('API 认证指南')).toBeInTheDocument() }) // Version numbers in the table expect(screen.getByText('2')).toBeInTheDocument() }) it('shows empty state when no items', async () => { setupKnowledgeHandlers({ items: { items: [], total: 0, page: 1, page_size: 20 }, }) renderWithProviders() await waitFor(() => { const empties = screen.getAllByText('暂无数据') expect(empties.length).toBeGreaterThanOrEqual(1) }) }) // ── Categories Tab ─────────────────────────────────────────── it('switches to categories tab and displays categories', async () => { setupKnowledgeHandlers() renderWithProviders() await waitFor(() => { expect(screen.getByText('API 认证指南')).toBeInTheDocument() }) // Find the tab button (not the panel heading) and click it const categoryTabs = screen.getAllByText('分类管理') await act(async () => { fireEvent.click(categoryTabs[0]) }) // Wait for the categories panel to render its heading and tree await waitFor(() => { // "新建分类" button should appear in the CategoriesPanel toolbar expect(screen.getByText('新建分类')).toBeInTheDocument() }, { timeout: 3000 }) // Category names rendered via Tree component inside spans with icon prefix // Use stringContaining since the text includes icon emoji prefix expect(screen.getByText((content) => content.includes('技术文档'))).toBeInTheDocument() expect(screen.getByText((content) => content.includes('行业知识'))).toBeInTheDocument() }) it('shows empty categories state', async () => { setupKnowledgeHandlers({ categories: [] }) renderWithProviders() // Wait for items tab to load first await waitFor(() => { expect(screen.getByText('API 认证指南')).toBeInTheDocument() }) // Switch to categories tab const categoryTabs = screen.getAllByText('分类管理') await act(async () => { fireEvent.click(categoryTabs[0]) }) // The CategoriesPanel should show "暂无分类,请新建一个" for empty state await waitFor(() => { expect(screen.getByText('暂无分类,请新建一个')).toBeInTheDocument() }, { timeout: 3000 }) }) // ── Analytics Tab ──────────────────────────────────────────── it('switches to analytics tab and shows overview stats', async () => { setupKnowledgeHandlers() renderWithProviders() const analyticsTab = screen.getByText('分析看板') await act(async () => { fireEvent.click(analyticsTab) }) await waitFor(() => { expect(screen.getByText('总条目数')).toBeInTheDocument() }) expect(screen.getByText('活跃条目')).toBeInTheDocument() expect(screen.getByText('分类数')).toBeInTheDocument() expect(screen.getByText('本周新增')).toBeInTheDocument() }) it('displays analytics numbers', async () => { setupKnowledgeHandlers() renderWithProviders() const analyticsTab = screen.getByText('分析看板') await act(async () => { fireEvent.click(analyticsTab) }) await waitFor(() => { // total_items: 8 expect(screen.getByText('8')).toBeInTheDocument() }) }) // ── Error Handling ─────────────────────────────────────────── it('shows empty on API failure', async () => { server.use( http.get('*/api/v1/knowledge/categories', () => { return HttpResponse.json( { error: 'internal_error', message: '数据库错误' }, { status: 500 }, ) }), http.get('*/api/v1/knowledge/items', () => { return HttpResponse.json( { error: 'internal_error', message: '数据库错误' }, { status: 500 }, ) }), ) renderWithProviders() await waitFor(() => { const empties = screen.getAllByText('暂无数据') expect(empties.length).toBeGreaterThanOrEqual(1) }) }) // ── Loading State ──────────────────────────────────────────── it('shows loading spinner', async () => { server.use( http.get('*/api/v1/knowledge/categories', async () => { await new Promise(resolve => setTimeout(resolve, 500)) return HttpResponse.json(mockCategories) }), http.get('*/api/v1/knowledge/items', async () => { await new Promise(resolve => setTimeout(resolve, 500)) return HttpResponse.json(mockItems) }), ) renderWithProviders() expect(document.querySelector('.ant-spin')).toBeTruthy() }) // ── Item Tags ──────────────────────────────────────────────── it('displays item tags in table', async () => { setupKnowledgeHandlers() renderWithProviders() await waitFor(() => { expect(screen.getByText('api')).toBeInTheDocument() }) expect(screen.getByText('auth')).toBeInTheDocument() expect(screen.getByText('market')).toBeInTheDocument() }) })