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
- 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
300 lines
10 KiB
TypeScript
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()
|
|
})
|
|
})
|