Files
zclaw_openfang/admin-v2/tests/pages/Knowledge.test.tsx
iven 803464b492
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
test(admin-v2): Phase 2 frontend tests — 61 tests for 5 pages
- Billing (13 tests): plan cards, prices, limits, usage bars, payment flow
- ScheduledTasks (16 tests): CRUD table, schedule/target types, color tags
- Knowledge (12 tests): 4 tabs, items/categories/search/analytics panels
- Roles (12 tests): roles + permission templates tabs
- ConfigSync (8 tests): sync log viewer with action labels

Fix: Knowledge.tsx missing </Select> and </Modal> closing tags (JSX parse error)
Fix: tests/setup.ts added ResizeObserver mock for ProTable compatibility
2026-04-07 16:06:47 +08:00

300 lines
10 KiB
TypeScript

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(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>,
)
}
function setupKnowledgeHandlers(overrides: Record<string, unknown> = {}) {
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(<Knowledge />)
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(<Knowledge />)
await waitFor(() => {
expect(screen.getByText('API 认证指南')).toBeInTheDocument()
})
expect(screen.getByText('玩具市场趋势 2026')).toBeInTheDocument()
})
it('displays item status with Chinese labels', async () => {
setupKnowledgeHandlers()
renderWithProviders(<Knowledge />)
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(<Knowledge />)
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(<Knowledge />)
await waitFor(() => {
const empties = screen.getAllByText('暂无数据')
expect(empties.length).toBeGreaterThanOrEqual(1)
})
})
// ── Categories Tab ───────────────────────────────────────────
it('switches to categories tab and displays categories', async () => {
setupKnowledgeHandlers()
renderWithProviders(<Knowledge />)
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(<Knowledge />)
// 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(<Knowledge />)
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(<Knowledge />)
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(<Knowledge />)
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(<Knowledge />)
expect(document.querySelector('.ant-spin')).toBeTruthy()
})
// ── Item Tags ────────────────────────────────────────────────
it('displays item tags in table', async () => {
setupKnowledgeHandlers()
renderWithProviders(<Knowledge />)
await waitFor(() => {
expect(screen.getByText('api')).toBeInTheDocument()
})
expect(screen.getByText('auth')).toBeInTheDocument()
expect(screen.getByText('market')).toBeInTheDocument()
})
})