test(ui): 移除过度测试文件

删除 context-editor-sync.spec.ts 和 context-editor-refactor.spec.ts,
这些测试存在以下问题:
- Mock 过度复杂,维护成本高
- 测试的是 mock 配置而非真实组件行为
- 本地与 CI 环境因模块缓存差异导致结果不一致
This commit is contained in:
linshen committed 2025-11-23 21:45:41 +08:00
1 parent e0319b7ef9
commit d8018dcc68
2 files changed
-1747

No files matched your search

@@ -1,951 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { mount, VueWrapper } from '@vue/test-utils'
import { nextTick } from 'vue'
// 组件导入
import ConversationManager from '../../src/components/context-mode/ConversationManager.vue'
import ContextEditor from '../../src/components/context-mode/ContextEditor.vue'
import ImportExportDialog from '../../src/components/context-mode/ImportExportDialog.vue'
import VariableManagerModal from '../../src/components/variable/VariableManagerModal.vue'
// Mock Naive UI 组件 - 简化版本用于E2E测试
vi.mock('naive-ui', () => ({
NCard: {
name: 'NCard',
template: `<div class="n-card" data-testid="card"><div class="n-card__header"><slot name="header" /></div><div class="n-card__content"><slot /></div></div>`,
props: ['size', 'bordered', 'embedded', 'hoverable', 'dashed']
},
NButton: {
name: 'NButton',
template: '<button class="n-button" :disabled="disabled" :loading="loading" @click="$emit(\'click\')" data-testid="button"><slot name="icon" /><slot /></button>',
props: ['type', 'disabled', 'loading', 'size', 'dashed', 'block', 'quaternary', 'circle', 'secondary', 'text'],
emits: ['click']
},
NSpace: {
name: 'NSpace',
template: '<div class="n-space"><slot /></div>',
props: ['justify', 'align', 'size', 'wrap']
},
NTag: {
name: 'NTag',
template: '<span class="n-tag"><slot name="icon" /><slot /></span>',
props: ['size', 'type', 'round']
},
NEmpty: {
name: 'NEmpty',
template: '<div class="n-empty" data-testid="empty"><slot name="icon" /><div><slot /></div><slot name="extra" /></div>',
props: ['description', 'size']
},
NScrollbar: {
name: 'NScrollbar',
template: '<div class="n-scrollbar"><slot /></div>',
props: ['style']
},
NList: {
name: 'NList',
template: '<div class="n-list"><slot /></div>'
},
NListItem: {
name: 'NListItem',
template: '<div class="n-list-item"><slot /></div>'
},
NInput: {
name: 'NInput',
template: '<textarea v-if="type === \'textarea\'" class="n-input" :value="value" :placeholder="placeholder" :disabled="disabled" @input="$emit(\'update:value\', $event.target.value)" data-testid="message-input"></textarea>',
props: ['value', 'type', 'placeholder', 'autosize', 'size', 'disabled', 'readonly'],
emits: ['update:value']
},
NDropdown: {
name: 'NDropdown',
template: '<div class="n-dropdown" @click="$emit(\'select\', \'user\')"><slot /></div>',
props: ['options'],
emits: ['select']
},
NText: {
name: 'NText',
template: '<span class="n-text"><slot /></span>',
props: ['depth', 'type', 'size']
},
NModal: {
name: 'NModal',
template: `<div v-if="show" class="n-modal" data-testid="modal"><div class="n-card"><div class="n-card__header"><slot name="header" />{{ title }}<slot name="header-extra" /></div><div class="n-card__content"><slot /></div><div class="n-card__footer"><slot name="action" /></div></div></div>`,
props: ['show', 'preset', 'title', 'style', 'size', 'bordered', 'segmented', 'maskClosable'],
emits: ['update:show', 'afterEnter', 'afterLeave']
},
NTabs: {
name: 'NTabs',
template: '<div class="n-tabs"><slot /></div>',
props: ['value', 'type', 'size'],
emits: ['update:value']
},
NTabPane: {
name: 'NTabPane',
template: '<div class="n-tab-pane" v-if="$parent.value === name || !$parent.value"><slot /></div>',
props: ['name', 'tab']
},
NSelect: {
name: 'NSelect',
template: '<select class="n-select" :value="value" @change="$emit(\'update:value\', $event.target.value)"><option v-for="opt in options" :key="opt.value" :value="opt.value">{{opt.label}}</option></select>',
props: ['value', 'options', 'size', 'disabled'],
emits: ['update:value']
},
NGrid: {
name: 'NGrid',
template: '<div class="n-grid"><slot /></div>',
props: ['cols', 'xGap', 'yGap']
},
NGridItem: {
name: 'NGridItem',
template: '<div class="n-grid-item"><slot /></div>'
},
NFormItem: {
name: 'NFormItem',
template: '<div class="n-form-item"><label>{{ label }}</label><slot /></div>',
props: ['label', 'labelPlacement']
}
}))
// Mock vue-i18n
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key: string, params?: any) => {
const translations = {
'conversation.management.title': '会话管理',
'conversation.noMessages': '暂无消息',
'conversation.addFirst': '添加第一条消息',
'conversation.addMessage': '添加消息',
'conversation.messageCount': '消息数: {count}',
'conversation.stats.messages': '消息',
'conversation.stats.variables': '变量',
'conversation.stats.missing': '缺失',
'conversation.stats.tools': '工具',
'conversation.roles.system': '系统',
'conversation.roles.user': '用户',
'conversation.roles.assistant': '助手',
'conversation.placeholders.system': '输入系统提示...',
'conversation.placeholders.user': '输入用户消息...',
'conversation.placeholders.assistant': '输入助手回复...',
'conversation.management.openEditor': '打开编辑器',
'variables.count': '变量: {count}',
'variables.missing': '缺失: {count}',
'variables.management.title': '变量管理',
'common.expand': '展开',
'common.collapse': '折叠',
'common.moveUp': '上移',
'common.moveDown': '下移',
'common.delete': '删除',
'common.save': '保存',
'common.cancel': '取消',
'common.import': '导入',
'common.export': '导出',
'contextEditor.addFirstMessage': '添加第一条消息',
'contextEditor.addMessage': '添加消息',
'contextEditor.templateApplied': '已应用模板:{name}',
'contextEditor.applyTemplate': '应用模板',
'contextEditor.importFormats.smart.name': '智能识别',
'contextEditor.importFormats.smart.description': '自动检测格式并转换',
'contextEditor.importFormats.conversation.name': '会话格式',
'contextEditor.importFormats.conversation.description': '标准的会话消息格式',
'contextEditor.importFormats.openai.name': 'OpenAI',
'contextEditor.importFormats.openai.description': 'OpenAI API 请求格式',
'contextEditor.importFormats.langfuse.name': 'LangFuse',
'contextEditor.importFormats.langfuse.description': 'LangFuse 追踪数据格式'
}
if (params) {
return translations[key]?.replace('{count}', params.count)?.replace('{name}', params.name) || `${key}:${JSON.stringify(params)}`
}
return translations[key] || key
},
locale: { value: 'zh-CN' }
})
}))
// Mock composables
vi.mock('../../src/composables/useResponsive', () => ({
useResponsive: () => ({
modalWidth: { value: '90vw' },
buttonSize: { value: 'medium' },
inputSize: { value: 'medium' },
shouldUseVerticalLayout: { value: false },
isMobile: { value: false },
isTablet: { value: false },
isDesktop: { value: true },
screenSize: { value: 'large' },
shouldUseCompactMode: { value: false },
responsiveButtonSize: { value: 'medium' },
responsiveSpacing: { value: 'normal' }
})
}))
vi.mock('../../src/composables/usePerformanceMonitor', () => ({
usePerformanceMonitor: () => ({
recordUpdate: vi.fn()
})
}))
vi.mock('../../src/composables/useDebounceThrottle', () => ({
useDebounceThrottle: () => ({
debounce: (fn: Function, delay: number) => fn,
throttle: (fn: Function, delay: number) => fn,
batchExecute: (fn: Function, delay: number) => {
return (updates: any) => {
if (Array.isArray(updates)) {
updates.forEach(update => update())
} else if (typeof updates === 'function') {
updates()
}
}
}
})
}))
vi.mock('../../src/composables/useAccessibility', () => ({
useAccessibility: () => ({
aria: {
getLabel: (key: string, fallback?: string) => fallback || key,
getDescription: (key: string) => key,
getLiveRegionText: (key: string) => key
},
announce: vi.fn(),
accessibilityClasses: { value: {} },
isAccessibilityMode: { value: false },
liveRegionMessage: { value: '' },
announcements: { value: [] }
})
}))
const mockConverter = {
toOpenAI: vi.fn().mockImplementation((data) => ({
success: true,
data: {
model: data.model || 'gpt-test',
messages: data.messages,
tools: data.tools
}
}))
}
vi.mock('../../src/composables/context/useContextEditor', () => ({
useContextEditor: () => ({
currentData: { value: null },
isLoading: { value: false },
smartImport: vi.fn(),
convertFromOpenAI: vi.fn(),
convertFromLangFuse: vi.fn(),
importFromFile: vi.fn(),
exportToFile: vi.fn().mockReturnValue(true),
exportToClipboard: vi.fn().mockResolvedValue(true),
setData: vi.fn(),
services: {
converter: mockConverter
}
})
}))
// 创建 variableManager mock
const createMockVariableManager = () => ({
variableManager: { value: null },
isReady: { value: true },
isAdvancedMode: { value: false },
customVariables: { value: {} },
allVariables: { value: {} },
statistics: { value: {
customVariableCount: 0,
predefinedVariableCount: 7,
totalVariableCount: 7,
advancedModeEnabled: false
}},
setAdvancedMode: vi.fn(),
addVariable: vi.fn(),
updateVariable: vi.fn(),
deleteVariable: vi.fn(),
getVariable: vi.fn((name: string) => undefined),
validateVariableName: vi.fn(() => true),
scanVariablesInContent: vi.fn(() => []),
replaceVariables: vi.fn((content: string) => content),
detectMissingVariables: vi.fn(() => []),
getConversationMessages: vi.fn(() => []),
setConversationMessages: vi.fn(),
exportVariables: vi.fn(() => '{}'),
importVariables: vi.fn(),
refresh: vi.fn()
})
describe('完整用户流程E2E测试', () => {
let conversationWrapper: VueWrapper<any>
let contextEditorWrapper: VueWrapper<any>
let variableManagerWrapper: VueWrapper<any>
// 测试用数据
const testMessages = [
{ role: 'system', content: '你是一个助手,使用变量 {{assistantType}}' },
{ role: 'user', content: '请帮我 {{userRequest}},我的偏好是 {{userPreference}}' }
]
const testVariables = {
assistantType: 'AI助手',
userRequest: '写代码'
}
const scanVariables = vi.fn((content: string) => {
if (!content || typeof content !== 'string') return []
const matches = content.match(/\{\{([^}]+)\}\}/g) || []
return matches.map(match => match.slice(2, -2))
})
const replaceVariables = vi.fn((content: string, variables: Record<string, string> = {}) => {
if (!content || typeof content !== 'string') return content
return content.replace(/\{\{([^}]+)\}\}/g, (match, key) => variables[key] || match)
})
beforeEach(() => {
conversationWrapper?.unmount()
contextEditorWrapper?.unmount()
variableManagerWrapper?.unmount()
vi.clearAllMocks()
scanVariables.mockImplementation((content: string) => {
if (!content || typeof content !== 'string') return []
const matches = content.match(/\{\{([^}]+)\}\}/g) || []
return matches.map(match => match.slice(2, -2))
})
replaceVariables.mockImplementation((content: string, variables: Record<string, string> = {}) => {
if (!content || typeof content !== 'string') return content
return content.replace(/\{\{([^}]+)\}\}/g, (match, key) => variables[key] || match)
})
})
describe('1. 轻量管理到深度编辑的完整流程', () => {
it('应该支持从轻量ConversationManager过渡到深度ContextEditor', async () => {
// 步骤1:初始化ConversationManager(轻量管理)
conversationWrapper = mount(ConversationManager, {
props: {
messages: testMessages,
availableVariables: testVariables,
scanVariables,
replaceVariables,
isPredefinedVariable: () => false
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
// 验证轻量管理界面
expect(conversationWrapper.exists()).toBe(true)
expect(conversationWrapper.text()).toContain('会话管理')
expect(conversationWrapper.text()).toContain('消息: 2')
// 步骤2:检测到缺失变量并显示统计
expect(conversationWrapper.text()).toContain('变量: 3') // assistantType, userRequest, userPreference
expect(conversationWrapper.text()).toContain('缺失: 1') // userPreference
// 步骤3:点击"打开编辑器"按钮进入深度编辑
const openEditorButton = conversationWrapper.find('[data-testid="button"]')
expect(openEditorButton.exists()).toBe(true)
// 模拟点击 - 直接验证方法调用而不是UI交互
const handleOpenContextEditor = conversationWrapper.vm.handleOpenContextEditor
expect(handleOpenContextEditor).toBeDefined()
// 手动调用方法以验证功能
await handleOpenContextEditor()
// 验证事件发射(如果有的话)
const emittedEvents = conversationWrapper.emitted('openContextEditor')
if (emittedEvents) {
const emittedData = emittedEvents[0]
expect(emittedData[0]).toEqual(testMessages) // messages
expect(emittedData[1]).toEqual(testVariables) // availableVariables
}
// 步骤4:初始化ContextEditor(深度编辑)
contextEditorWrapper = mount(ContextEditor, {
props: {
visible: true,
state: {
messages: testMessages,
variables: testVariables,
tools: [],
showVariablePreview: true,
showToolManager: false,
mode: 'edit'
},
scanVariables,
replaceVariables,
isPredefinedVariable: () => false,
variableManager: createMockVariableManager()
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
// 验证组件正确初始化(简化验证)
expect(contextEditorWrapper.exists()).toBe(true)
expect(contextEditorWrapper.find('[data-testid="modal"]').exists()).toBe(true)
// 验证核心功能存在而不依赖具体数据结构
expect(contextEditorWrapper.vm).toBeTruthy()
expect(typeof contextEditorWrapper.vm === 'object').toBe(true)
})
})
describe('2. 模板选择和应用的用户体验', () => {
it('应该支持模板预览和应用流程', async () => {
// 初始化ContextEditor
contextEditorWrapper = mount(ContextEditor, {
props: {
visible: true,
state: {
messages: [],
variables: {},
tools: [],
showVariablePreview: true,
showToolManager: false,
mode: 'edit'
},
optimizationMode: 'system',
scanVariables,
replaceVariables,
isPredefinedVariable: () => false,
variableManager: createMockVariableManager()
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
// 验证模板管理界面可用
expect(contextEditorWrapper.exists()).toBe(true)
// 验证模板管理相关的方法存在
expect(contextEditorWrapper.vm.handleTemplateApply).toBeDefined()
// 验证模板应用功能
expect(contextEditorWrapper.vm.handleTemplateApply).toBeDefined()
// 模拟应用模板
const testTemplate = {
id: 'test-template',
name: '测试模板',
messages: [
{ role: 'system', content: '系统消息 {{var1}}' },
{ role: 'user', content: '用户消息 {{var2}}' }
]
}
await contextEditorWrapper.vm.handleTemplateApply(testTemplate)
await nextTick()
// 验证模板应用后的状态变化
expect(contextEditorWrapper.emitted('update:state')).toBeTruthy()
const newState = contextEditorWrapper.emitted('update:state')[0][0]
expect(newState.messages).toEqual(testTemplate.messages)
})
it('应该正确处理模板中的变量检测', async () => {
contextEditorWrapper = mount(ContextEditor, {
props: {
visible: true,
state: {
messages: [
{ role: 'system', content: '模板消息包含 {{templateVar}} 和 {{anotherVar}}' }
],
variables: { templateVar: 'value1' },
tools: [],
showVariablePreview: true,
showToolManager: false,
mode: 'edit'
},
scanVariables,
replaceVariables,
isPredefinedVariable: () => false,
variableManager: createMockVariableManager()
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
// 验证变量检测功能存在
expect(scanVariables).toBeDefined()
expect(replaceVariables).toBeDefined()
// 测试变量检测
const testContent = '模板消息包含 {{templateVar}} 和 {{anotherVar}}'
const detectedVars = scanVariables(testContent)
const availableVars = { templateVar: 'value1' }
const missingVars = detectedVars.filter(v => !availableVars[v])
expect(detectedVars).toEqual(['templateVar', 'anotherVar'])
expect(missingVars).toEqual(['anotherVar'])
// 验证组件存在
expect(contextEditorWrapper.exists()).toBe(true)
})
})
describe('3. 导入导出和格式转换功能', () => {
it('应该支持多种格式的导入功能', async () => {
const dialogWrapper = mount(ImportExportDialog, {
props: {
visible: true,
mode: 'import',
messages: [],
tools: []
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
const formats = dialogWrapper.vm.importFormats
expect(formats.map(f => f.id)).toEqual(['smart', 'openai', 'langfuse', 'conversation'])
dialogWrapper.vm.importData = JSON.stringify({
messages: [
{ role: 'system', content: '系统消息' },
{ role: 'user', content: '用户消息' }
],
tools: []
})
dialogWrapper.vm.selectedImportFormat = 'conversation'
await dialogWrapper.vm.handleImportSubmit()
expect(dialogWrapper.emitted('import-success')).toBeTruthy()
})
it('应该支持多种格式的导出功能', async () => {
const dialogWrapper = mount(ImportExportDialog, {
props: {
visible: true,
mode: 'export',
messages: [{ role: 'user', content: '测试消息' }],
tools: []
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
const formats = dialogWrapper.vm.exportFormats
expect(formats.map(f => f.id)).toEqual(['standard', 'openai'])
await dialogWrapper.vm.handleExportToFile()
expect(dialogWrapper.emitted('export-success')).toBeTruthy()
})
})
describe('4. 变量管理的跨组件协作', () => {
it('应该为变量管理器提供缺失变量信息以便触发创建流程', async () => {
// 步骤1:初始化ConversationManager,包含缺失变量
conversationWrapper = mount(ConversationManager, {
props: {
messages: [
{ role: 'user', content: '消息包含 {{existingVar}} 和 {{missingVar}}' }
],
availableVariables: { existingVar: 'value1' },
scanVariables,
replaceVariables,
isPredefinedVariable: () => false
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
// 验证缺失变量检测
const testContent = '消息包含 {{existingVar}} 和 {{missingVar}}'
const detectedVars = scanVariables(testContent)
const availableVars = { existingVar: 'value1' }
const missingVars = detectedVars.filter(v => !availableVars[v])
expect(detectedVars).toEqual(['existingVar', 'missingVar'])
expect(missingVars).toEqual(['missingVar'])
// 步骤2:获取缺失变量并模拟父层触发变量管理器
const missingVariablesFromComponent = conversationWrapper.vm.allMissingVariables
expect(missingVariablesFromComponent).toEqual(['missingVar'])
const variableToCreate = missingVariablesFromComponent[0]
expect(variableToCreate).toBe('missingVar')
// 步骤3:初始化VariableManagerModal
variableManagerWrapper = mount(VariableManagerModal, {
props: {
visible: true,
variables: { existingVar: 'value1' },
variableManager: {
// 提供完整的variableManager接口mock
createVariable: vi.fn().mockResolvedValue(true),
updateVariable: vi.fn().mockResolvedValue(true),
deleteVariable: vi.fn().mockResolvedValue(true),
getVariable: vi.fn().mockReturnValue('mockValue'),
exportVariables: vi.fn().mockResolvedValue('{}'),
importVariables: vi.fn().mockResolvedValue(true),
variables: { existingVar: 'value1' },
loading: false
}
},
global: {
stubs: {
'NModal': true,
'NSpace': true,
'NButton': true,
'NIcon': true,
'NCard': true,
'NDataTable': true,
'NInput': true,
'NSelect': true,
'NDynamicInput': true,
'NPopconfirm': true
},
mocks: {
$t: (key: string) => key,
announcements: []
},
plugins: [
// 模拟i18n
{
install(app: any) {
app.config.globalProperties.$t = (key: string) => key
app.provide('i18n', {
global: {
t: (key: string) => key
}
})
}
}
]
}
})
await nextTick()
// 验证变量管理器正常初始化(简化验证)
expect(variableManagerWrapper.exists()).toBe(true)
expect(variableManagerWrapper.vm).toBeTruthy()
// 验证组件功能而不依赖特定UI结构
expect(typeof variableManagerWrapper.vm === 'object').toBe(true)
})
it('应该支持ContextEditor中的变量管理集成', async () => {
contextEditorWrapper = mount(ContextEditor, {
props: {
visible: true,
state: {
messages: [
{ role: 'user', content: '编辑器消息 {{contextVar}} {{newVar}}' }
],
variables: { contextVar: 'contextValue' },
tools: [],
showVariablePreview: true,
showToolManager: false,
mode: 'edit'
},
scanVariables,
replaceVariables,
isPredefinedVariable: () => false,
variableManager: createMockVariableManager()
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
// 验证变量检测功能
const testContent = '编辑器消息 {{contextVar}} {{newVar}}'
const detectedVars = scanVariables(testContent)
const availableVars = { contextVar: 'contextValue' }
const missingVars = detectedVars.filter(v => !availableVars[v])
expect(detectedVars).toEqual(['contextVar', 'newVar'])
expect(missingVars).toEqual(['newVar'])
// 测试变量创建功能
expect(contextEditorWrapper.vm.handleCreateVariableAndOpenManager).toBeDefined()
// 验证组件功能
expect(contextEditorWrapper.exists()).toBe(true)
})
it('应该保持跨组件变量状态同步', async () => {
// 初始变量状态
const sharedVariables = { var1: 'value1', var2: 'value2' }
// ConversationManager
conversationWrapper = mount(ConversationManager, {
props: {
messages: [{ role: 'user', content: '测试 {{var1}} {{var3}}' }],
availableVariables: sharedVariables,
scanVariables,
replaceVariables,
isPredefinedVariable: () => false
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
// ContextEditor
contextEditorWrapper = mount(ContextEditor, {
props: {
visible: true,
state: {
messages: [{ role: 'system', content: '编辑器 {{var2}} {{var4}}' }],
variables: sharedVariables,
tools: [],
showVariablePreview: true,
showToolManager: false,
mode: 'edit'
},
scanVariables,
replaceVariables,
isPredefinedVariable: () => false,
variableManager: createMockVariableManager()
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
// 验证两个组件都能正确处理相同的变量状态
const conversationContent = '测试 {{var1}} {{var3}}'
const contextContent = '编辑器 {{var2}} {{var4}}'
const conversationVars = scanVariables(conversationContent)
const contextVars = scanVariables(contextContent)
const conversationMissing = conversationVars.filter(v => !sharedVariables[v])
const contextMissing = contextVars.filter(v => !sharedVariables[v])
expect(conversationMissing).toEqual(['var3']) // var1存在
expect(contextMissing).toEqual(['var4']) // var2存在
// 验证变量统计计算
expect(conversationWrapper.vm.allUsedVariables).toContain('var1')
expect(conversationWrapper.vm.allUsedVariables).toContain('var3')
expect(conversationWrapper.vm.allMissingVariables).toEqual(['var3'])
})
})
describe('5. 综合用户体验流程测试', () => {
it('应该支持完整的从创建到导出的工作流程', async () => {
// 步骤1:从空状态开始
conversationWrapper = mount(ConversationManager, {
props: {
messages: [],
availableVariables: {},
scanVariables,
replaceVariables,
isPredefinedVariable: () => false
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
// 验证空状态显示
expect(conversationWrapper.find('[data-testid="empty"]').exists()).toBe(true)
// 简化文本验证,只检查组件存在
expect(conversationWrapper.exists()).toBe(true)
// 步骤2:添加第一条消息
expect(conversationWrapper.vm.handleAddMessage).toBeDefined()
await conversationWrapper.vm.handleAddMessage()
// 验证消息添加
expect(conversationWrapper.emitted('update:messages')).toBeTruthy()
const addedMessages = conversationWrapper.emitted('update:messages')[0][0]
expect(addedMessages.length).toBe(1)
expect(addedMessages[0]).toMatchObject({ role: 'user', content: '' })
// 验证自动添加的字段
expect(addedMessages[0].id).toBeDefined()
expect(addedMessages[0].originalContent).toBeDefined()
// 步骤3:编辑消息内容
const messageWithVariables = '用户请求 {{userInput}} 处理 {{actionType}}'
vi.useFakeTimers()
await conversationWrapper.vm.handleMessageUpdate(0, { role: 'user', content: messageWithVariables })
vi.runAllTimers()
await nextTick()
vi.useRealTimers()
const updateEvents = conversationWrapper.emitted('update:messages') || []
const latestPayload = updateEvents[updateEvents.length - 1]?.[0]
if (latestPayload) {
await conversationWrapper.setProps({ messages: latestPayload })
await nextTick()
}
// 验证变量检测
const detectedVars = scanVariables(messageWithVariables)
const missingVars = detectedVars.filter(v => !{}.hasOwnProperty(v)) // 空的availableVariables
expect(detectedVars).toEqual(['userInput', 'actionType'])
expect(missingVars).toEqual(['userInput', 'actionType'])
// 步骤4:识别缺失变量并模拟父层响应
const missingVariables = conversationWrapper.vm.allMissingVariables
expect(missingVariables).toEqual(['userInput', 'actionType'])
const targetVariable = missingVariables[0]
expect(targetVariable).toBe('userInput')
// 步骤5:进入深度编辑模式 - 简化验证
const handleOpenContextEditor = conversationWrapper.vm.handleOpenContextEditor
if (handleOpenContextEditor) {
await handleOpenContextEditor()
}
// 验证核心功能存在即可,不强制要求特定事件发射
expect(conversationWrapper.exists()).toBe(true)
// 步骤6:在ContextEditor中完成编辑和导出
contextEditorWrapper = mount(ContextEditor, {
props: {
visible: true,
state: {
messages: [{ role: 'user', content: messageWithVariables }],
variables: { userInput: 'test input', actionType: 'process' },
tools: [],
showVariablePreview: true,
showToolManager: false,
mode: 'edit'
},
scanVariables,
replaceVariables,
isPredefinedVariable: () => false,
variableManager: createMockVariableManager()
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
// 验证最终状态(简化验证)
expect(contextEditorWrapper.exists()).toBe(true)
expect(contextEditorWrapper.vm).toBeTruthy()
// 注意:导出功能已移至 ImportExportDialog 组件
// 验证导入导出对话框可以打开
expect(contextEditorWrapper.vm.handleImport).toBeDefined()
expect(contextEditorWrapper.vm.handleExport).toBeDefined()
})
it('应该正确处理错误和边界情况', async () => {
// 测试空消息处理
conversationWrapper = mount(ConversationManager, {
props: {
messages: [{ role: 'user', content: '' }],
availableVariables: {},
scanVariables,
replaceVariables,
isPredefinedVariable: () => false
},
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
// 验证空内容的变量处理
const emptyVars = scanVariables('')
expect(emptyVars).toEqual([])
// 测试无效变量名处理
const invalidContent = '{{}} {{invalid!@#}}'
const invalidVars = scanVariables(invalidContent)
expect(Array.isArray(invalidVars)).toBe(true)
// 测试大量消息的处理性能
const largeMessageList = Array.from({ length: 50 }, (_, i) => ({
role: i % 2 === 0 ? 'user' : 'assistant',
content: `消息 ${i} 包含 {{var${i}}}`
}))
const performanceStartTime = Date.now()
await conversationWrapper.setProps({
messages: largeMessageList,
availableVariables: {},
scanVariables,
replaceVariables,
isPredefinedVariable: () => false
})
await nextTick()
const performanceEndTime = Date.now()
const renderTime = performanceEndTime - performanceStartTime
// 验证大量数据处理性能(应该在合理范围内)
expect(renderTime).toBeLessThan(1000) // 小于1秒
expect(conversationWrapper.exists()).toBe(true)
})
})
})
@@ -1,796 +0,0 @@
import { describe, it, expect, beforeEach, vi, beforeAll, afterAll } from 'vitest'
import { mount, VueWrapper } from '@vue/test-utils'
import { ref, nextTick, reactive } from 'vue'
import { createApp } from 'vue'
import ConversationManager from '../../src/components/context-mode/ConversationManager.vue'
import ContextEditor from '../../src/components/context-mode/ContextEditor.vue'
// Mock Naive UI 组件
vi.mock('naive-ui', () => ({
NCard: {
name: 'NCard',
template: `<div class="n-card"><div class="n-card__header" v-if="$slots.header"><slot name="header" /></div><div class="n-card__content"><slot /></div></div>`,
props: ['size', 'bordered', 'embedded', 'hoverable', 'dashed']
},
NSpace: {
name: 'NSpace',
template: '<div class="n-space"><slot /></div>',
props: ['justify', 'align', 'size', 'wrap']
},
NText: {
name: 'NText',
template: '<span class="n-text"><slot /></span>',
props: ['class', 'depth']
},
NTag: {
name: 'NTag',
template: '<span class="n-tag"><slot name="icon" /><slot /></span>',
props: ['size', 'type', 'round']
},
NButton: {
name: 'NButton',
template: '<button class="n-button" :disabled="disabled" :loading="loading" @click="$emit(\'click\')" data-testid="button"><slot name="icon" /><slot /></button>',
props: ['type', 'disabled', 'loading', 'size', 'dashed', 'block', 'quaternary', 'circle', 'secondary'],
emits: ['click']
},
NEmpty: {
name: 'NEmpty',
template: '<div class="n-empty" data-testid="empty"><slot name="icon" /><div><slot /></div><slot name="extra" /></div>',
props: ['description', 'size']
},
NScrollbar: {
name: 'NScrollbar',
template: '<div class="n-scrollbar"><slot /></div>',
props: ['style']
},
NList: {
name: 'NList',
template: '<div class="n-list"><slot /></div>'
},
NListItem: {
name: 'NListItem',
template: '<div class="n-list-item"><slot /></div>'
},
NInput: {
name: 'NInput',
template: '<textarea v-if="type === \'textarea\'" class="n-input" :value="value" :placeholder="placeholder" :disabled="disabled" @input="$emit(\'update:value\', $event.target.value)"></textarea>',
props: ['value', 'type', 'placeholder', 'autosize', 'size', 'disabled', 'readonly'],
emits: ['update:value']
},
NDropdown: {
name: 'NDropdown',
template: '<div class="n-dropdown" @click="$emit(\'select\', \'user\')"><slot /></div>',
props: ['options'],
emits: ['select']
},
NModal: {
name: 'NModal',
template: `
<div v-if="show" class="n-modal" data-testid="modal">
<div class="n-card">
<div class="n-card__header">
<slot name="header" />{{ title }}<slot name="header-extra" />
</div>
<div class="n-card__content"><slot /></div>
<div class="n-card__footer"><slot name="action" /></div>
</div>
</div>
`,
props: ['show', 'preset', 'title', 'style', 'size', 'bordered', 'segmented', 'maskClosable'],
emits: ['update:show', 'afterEnter', 'afterLeave']
},
NTabs: {
name: 'NTabs',
template: '<div class="n-tabs"><slot /></div>',
props: ['value', 'type', 'size'],
emits: ['update:value']
},
NTabPane: {
name: 'NTabPane',
template: '<div class="n-tab-pane" v-if="$parent.value === name || !$parent.value"><slot /></div>',
props: ['name', 'tab']
},
NSelect: {
name: 'NSelect',
template: '<select class="n-select" :value="value" @change="$emit(\'update:value\', $event.target.value)"><option v-for="opt in options" :key="opt.value" :value="opt.value">{{opt.label}}</option></select>',
props: ['value', 'options', 'size', 'disabled'],
emits: ['update:value']
},
NGrid: {
name: 'NGrid',
template: '<div class="n-grid"><slot /></div>',
props: ['cols', 'xGap', 'yGap']
},
NGridItem: {
name: 'NGridItem',
template: '<div class="n-grid-item"><slot /></div>'
}
}))
// Mock vue-i18n
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key: string, params?: any) => {
// 基础翻译键处理
const translations: Record<string, any> = {
'conversation.management.title': '对话管理',
'conversation.messageCount': `${params?.count || 0} 条消息`,
'variables.count': `${params?.count || 0} 个变量`,
'variables.missing': `缺失 ${params?.count || 0} 个`,
'tools.count': `${params?.count || 0} 个工具`,
'conversation.noMessages': '暂无消息',
'conversation.addFirst': '添加第一条消息',
'conversation.addMessage': '添加消息',
'common.expand': '展开',
'common.collapse': '折叠',
'conversation.management.openEditor': '打开编辑器',
'common.moveUp': '上移',
'common.moveDown': '下移',
'common.delete': '删除',
'conversation.roles.system': '系统',
'conversation.roles.user': '用户',
'conversation.roles.assistant': '助手',
'conversation.placeholders.system': '输入系统提示...',
'conversation.placeholders.user': '输入用户消息...',
'conversation.placeholders.assistant': '输入助手回复...',
'conversation.clickToCreateVariable': '点击创建变量',
'contextEditor.noMessages': '暂无消息',
'contextEditor.addFirstMessage': '添加第一条消息',
'contextEditor.addMessage': '添加消息',
'common.preview': '预览',
'common.edit': '编辑',
'common.save': '保存',
'common.cancel': '取消',
'common.import': '导入',
'common.export': '导出'
}
return translations[key] || key
},
locale: { value: 'zh-CN' }
})
}))
// Mock composables
vi.mock('../../src/composables/useResponsive', () => ({
useResponsive: () => ({
modalWidth: { value: '90vw' },
buttonSize: { value: 'medium' },
inputSize: { value: 'medium' },
shouldUseVerticalLayout: { value: false },
isMobile: { value: false }
})
}))
vi.mock('../../src/composables/usePerformanceMonitor', () => ({
usePerformanceMonitor: () => ({
recordUpdate: vi.fn()
})
}))
vi.mock('../../src/composables/useDebounceThrottle', () => ({
useDebounceThrottle: () => ({
debounce: (fn: Function) => fn,
throttle: (fn: Function) => fn,
batchExecute: (fn: Function) => {
// 正确模拟批处理函数:接收一个处理数组的函数,返回一个接收单个项目的函数
return (item: any) => {
// 在测试中立即执行,传入包含单个项目的数组
fn([item])
}
}
})
}))
vi.mock('../../src/composables/useAccessibility', () => ({
useAccessibility: () => ({
aria: {
getLabel: (key: string, fallback?: string) => fallback || key,
getDescription: (key: string) => key,
getLiveRegionText: (key: string) => key
},
announce: vi.fn(),
accessibilityClasses: { value: {} },
isAccessibilityMode: { value: false },
liveRegionMessage: { value: '' },
announcements: { value: [] }
})
}))
// Mock useContextEditor
const mockContextEditor = {
currentData: { value: null },
isLoading: { value: false },
smartImport: vi.fn(),
convertFromOpenAI: vi.fn(),
convertFromLangFuse: vi.fn(),
importFromFile: vi.fn(),
exportToFile: vi.fn(),
exportToClipboard: vi.fn(),
setData: vi.fn()
}
vi.mock('../../src/composables/useContextEditor', () => ({
useContextEditor: () => mockContextEditor
}))
/**
* 测试父组件,用于模拟真实的数据同步场景
*/
const TestParentComponent = {
name: 'TestParentComponent',
props: {
initialMessages: {
type: Array,
default: () => []
},
initialVariables: {
type: Object,
default: () => ({})
}
},
setup(props: any, { emit }: any) {
// 共享的响应式状态
const messages = ref([...props.initialMessages])
const variables = ref({ ...props.initialVariables })
const showContextEditor = ref(false)
// 模拟变量扫描函数
const scanVariables = (content: string): string[] => {
if (!content) return []
const matches = content.match(/\{\{([^}]+)\}\}/g) || []
return matches.map(match => match.slice(2, -2))
}
// 模拟变量替换函数
const replaceVariables = (content: string, vars?: Record<string, string>): string => {
if (!content || !vars) return content
let result = content
Object.entries(vars).forEach(([key, value]) => {
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value)
})
return result
}
// Mock variableManager
const mockVariableManager = {
variableManager: ref(null),
isReady: ref(true),
isAdvancedMode: ref(false),
customVariables: ref<Record<string, string>>({}),
allVariables: ref<Record<string, string>>({}),
statistics: ref({
customVariableCount: 0,
predefinedVariableCount: 7,
totalVariableCount: 7,
advancedModeEnabled: false
}),
setAdvancedMode: vi.fn(),
addVariable: vi.fn(),
updateVariable: vi.fn(),
deleteVariable: vi.fn(),
getVariable: vi.fn((name: string) => undefined),
validateVariableName: vi.fn(() => true),
scanVariablesInContent: vi.fn(scanVariables),
replaceVariables: vi.fn(replaceVariables),
detectMissingVariables: vi.fn(() => []),
getConversationMessages: vi.fn(() => []),
setConversationMessages: vi.fn(),
exportVariables: vi.fn(() => '{}'),
importVariables: vi.fn(),
refresh: vi.fn()
}
// 处理 ConversationManager 的消息更新
const handleMessagesUpdate = (newMessages: any[]) => {
messages.value = [...newMessages]
emit('messagesChanged', messages.value)
}
// 处理 ContextEditor 的状态更新
const handleContextEditorStateUpdate = (newState: any) => {
if (newState.messages) {
messages.value = [...newState.messages]
}
if (newState.variables) {
variables.value = { ...newState.variables }
}
emit('contextChanged', { messages: messages.value, variables: variables.value })
}
// 处理上下文变更事件
const handleContextChange = (newMessages: any[], newVariables: Record<string, string>) => {
messages.value = [...newMessages]
variables.value = { ...newVariables }
emit('contextChanged', { messages: messages.value, variables: variables.value })
}
// 打开编辑器
const handleOpenContextEditor = () => {
showContextEditor.value = true
}
// 关闭编辑器
const handleCloseContextEditor = () => {
showContextEditor.value = false
}
return {
messages,
variables,
showContextEditor,
scanVariables,
replaceVariables,
handleMessagesUpdate,
handleContextEditorStateUpdate,
handleContextChange,
handleOpenContextEditor,
handleCloseContextEditor,
mockVariableManager
}
},
template: `
<div>
<ConversationManager
:messages="messages"
:available-variables="variables"
:scan-variables="scanVariables"
:replace-variables="replaceVariables"
@update:messages="handleMessagesUpdate"
@openContextEditor="handleOpenContextEditor"
data-testid="conversation-manager"
/>
<ContextEditor
:visible="showContextEditor"
:state="{ messages, variables, tools: [], showVariablePreview: true, showToolManager: false, mode: 'edit' }"
:scan-variables="scanVariables"
:replace-variables="replaceVariables"
:is-predefined-variable="() => false"
:variable-manager="mockVariableManager"
@update:visible="(visible) => showContextEditor = visible"
@update:state="handleContextEditorStateUpdate"
@contextChange="handleContextChange"
data-testid="context-editor"
/>
</div>
`,
components: {
ConversationManager,
ContextEditor
}
}
describe('ConversationManager 和 ContextEditor 数据同步集成测试', () => {
let wrapper: VueWrapper<any>
beforeEach(() => {
// 清理 mock
vi.clearAllMocks()
if (wrapper) {
wrapper.unmount()
}
})
const createIntegratedWrapper = async (props = {}) => {
const defaultProps = {
initialMessages: [],
initialVariables: {}
}
wrapper = mount(TestParentComponent, {
props: { ...defaultProps, ...props },
global: {
stubs: {},
mocks: {
announcements: []
}
}
})
await nextTick()
return wrapper
}
describe('基础数据同步机制', () => {
it('应该正确初始化共享数据状态', async () => {
const initialMessages = [
{ role: 'user', content: '测试消息 {{testVar}}' }
]
const initialVariables = { testVar: 'value1' }
wrapper = await createIntegratedWrapper({
initialMessages,
initialVariables
})
// 验证 ConversationManager 接收到正确数据
const manager = wrapper.findComponent({ name: 'ConversationManager' })
// 使用 toMatchObject 而不是 toEqual,因为 ConversationManager 会自动添加 id 和 originalContent
expect(manager.props('messages')).toMatchObject(initialMessages)
expect(manager.props('availableVariables')).toEqual(initialVariables)
// 验证父组件状态正确初始化
expect(wrapper.vm.messages).toMatchObject(initialMessages)
expect(wrapper.vm.variables).toEqual(initialVariables)
})
it('应该在 ConversationManager 和 ContextEditor 之间共享同一数据源', async () => {
const initialMessages = [
{ role: 'system', content: '系统提示' },
{ role: 'user', content: '用户消息' }
]
wrapper = await createIntegratedWrapper({
initialMessages
})
// 打开 ContextEditor
wrapper.vm.handleOpenContextEditor()
await nextTick()
// 验证 ContextEditor 接收到相同的数据
const editor = wrapper.findComponent({ name: 'ContextEditor' })
// 使用 toMatchObject 而不是 toEqual,因为消息会自动添加 id 和 originalContent
expect(editor.props('state').messages).toMatchObject(initialMessages)
// 验证两个组件引用同一数据源
expect(wrapper.vm.messages).toBe(wrapper.vm.messages) // 引用相等
})
})
describe('ConversationManager → ContextEditor 数据同步', () => {
it('在 ConversationManager 修改消息时,ContextEditor 应该实时反映变化', async () => {
wrapper = await createIntegratedWrapper({
initialMessages: [
{ role: 'user', content: '原始消息' }
]
})
// 打开 ContextEditor
wrapper.vm.handleOpenContextEditor()
await nextTick()
const manager = wrapper.findComponent({ name: 'ConversationManager' })
const editor = wrapper.findComponent({ name: 'ContextEditor' })
// 在 ConversationManager 中修改消息
const newMessages = [
{ role: 'user', content: '修改后的消息' }
]
await manager.vm.$emit('update:messages', newMessages)
await nextTick()
// 验证 ContextEditor 实时反映变化
expect(wrapper.vm.messages[0].content).toBe('修改后的消息')
expect(editor.props('state').messages[0].content).toBe('修改后的消息')
})
it('在 ConversationManager 添加消息时,ContextEditor 应该立即显示新消息', async () => {
wrapper = await createIntegratedWrapper({
initialMessages: [
{ role: 'user', content: '第一条消息' }
]
})
wrapper.vm.handleOpenContextEditor()
await nextTick()
const manager = wrapper.findComponent({ name: 'ConversationManager' })
const editor = wrapper.findComponent({ name: 'ContextEditor' })
// 在 ConversationManager 中添加消息
const newMessages = [
{ role: 'user', content: '第一条消息' },
{ role: 'assistant', content: '第二条消息' }
]
await manager.vm.$emit('update:messages', newMessages)
await nextTick()
// 验证 ContextEditor 显示新消息
expect(wrapper.vm.messages).toHaveLength(2)
expect(wrapper.vm.messages[1].content).toBe('第二条消息')
expect(editor.props('state').messages).toHaveLength(2)
})
it('在 ConversationManager 删除消息时,ContextEditor 应该同步删除', async () => {
wrapper = await createIntegratedWrapper({
initialMessages: [
{ role: 'user', content: '消息1' },
{ role: 'assistant', content: '消息2' }
]
})
wrapper.vm.handleOpenContextEditor()
await nextTick()
const manager = wrapper.findComponent({ name: 'ConversationManager' })
// 删除第一条消息
const newMessages = [
{ role: 'assistant', content: '消息2' }
]
await manager.vm.$emit('update:messages', newMessages)
await nextTick()
// 验证两个组件都反映删除操作
expect(wrapper.vm.messages).toHaveLength(1)
expect(wrapper.vm.messages[0].content).toBe('消息2')
})
})
describe('ContextEditor → ConversationManager 数据同步', () => {
it('在 ContextEditor 修改消息时,ConversationManager 应该实时反映变化', async () => {
wrapper = await createIntegratedWrapper({
initialMessages: [
{ role: 'user', content: '原始内容' }
]
})
wrapper.vm.handleOpenContextEditor()
await nextTick()
const editor = wrapper.findComponent({ name: 'ContextEditor' })
// 模拟在 ContextEditor 中修改状态
const newState = {
messages: [{ role: 'user', content: '在编辑器中修改的内容' }],
variables: {},
tools: [],
showVariablePreview: true,
showToolManager: false,
mode: 'edit'
}
await editor.vm.$emit('update:state', newState)
await nextTick()
// 验证 ConversationManager 实时反映变化
expect(wrapper.vm.messages[0].content).toBe('在编辑器中修改的内容')
const manager = wrapper.findComponent({ name: 'ConversationManager' })
expect(manager.props('messages')[0].content).toBe('在编辑器中修改的内容')
})
it('在 ContextEditor 通过 contextChange 事件修改数据时,ConversationManager 应该同步', async () => {
wrapper = await createIntegratedWrapper({
initialMessages: [
{ role: 'system', content: '系统消息' }
],
initialVariables: { oldVar: 'oldValue' }
})
wrapper.vm.handleOpenContextEditor()
await nextTick()
const editor = wrapper.findComponent({ name: 'ContextEditor' })
// 模拟 contextChange 事件
const newMessages = [
{ role: 'system', content: '更新的系统消息' },
{ role: 'user', content: '新增的用户消息 {{newVar}}' }
]
const newVariables = { newVar: 'newValue' }
await editor.vm.$emit('contextChange', newMessages, newVariables)
await nextTick()
// 验证父组件状态更新
expect(wrapper.vm.messages).toHaveLength(2)
expect(wrapper.vm.messages[0].content).toBe('更新的系统消息')
expect(wrapper.vm.messages[1].content).toBe('新增的用户消息 {{newVar}}')
expect(wrapper.vm.variables).toEqual({ newVar: 'newValue' })
// 验证 ConversationManager 接收到新数据
const manager = wrapper.findComponent({ name: 'ConversationManager' })
expect(manager.props('messages')).toEqual(newMessages)
expect(manager.props('availableVariables')).toEqual(newVariables)
})
})
describe('模板应用对数据同步的影响', () => {
it('在 ContextEditor 中应用模板时,ConversationManager 应该实时反映模板内容', async () => {
wrapper = await createIntegratedWrapper()
wrapper.vm.handleOpenContextEditor()
await nextTick()
const editor = wrapper.findComponent({ name: 'ContextEditor' })
// 模拟应用模板(通过 contextChange 事件)
const templateMessages = [
{ role: 'system', content: 'You are a helpful assistant specialized in {{domain}}.' },
{ role: 'user', content: '{{userQuery}}' }
]
const templateVariables = { domain: 'AI', userQuery: 'Help me' }
await editor.vm.$emit('contextChange', templateMessages, templateVariables)
await nextTick()
// 验证 ConversationManager 显示模板内容
expect(wrapper.vm.messages).toEqual(templateMessages)
expect(wrapper.vm.variables).toEqual(templateVariables)
const manager = wrapper.findComponent({ name: 'ConversationManager' })
expect(manager.props('messages')).toEqual(templateMessages)
expect(manager.props('availableVariables')).toEqual(templateVariables)
})
})
describe('导入导出对数据同步的影响', () => {
it('在 ContextEditor 中导入数据时,ConversationManager 应该实时更新', async () => {
wrapper = await createIntegratedWrapper({
initialMessages: [{ role: 'user', content: '旧数据' }]
})
wrapper.vm.handleOpenContextEditor()
await nextTick()
const editor = wrapper.findComponent({ name: 'ContextEditor' })
// 模拟导入操作(通过 contextChange 事件)
const importedMessages = [
{ role: 'system', content: '导入的系统消息' },
{ role: 'user', content: '导入的用户消息 {{importedVar}}' }
]
const importedVariables = { importedVar: '导入的变量值' }
await editor.vm.$emit('contextChange', importedMessages, importedVariables)
await nextTick()
// 验证数据同步
expect(wrapper.vm.messages).toEqual(importedMessages)
expect(wrapper.vm.variables).toEqual(importedVariables)
const manager = wrapper.findComponent({ name: 'ConversationManager' })
expect(manager.props('messages')).toEqual(importedMessages)
expect(manager.props('availableVariables')).toEqual(importedVariables)
})
it('导入的数据应该立即在两个组件中可用', async () => {
wrapper = await createIntegratedWrapper()
wrapper.vm.handleOpenContextEditor()
await nextTick()
// 模拟复杂的导入数据
const complexImportedData = {
messages: [
{ role: 'system', content: '复杂系统提示:处理 {{taskType}} 任务' },
{ role: 'user', content: '用户问题:{{question}}' },
{ role: 'assistant', content: '助手回复模板' }
],
variables: {
taskType: '数据分析',
question: '如何优化性能?'
}
}
const editor = wrapper.findComponent({ name: 'ContextEditor' })
await editor.vm.$emit('contextChange', complexImportedData.messages, complexImportedData.variables)
await nextTick()
// 验证复杂数据的同步
expect(wrapper.vm.messages).toHaveLength(3)
expect(wrapper.vm.messages[0].content).toContain('{{taskType}}')
expect(wrapper.vm.variables.taskType).toBe('数据分析')
// 验证 ConversationManager 的变量扫描功能
const detectedVars = wrapper.vm.scanVariables(wrapper.vm.messages[0].content)
expect(detectedVars).toContain('taskType')
})
})
describe('完整的 "编辑即emit → 父级更新 → 实时反映" 链路验证', () => {
it('应该验证完整的数据流链路', async () => {
const eventsEmitted: any[] = []
wrapper = await createIntegratedWrapper({
initialMessages: [{ role: 'user', content: '测试消息' }]
})
// 用onMounted来监听组件事件(替代Vue 3中移除的$on)
const handleMessagesChanged = (messages: any) => {
eventsEmitted.push({ type: 'messagesChanged', data: messages })
}
const handleContextChanged = (context: any) => {
eventsEmitted.push({ type: 'contextChanged', data: context })
}
// 模拟事件监听(通过直接验证组件状态变化替代事件监听)
wrapper.vm.handleOpenContextEditor()
await nextTick()
const manager = wrapper.findComponent({ name: 'ConversationManager' })
const editor = wrapper.findComponent({ name: 'ContextEditor' })
// 第1步:ConversationManager 编辑 → emit
const newMessages1 = [{ role: 'user', content: '第1步修改' }]
await manager.vm.$emit('update:messages', newMessages1)
await nextTick()
// 验证链路:编辑 → emit → 父级更新 → ContextEditor 反映
expect(wrapper.vm.messages[0].content).toBe('第1步修改')
expect(editor.props('state').messages[0].content).toBe('第1步修改')
// 第2步:ContextEditor 编辑 → emit
const newMessages2 = [{ role: 'user', content: '第2步修改' }]
await editor.vm.$emit('contextChange', newMessages2, {})
await nextTick()
// 验证链路:编辑 → emit → 父级更新 → ConversationManager 反映
expect(wrapper.vm.messages[0].content).toBe('第2步修改')
expect(manager.props('messages')[0].content).toBe('第2步修改')
// 验证事件发射(通过emitted()检查)
expect(manager.emitted('update:messages')).toBeTruthy()
expect(editor.emitted('contextChange')).toBeTruthy()
})
it('应该确保数据一致性不会出现竞态条件', async () => {
wrapper = await createIntegratedWrapper({
initialMessages: [{ role: 'user', content: '初始消息' }]
})
wrapper.vm.handleOpenContextEditor()
await nextTick()
const manager = wrapper.findComponent({ name: 'ConversationManager' })
const editor = wrapper.findComponent({ name: 'ContextEditor' })
// 快速连续的修改操作
const operations = [
{ source: 'manager', content: '快速修改1' },
{ source: 'editor', content: '快速修改2' },
{ source: 'manager', content: '快速修改3' }
]
// 模拟快速连续操作
for (const op of operations) {
if (op.source === 'manager') {
await manager.vm.$emit('update:messages', [{ role: 'user', content: op.content }])
} else {
await editor.vm.$emit('contextChange', [{ role: 'user', content: op.content }], {})
}
await nextTick()
}
// 验证最终状态一致
expect(wrapper.vm.messages[0].content).toBe('快速修改3')
expect(manager.props('messages')[0].content).toBe('快速修改3')
expect(editor.props('state').messages[0].content).toBe('快速修改3')
})
})
describe('变量同步特性', () => {
it('变量更新应该在两个组件中同步反映', async () => {
wrapper = await createIntegratedWrapper({
initialMessages: [{ role: 'user', content: 'Hello {{name}}!' }],
initialVariables: { name: 'World' }
})
wrapper.vm.handleOpenContextEditor()
await nextTick()
const editor = wrapper.findComponent({ name: 'ContextEditor' })
// 更新变量
const newVariables = { name: 'Vue', greeting: 'Hi' }
await editor.vm.$emit('contextChange', wrapper.vm.messages, newVariables)
await nextTick()
// 验证变量同步
expect(wrapper.vm.variables).toEqual(newVariables)
const manager = wrapper.findComponent({ name: 'ConversationManager' })
expect(manager.props('availableVariables')).toEqual(newVariables)
// 验证变量替换功能
const replacedContent = wrapper.vm.replaceVariables('Hello {{name}}!', wrapper.vm.variables)
expect(replacedContent).toBe('Hello Vue!')
})
})
})