901 lines
27 KiB
TypeScript
901 lines
27 KiB
TypeScript
import {
|
|
act,
|
|
cleanup,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
within
|
|
} from '@testing-library/react'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
KnowledgeWorkspace,
|
|
type KnowledgeWorkspaceProps
|
|
} from './KnowledgeWorkspace'
|
|
import i18n from './i18n'
|
|
|
|
const g6Mock = vi.hoisted(() => {
|
|
const handlers = new Map<string, (event: unknown) => void>()
|
|
const graph = {
|
|
destroy: vi.fn(),
|
|
draw: vi.fn(async () => undefined),
|
|
fitView: vi.fn(async () => undefined),
|
|
getElementPosition: vi.fn(() => [240, 320]),
|
|
getZoom: vi.fn(() => 1),
|
|
off: vi.fn((eventName: string) => handlers.delete(eventName)),
|
|
on: vi.fn((eventName: string, handler: (event: unknown) => void) => {
|
|
handlers.set(eventName, handler)
|
|
}),
|
|
render: vi.fn(async () => undefined),
|
|
resize: vi.fn(),
|
|
setData: vi.fn(),
|
|
setEdge: vi.fn(),
|
|
setElementState: vi.fn(async () => undefined),
|
|
setLayout: vi.fn(),
|
|
setNode: vi.fn(),
|
|
setOptions: vi.fn(),
|
|
zoomTo: vi.fn(async () => undefined)
|
|
}
|
|
return {
|
|
graph,
|
|
handlers,
|
|
Graph: vi.fn(function () {
|
|
return graph
|
|
})
|
|
}
|
|
})
|
|
|
|
vi.mock('@antv/g6', () => ({
|
|
Graph: g6Mock.Graph,
|
|
GraphEvent: {
|
|
AFTER_TRANSFORM: 'aftertransform'
|
|
},
|
|
NodeEvent: {
|
|
CLICK: 'node:click',
|
|
DRAG_END: 'node:dragend'
|
|
}
|
|
}))
|
|
|
|
const library: KnowledgeWorkspaceProps['libraries'][number] = {
|
|
id: 'library-1',
|
|
name: '产品知识',
|
|
description: '产品设计与研发资料',
|
|
storageMode: 'managed',
|
|
graphEnabled: true,
|
|
graphStrategy: 'hybrid',
|
|
sourceCount: 1,
|
|
documentCount: 1,
|
|
indexedDocumentCount: 1,
|
|
updatedAt: '2026-07-30T08:00:00.000Z'
|
|
}
|
|
|
|
function createProps(
|
|
overrides: Partial<KnowledgeWorkspaceProps> = {}
|
|
): KnowledgeWorkspaceProps {
|
|
return {
|
|
libraries: [library],
|
|
selectedLibraryId: library.id,
|
|
sources: [
|
|
{
|
|
id: 'source-1',
|
|
libraryId: library.id,
|
|
name: '产品手册',
|
|
kind: 'directory',
|
|
location: 'D:\\Private\\产品手册',
|
|
status: 'ready',
|
|
documentCount: 1,
|
|
lastSyncedAt: '2026-07-30T08:00:00.000Z'
|
|
}
|
|
],
|
|
documents: [
|
|
{
|
|
id: 'document-1',
|
|
libraryId: library.id,
|
|
sourceId: 'source-1',
|
|
name: '架构说明.md',
|
|
path: 'D:\\Private\\架构说明.md',
|
|
status: 'ready',
|
|
indexProgress: 100,
|
|
chunkCount: 12,
|
|
size: 2048
|
|
}
|
|
],
|
|
graphNodes: [
|
|
{
|
|
id: 'entity-1',
|
|
label: 'GoodBuddy',
|
|
type: '产品',
|
|
description: '跨平台 AI 桌面助手',
|
|
aliases: ['好伙伴'],
|
|
x: 180,
|
|
y: 180,
|
|
evidenceIds: ['evidence-1']
|
|
},
|
|
{
|
|
id: 'entity-2',
|
|
label: 'Electron',
|
|
type: '技术',
|
|
x: 480,
|
|
y: 240
|
|
}
|
|
],
|
|
graphRelations: [
|
|
{
|
|
id: 'relation-1',
|
|
sourceId: 'entity-1',
|
|
targetId: 'entity-2',
|
|
type: '使用'
|
|
}
|
|
],
|
|
evidence: [
|
|
{
|
|
id: 'evidence-1',
|
|
documentId: 'document-1',
|
|
documentName: '架构说明.md',
|
|
excerpt: 'GoodBuddy 使用 Electron 构建。',
|
|
location: '第 2 段'
|
|
}
|
|
],
|
|
onSelectLibrary: vi.fn(),
|
|
onCreateLibrary: vi.fn(),
|
|
onDeleteLibrary: vi.fn(),
|
|
onUpdateLibrary: vi.fn(),
|
|
onReextractGraph: vi.fn(),
|
|
onImportFiles: vi.fn(),
|
|
onImportDirectory: vi.fn(),
|
|
onImportUrl: vi.fn(),
|
|
onSyncSource: vi.fn(),
|
|
onPauseSource: vi.fn(),
|
|
onRetrySource: vi.fn(),
|
|
onRemoveSource: vi.fn(),
|
|
onMoveNode: vi.fn(),
|
|
onCreateEntity: vi.fn(),
|
|
onUpdateEntity: vi.fn(),
|
|
onDeleteEntity: vi.fn(),
|
|
onMergeEntities: vi.fn(),
|
|
onCreateRelation: vi.fn(),
|
|
onUpdateRelation: vi.fn(),
|
|
onDeleteRelation: vi.fn(),
|
|
onRetryLoad: vi.fn(),
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
describe('KnowledgeWorkspace', () => {
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.clearAllMocks()
|
|
g6Mock.handlers.clear()
|
|
g6Mock.graph.getZoom.mockReturnValue(1)
|
|
g6Mock.graph.getElementPosition.mockReturnValue([240, 320])
|
|
})
|
|
|
|
it('creates a configured knowledge library', async () => {
|
|
const onCreateLibrary = vi.fn()
|
|
render(
|
|
<KnowledgeWorkspace
|
|
{...createProps({ onCreateLibrary })}
|
|
/>
|
|
)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '新建知识库' }))
|
|
fireEvent.change(screen.getByLabelText('名称'), {
|
|
target: { value: '客户研究' }
|
|
})
|
|
fireEvent.change(screen.getByLabelText('描述'), {
|
|
target: { value: '访谈与反馈' }
|
|
})
|
|
fireEvent.click(screen.getByLabelText(/引用原文件/))
|
|
expect(
|
|
screen.getByRole('switch', { name: /启用知识图谱/u })
|
|
).toBeChecked()
|
|
fireEvent.change(screen.getByLabelText('图谱生成策略'), {
|
|
target: { value: 'rules' }
|
|
})
|
|
fireEvent.click(screen.getByRole('button', { name: '创建知识库' }))
|
|
|
|
await waitFor(() =>
|
|
expect(onCreateLibrary).toHaveBeenCalledWith({
|
|
name: '客户研究',
|
|
description: '访谈与反馈',
|
|
storageMode: 'reference',
|
|
graphEnabled: true,
|
|
graphStrategy: 'rules'
|
|
})
|
|
)
|
|
})
|
|
|
|
it('renders English interface copy without translating knowledge content', async () => {
|
|
await i18n.changeLanguage('en-US')
|
|
render(<KnowledgeWorkspace {...createProps()} />)
|
|
|
|
expect(
|
|
screen.getByRole('heading', { name: 'Knowledge Base' })
|
|
).toBeInTheDocument()
|
|
expect(
|
|
screen.getByRole('tab', { name: 'Documents and sources' })
|
|
).toBeInTheDocument()
|
|
expect(
|
|
screen.getByRole('heading', { name: '产品知识' })
|
|
).toBeInTheDocument()
|
|
expect(screen.getByText('架构说明.md')).toBeInTheDocument()
|
|
expect(screen.getByText('Local file · 架构说明.md')).toBeInTheDocument()
|
|
})
|
|
|
|
it('imports an HTTP URL into the selected library', async () => {
|
|
const onImportUrl = vi.fn()
|
|
render(
|
|
<KnowledgeWorkspace {...createProps({ onImportUrl })} />
|
|
)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '导入 URL' }))
|
|
fireEvent.change(screen.getByLabelText('URL 地址'), {
|
|
target: { value: 'https://example.com/guide' }
|
|
})
|
|
fireEvent.click(screen.getByRole('button', { name: '导入' }))
|
|
|
|
await waitFor(() =>
|
|
expect(onImportUrl).toHaveBeenCalledWith(
|
|
'library-1',
|
|
'https://example.com/guide',
|
|
undefined
|
|
)
|
|
)
|
|
})
|
|
|
|
it('switches to the graph and opens entity details', () => {
|
|
render(<KnowledgeWorkspace {...createProps()} />)
|
|
|
|
fireEvent.click(screen.getByRole('tab', { name: '知识图谱' }))
|
|
expect(screen.getByLabelText('实体关系图')).toBeInTheDocument()
|
|
expect(
|
|
screen.getByRole('tab', { name: /拓扑/u })
|
|
).toHaveAttribute('aria-selected', 'true')
|
|
expect(screen.getByLabelText('图谱拓扑')).toBeInTheDocument()
|
|
|
|
fireEvent.change(screen.getByLabelText('选择图谱实体'), {
|
|
target: { value: 'entity-1' }
|
|
})
|
|
expect(
|
|
screen.getByRole('tab', { name: '详情' })
|
|
).toHaveAttribute('aria-selected', 'true')
|
|
expect(screen.getByLabelText('实体详情')).toBeInTheDocument()
|
|
expect(screen.getByText('跨平台 AI 桌面助手')).toBeInTheDocument()
|
|
expect(screen.getByText('架构说明.md')).toBeInTheDocument()
|
|
})
|
|
|
|
it('uses shared tabs and keeps graph configuration in settings', () => {
|
|
const onUpdateLibrary = vi.fn()
|
|
render(<KnowledgeWorkspace {...createProps({ onUpdateLibrary })} />)
|
|
|
|
const tabs = screen.getByRole('tablist', { name: '知识库视图' })
|
|
expect(within(tabs).getAllByRole('tab').map((item) => item.textContent))
|
|
.toEqual(['文档与来源', '知识图谱', '任务中心', '设置'])
|
|
expect(
|
|
screen.queryByRole('switch', { name: '知识图谱' })
|
|
).not.toBeInTheDocument()
|
|
|
|
fireEvent.click(screen.getByRole('tab', { name: '设置' }))
|
|
fireEvent.click(screen.getByRole('switch', { name: /启用知识图谱/u }))
|
|
expect(onUpdateLibrary).toHaveBeenCalledWith('library-1', {
|
|
graphEnabled: false
|
|
})
|
|
})
|
|
|
|
it('shows parsing, embedding, and graph progress in the task center', () => {
|
|
render(
|
|
<KnowledgeWorkspace
|
|
{...createProps({
|
|
tasks: [
|
|
{
|
|
id: 'task-1',
|
|
libraryId: 'library-1',
|
|
documentId: 'document-1',
|
|
documentName: '架构说明.md',
|
|
kind: 'graph',
|
|
status: 'running',
|
|
progress: 40,
|
|
message: '正在重新抽取知识图谱',
|
|
createdAt: '2026-08-10T08:00:00.000Z',
|
|
startedAt: '2026-08-10T08:00:01.000Z'
|
|
}
|
|
]
|
|
})}
|
|
/>
|
|
)
|
|
|
|
fireEvent.click(
|
|
screen.getByRole('tab', { name: /^任务中心/u })
|
|
)
|
|
expect(screen.getByText('图谱抽取')).toBeInTheDocument()
|
|
expect(screen.getByText('正在重新抽取知识图谱')).toBeInTheDocument()
|
|
expect(
|
|
screen.getByRole('progressbar', {
|
|
name: '架构说明.md 图谱抽取进度'
|
|
})
|
|
).toHaveValue(40)
|
|
})
|
|
|
|
it('edits library metadata from the detail header', async () => {
|
|
const onUpdateLibrary = vi.fn()
|
|
render(<KnowledgeWorkspace {...createProps({ onUpdateLibrary })} />)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '编辑' }))
|
|
fireEvent.change(screen.getByLabelText('名称'), {
|
|
target: { value: '研发知识' }
|
|
})
|
|
fireEvent.change(screen.getByLabelText('描述'), {
|
|
target: { value: '研发资料与设计说明' }
|
|
})
|
|
fireEvent.click(screen.getByRole('button', { name: '保存修改' }))
|
|
|
|
await waitFor(() =>
|
|
expect(onUpdateLibrary).toHaveBeenCalledWith('library-1', {
|
|
name: '研发知识',
|
|
description: '研发资料与设计说明'
|
|
})
|
|
)
|
|
})
|
|
|
|
it('reextracts the graph from the graph tab', async () => {
|
|
const onReextractGraph = vi.fn()
|
|
render(<KnowledgeWorkspace {...createProps({ onReextractGraph })} />)
|
|
|
|
fireEvent.click(screen.getByRole('tab', { name: '知识图谱' }))
|
|
fireEvent.click(screen.getByRole('button', { name: '重新抽取' }))
|
|
|
|
await waitFor(() =>
|
|
expect(onReextractGraph).toHaveBeenCalledWith('library-1')
|
|
)
|
|
})
|
|
|
|
it('renders and filters graph nodes with their relationships', async () => {
|
|
render(<KnowledgeWorkspace {...createProps()} />)
|
|
|
|
fireEvent.click(screen.getByRole('tab', { name: '知识图谱' }))
|
|
expect(
|
|
screen.getByRole('option', { name: 'GoodBuddy · 产品' })
|
|
).toBeInTheDocument()
|
|
expect(
|
|
screen.getByRole('option', { name: 'Electron · 技术' })
|
|
).toBeInTheDocument()
|
|
expect(screen.getByText('可见关系')).toBeInTheDocument()
|
|
expect(screen.getByText('使用')).toBeInTheDocument()
|
|
|
|
fireEvent.change(screen.getByLabelText('搜索图谱实体'), {
|
|
target: { value: 'Electron' }
|
|
})
|
|
expect(
|
|
screen.queryByRole('option', { name: 'GoodBuddy · 产品' })
|
|
).not.toBeInTheDocument()
|
|
expect(screen.queryByText('使用')).not.toBeInTheDocument()
|
|
expect(g6Mock.graph.setOptions).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
data: {
|
|
nodes: [
|
|
expect.objectContaining({
|
|
id: 'entity-2'
|
|
})
|
|
],
|
|
edges: []
|
|
}
|
|
})
|
|
)
|
|
|
|
fireEvent.change(screen.getByLabelText('搜索图谱实体'), {
|
|
target: { value: '' }
|
|
})
|
|
fireEvent.change(screen.getByLabelText('筛选实体类型'), {
|
|
target: { value: '产品' }
|
|
})
|
|
expect(
|
|
screen.getByRole('option', { name: 'GoodBuddy · 产品' })
|
|
).toBeInTheDocument()
|
|
expect(
|
|
screen.queryByRole('option', { name: 'Electron · 技术' })
|
|
).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('provides responsive workspace and graph layout hooks', () => {
|
|
render(<KnowledgeWorkspace {...createProps()} />)
|
|
|
|
const workspace = screen.getByLabelText('知识工作区')
|
|
expect(workspace).toHaveClass('knowledge-workspace')
|
|
expect(workspace).toHaveStyle({
|
|
background: 'var(--surface-canvas)'
|
|
})
|
|
expect(workspace.querySelector('aside')).toHaveClass(
|
|
'knowledge-workspace__sidebar'
|
|
)
|
|
expect(workspace.querySelector('aside')).not.toHaveAttribute('style')
|
|
const detailRegion = within(workspace).getByRole('region', {
|
|
name: '知识库详情'
|
|
})
|
|
expect(detailRegion).toHaveClass('knowledge-workspace__main')
|
|
expect(detailRegion).toHaveStyle({
|
|
background: 'var(--surface-raised)'
|
|
})
|
|
expect(screen.getByText('全局')).toHaveClass('scope-badge')
|
|
const mobileBack = screen.getByRole('button', {
|
|
name: '返回知识库列表'
|
|
})
|
|
expect(mobileBack).toHaveClass('knowledge-workspace__mobile-back')
|
|
fireEvent.click(mobileBack)
|
|
expect(workspace).toHaveClass('knowledge-workspace--mobile-list')
|
|
fireEvent.click(
|
|
screen.getByRole('button', {
|
|
name: /^产品知识 1 个文档/u
|
|
})
|
|
)
|
|
expect(workspace).not.toHaveClass('knowledge-workspace--mobile-list')
|
|
expect(screen.getByRole('tablist', { name: '知识库视图' })).toHaveClass(
|
|
'page-tabs'
|
|
)
|
|
expect(screen.getByLabelText('搜索文档').closest('label')).toHaveClass(
|
|
'knowledge-documents__search'
|
|
)
|
|
expect(screen.getByLabelText('搜索文档')).not.toHaveStyle({
|
|
outline: 'none'
|
|
})
|
|
expect(screen.getByText('本地文件 · 架构说明.md')).toBeInTheDocument()
|
|
expect(screen.queryByText('D:\\Private\\架构说明.md')).not
|
|
.toBeInTheDocument()
|
|
expect(screen.queryByTitle('D:\\Private\\架构说明.md')).not
|
|
.toBeInTheDocument()
|
|
expect(screen.queryByTitle('D:\\Private\\产品手册')).not.toBeInTheDocument()
|
|
const syncSource = screen.getByRole('button', {
|
|
name: '同步 产品手册'
|
|
})
|
|
const removeSource = screen.getByRole('button', {
|
|
name: '移除来源 产品手册'
|
|
})
|
|
expect(syncSource.parentElement).toBe(removeSource.parentElement)
|
|
expect(syncSource.parentElement).toHaveClass(
|
|
'knowledge-source-row__actions'
|
|
)
|
|
expect(removeSource).not.toHaveStyle({ padding: '8px' })
|
|
|
|
fireEvent.click(screen.getByRole('tab', { name: '知识图谱' }))
|
|
fireEvent.change(screen.getByLabelText('选择图谱实体'), {
|
|
target: { value: 'entity-1' }
|
|
})
|
|
expect(screen.getByLabelText('知识图谱画布').parentElement).toHaveClass(
|
|
'knowledge-graph--with-details'
|
|
)
|
|
expect(screen.getByLabelText('实体详情')).toHaveClass(
|
|
'knowledge-graph__detail'
|
|
)
|
|
})
|
|
|
|
it('manages the G6 graph, zoom, selection, movement, and cleanup', async () => {
|
|
const onMoveNode = vi.fn()
|
|
const { rerender, unmount } = render(
|
|
<KnowledgeWorkspace {...createProps({ onMoveNode })} />
|
|
)
|
|
|
|
fireEvent.click(screen.getByRole('tab', { name: '知识图谱' }))
|
|
const graph = screen.getByLabelText('实体关系图')
|
|
expect(graph).toHaveClass('knowledge-graph__chart')
|
|
expect(g6Mock.Graph).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
container: graph,
|
|
zoomRange: [0.5, 2]
|
|
})
|
|
)
|
|
expect(g6Mock.graph.setOptions).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
data: {
|
|
nodes: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: 'entity-1',
|
|
data: expect.objectContaining({
|
|
entityType: '产品',
|
|
label: 'GoodBuddy'
|
|
})
|
|
})
|
|
]),
|
|
edges: [
|
|
expect.objectContaining({
|
|
id: 'relation-1',
|
|
data: expect.objectContaining({
|
|
label: '使用'
|
|
})
|
|
})
|
|
]
|
|
},
|
|
layout: expect.objectContaining({
|
|
animate: false,
|
|
centerStrength: 0.8,
|
|
linkDistance: 64,
|
|
nodeStrength: -70,
|
|
preventOverlap: true,
|
|
radialRadius: 0,
|
|
radialStrength: 0.04,
|
|
type: 'd3-force'
|
|
}),
|
|
behaviors: expect.arrayContaining([
|
|
'drag-canvas',
|
|
'zoom-canvas',
|
|
'drag-element',
|
|
expect.objectContaining({ type: 'auto-adapt-label' })
|
|
])
|
|
})
|
|
)
|
|
const graphOptions = g6Mock.graph.setOptions.mock.lastCall?.[0]
|
|
expect(graphOptions?.behaviors).not.toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ type: 'hover-activate' })
|
|
])
|
|
)
|
|
expect(graphOptions?.node.state).not.toHaveProperty('inactive')
|
|
expect(graphOptions?.edge.state).not.toHaveProperty('inactive')
|
|
await waitFor(() => expect(g6Mock.graph.render).toHaveBeenCalledTimes(1))
|
|
expect(g6Mock.graph.setElementState).toHaveBeenCalledWith(
|
|
{
|
|
'entity-1': [],
|
|
'entity-2': []
|
|
},
|
|
false
|
|
)
|
|
expect(g6Mock.graph.fitView).toHaveBeenCalledWith(
|
|
{
|
|
when: 'always',
|
|
direction: 'both'
|
|
},
|
|
false
|
|
)
|
|
const stableRenderCallCount = g6Mock.graph.render.mock.calls.length
|
|
rerender(<KnowledgeWorkspace {...createProps({ onMoveNode })} />)
|
|
expect(g6Mock.graph.render).toHaveBeenCalledTimes(stableRenderCallCount)
|
|
const movedGraphNodes = createProps().graphNodes.map((node) =>
|
|
node.id === 'entity-1' ? { ...node, x: 240, y: 320 } : node
|
|
)
|
|
rerender(
|
|
<KnowledgeWorkspace
|
|
{...createProps({ graphNodes: movedGraphNodes, onMoveNode })}
|
|
/>
|
|
)
|
|
expect(g6Mock.graph.render).toHaveBeenCalledTimes(stableRenderCallCount)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '放大图谱' }))
|
|
expect(screen.getByText('115%')).toBeInTheDocument()
|
|
expect(g6Mock.graph.zoomTo).toHaveBeenLastCalledWith(1.15, false)
|
|
|
|
act(() => {
|
|
g6Mock.handlers.get('node:click')?.({
|
|
target: { id: 'entity-1' },
|
|
targetType: 'node'
|
|
})
|
|
})
|
|
expect(screen.getByLabelText('实体详情')).toBeInTheDocument()
|
|
await waitFor(() =>
|
|
expect(g6Mock.graph.setElementState).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
'entity-1': ['selected'],
|
|
'entity-2': []
|
|
}),
|
|
false
|
|
)
|
|
)
|
|
expect(onMoveNode).not.toHaveBeenCalled()
|
|
|
|
act(() => {
|
|
g6Mock.handlers.get('node:dragend')?.({
|
|
target: { id: 'entity-1' },
|
|
targetType: 'node'
|
|
})
|
|
})
|
|
expect(onMoveNode).toHaveBeenCalledWith('entity-1', {
|
|
x: 240,
|
|
y: 320
|
|
})
|
|
|
|
fireEvent.click(
|
|
screen.getByRole('button', { name: 'Electron' })
|
|
)
|
|
expect(
|
|
screen.getByRole('heading', { name: 'Electron' })
|
|
).toBeInTheDocument()
|
|
|
|
unmount()
|
|
expect(g6Mock.graph.off).toHaveBeenCalledWith(
|
|
'node:click',
|
|
expect.any(Function)
|
|
)
|
|
expect(g6Mock.graph.off).toHaveBeenCalledWith(
|
|
'node:dragend',
|
|
expect.any(Function)
|
|
)
|
|
expect(g6Mock.graph.off).toHaveBeenCalledWith(
|
|
'aftertransform',
|
|
expect.any(Function)
|
|
)
|
|
expect(g6Mock.graph.destroy).toHaveBeenCalled()
|
|
})
|
|
|
|
it('preserves the G6 instance and refreshes theme colors', async () => {
|
|
render(<KnowledgeWorkspace {...createProps()} />)
|
|
fireEvent.click(screen.getByRole('tab', { name: '知识图谱' }))
|
|
|
|
g6Mock.graph.getZoom.mockReturnValueOnce(1.3)
|
|
act(() => {
|
|
g6Mock.handlers.get('aftertransform')?.({})
|
|
})
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByText('130%')).toBeInTheDocument()
|
|
)
|
|
const renderCalls = g6Mock.graph.render.mock.calls.length
|
|
fireEvent.change(screen.getByLabelText('搜索图谱实体'), {
|
|
target: { value: 'Electron' }
|
|
})
|
|
await waitFor(() =>
|
|
expect(g6Mock.graph.render.mock.calls.length).toBeGreaterThan(
|
|
renderCalls
|
|
)
|
|
)
|
|
expect(g6Mock.Graph).toHaveBeenCalledTimes(1)
|
|
|
|
const themeRenderCalls = g6Mock.graph.render.mock.calls.length
|
|
act(() => {
|
|
document.documentElement.dataset.theme = 'dark'
|
|
})
|
|
await waitFor(() =>
|
|
expect(g6Mock.graph.render.mock.calls.length).toBeGreaterThan(
|
|
themeRenderCalls
|
|
)
|
|
)
|
|
delete document.documentElement.dataset.theme
|
|
})
|
|
|
|
it('sizes dense nodes by degree and labels key entities', () => {
|
|
const graphNodes = Array.from({ length: 30 }, (_, index) => ({
|
|
id: `entity-${index}`,
|
|
label: `实体 ${index}`,
|
|
type: '概念',
|
|
x: index * 10,
|
|
y: index * 5
|
|
}))
|
|
render(
|
|
<KnowledgeWorkspace
|
|
{...createProps({
|
|
graphNodes,
|
|
graphRelations: [
|
|
{
|
|
id: 'relation-dense-1',
|
|
sourceId: 'entity-0',
|
|
targetId: 'entity-1',
|
|
type: '关联'
|
|
},
|
|
{
|
|
id: 'relation-dense-2',
|
|
sourceId: 'entity-0',
|
|
targetId: 'entity-2',
|
|
type: '关联'
|
|
}
|
|
]
|
|
})}
|
|
/>
|
|
)
|
|
fireEvent.click(screen.getByRole('tab', { name: '知识图谱' }))
|
|
|
|
expect(g6Mock.graph.setOptions).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({
|
|
nodes: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: 'entity-0',
|
|
data: expect.objectContaining({
|
|
degree: 2,
|
|
size: 32
|
|
}),
|
|
style: expect.objectContaining({
|
|
x: 0,
|
|
y: 0
|
|
})
|
|
}),
|
|
expect.objectContaining({
|
|
id: 'entity-29',
|
|
data: expect.objectContaining({
|
|
degree: 0,
|
|
size: 16
|
|
})
|
|
})
|
|
])
|
|
}),
|
|
layout: expect.objectContaining({
|
|
animate: false,
|
|
linkDistance: 44,
|
|
nodeSpacing: 10,
|
|
nodeStrength: -45,
|
|
preventOverlap: true,
|
|
radialRadius: 0,
|
|
radialStrength: 0.055,
|
|
type: 'd3-force'
|
|
}),
|
|
behaviors: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
sortNode: { type: 'degree' },
|
|
type: 'auto-adapt-label'
|
|
})
|
|
])
|
|
})
|
|
)
|
|
})
|
|
|
|
it('creates relationships, merges entities, and opens graph evidence', async () => {
|
|
const onCreateRelation = vi.fn()
|
|
const onMergeEntities = vi.fn()
|
|
const onOpenEvidence = vi.fn()
|
|
render(
|
|
<KnowledgeWorkspace
|
|
{...createProps({
|
|
onCreateRelation,
|
|
onMergeEntities,
|
|
onOpenEvidence
|
|
})}
|
|
/>
|
|
)
|
|
|
|
fireEvent.click(screen.getByRole('tab', { name: '知识图谱' }))
|
|
fireEvent.change(screen.getByLabelText('选择图谱实体'), {
|
|
target: { value: 'entity-1' }
|
|
})
|
|
fireEvent.click(
|
|
screen.getByRole('button', { name: /架构说明\.md/u })
|
|
)
|
|
expect(onOpenEvidence).toHaveBeenCalledWith(
|
|
expect.objectContaining({ id: 'evidence-1' })
|
|
)
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '新增' }))
|
|
fireEvent.change(screen.getByLabelText('关系类型'), {
|
|
target: { value: '依赖' }
|
|
})
|
|
fireEvent.change(screen.getByLabelText('说明'), {
|
|
target: { value: '桌面运行基础' }
|
|
})
|
|
fireEvent.click(screen.getByRole('button', { name: '新增关系' }))
|
|
await waitFor(() =>
|
|
expect(onCreateRelation).toHaveBeenCalledWith({
|
|
sourceId: 'entity-1',
|
|
targetId: 'entity-2',
|
|
type: '依赖',
|
|
description: '桌面运行基础'
|
|
})
|
|
)
|
|
|
|
fireEvent.change(screen.getByLabelText('选择合并目标'), {
|
|
target: { value: 'entity-2' }
|
|
})
|
|
fireEvent.click(screen.getByRole('button', { name: '合并到目标实体' }))
|
|
expect(onMergeEntities).toHaveBeenCalledWith('entity-1', 'entity-2')
|
|
})
|
|
|
|
it('renders an explicit empty graph state', () => {
|
|
render(
|
|
<KnowledgeWorkspace
|
|
{...createProps({ graphNodes: [], graphRelations: [] })}
|
|
/>
|
|
)
|
|
|
|
fireEvent.click(screen.getByRole('tab', { name: '知识图谱' }))
|
|
expect(
|
|
screen.getByText('当前知识库尚未生成实体关系。')
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it('keeps loading distinct from the first-library empty state', () => {
|
|
render(
|
|
<KnowledgeWorkspace
|
|
{...createProps({
|
|
libraries: [],
|
|
loading: true,
|
|
selectedLibraryId: undefined
|
|
})}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('正在加载知识库')).toBeInTheDocument()
|
|
expect(
|
|
screen.queryByText('建立第一个知识库')
|
|
).not.toBeInTheDocument()
|
|
expect(
|
|
screen.getByRole('button', { name: '新建知识库' })
|
|
).toBeDisabled()
|
|
})
|
|
|
|
it('shows a retryable load error instead of the first-library empty state', () => {
|
|
const onRetryLoad = vi.fn()
|
|
render(
|
|
<KnowledgeWorkspace
|
|
{...createProps({
|
|
libraries: [],
|
|
loadError: '数据库暂时不可用',
|
|
onRetryLoad,
|
|
selectedLibraryId: undefined
|
|
})}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('知识库加载失败')).toBeInTheDocument()
|
|
expect(screen.getByText('数据库暂时不可用')).toBeInTheDocument()
|
|
expect(
|
|
screen.queryByText('建立第一个知识库')
|
|
).not.toBeInTheDocument()
|
|
fireEvent.click(screen.getByRole('button', { name: '重试' }))
|
|
expect(onRetryLoad).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('keeps existing data and selection visible when refresh fails', () => {
|
|
render(
|
|
<KnowledgeWorkspace
|
|
{...createProps({ loadError: '刷新连接失败' })}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('知识库刷新失败')).toBeInTheDocument()
|
|
expect(screen.getByRole('heading', { name: library.name }))
|
|
.toBeInTheDocument()
|
|
expect(screen.getByText('架构说明.md')).toBeInTheDocument()
|
|
expect(
|
|
screen.getByRole('button', { name: /^产品知识 1 个文档/u })
|
|
).toHaveAttribute('aria-current', 'page')
|
|
})
|
|
|
|
it('confirms that deleting a managed library removes managed copies', async () => {
|
|
const onDeleteLibrary = vi.fn()
|
|
render(
|
|
<KnowledgeWorkspace
|
|
{...createProps({ onDeleteLibrary })}
|
|
/>
|
|
)
|
|
|
|
const trigger = screen.getByRole('button', {
|
|
name: '删除知识库 产品知识'
|
|
})
|
|
fireEvent.click(trigger)
|
|
const dialog = screen.getByRole('dialog', {
|
|
name: '删除知识库确认'
|
|
})
|
|
expect(screen.getByRole('button', { name: '取消' })).toHaveFocus()
|
|
fireEvent.keyDown(dialog, { key: 'Escape' })
|
|
await waitFor(() => expect(trigger).toHaveFocus())
|
|
fireEvent.click(trigger)
|
|
expect(
|
|
screen.getByText(
|
|
'此知识库使用托管存储。删除后,应用保存的托管副本、索引和图谱都会被永久删除。'
|
|
)
|
|
).toBeInTheDocument()
|
|
fireEvent.click(screen.getByRole('button', { name: '确认删除' }))
|
|
|
|
await waitFor(() =>
|
|
expect(onDeleteLibrary).toHaveBeenCalledWith('library-1')
|
|
)
|
|
})
|
|
|
|
it('explains that reference library deletion preserves original files', () => {
|
|
render(
|
|
<KnowledgeWorkspace
|
|
{...createProps({
|
|
libraries: [
|
|
{
|
|
...library,
|
|
storageMode: 'reference'
|
|
}
|
|
]
|
|
})}
|
|
/>
|
|
)
|
|
|
|
fireEvent.click(
|
|
screen.getByRole('button', { name: '删除知识库 产品知识' })
|
|
)
|
|
expect(
|
|
screen.getByText(
|
|
'此知识库引用原文件。删除后只会移除索引和图谱,不会删除磁盘上的原文件。'
|
|
)
|
|
).toBeInTheDocument()
|
|
})
|
|
})
|