diff --git a/packages/ui/src/components/context-mode/ContextEditor.vue b/packages/ui/src/components/context-mode/ContextEditor.vue index 861a77ac..d353c3a8 100644 --- a/packages/ui/src/components/context-mode/ContextEditor.vue +++ b/packages/ui/src/components/context-mode/ContextEditor.vue @@ -1265,239 +1265,23 @@ - - - - - + - - - - - + (); -// 导入导出功能 -const contextEditor = useContextEditor(); - // 临时变量管理 const tempVars = useTemporaryVariables(); @@ -1746,14 +1526,10 @@ const loading = ref(false); const activeTab = ref("messages"); const localVisible = ref(props.visible); -// 导入导出状态 +// 导入导出对话框状态 const showImportDialog = ref(false); const showExportDialog = ref(false); -const importData = ref(""); -const importError = ref(""); -const selectedImportFormat = ref("smart"); -const selectedExportFormat = ref("standard"); -const fileInputRef = ref(null); + // 变量值输入框引用(用于自动聚焦) const variableValueInputRef = ref(null); @@ -1906,41 +1682,6 @@ const quickTemplates = computed(() => { ); }); -const normalizeMessage = ( - msg: Partial, -): ConversationMessage => { - const normalizedRole = (msg.role ?? "user") as ConversationMessage["role"]; - - let content = ""; - if (typeof msg.content === "string") { - content = msg.content; - } else if (msg.content != null) { - try { - content = - typeof msg.content === "object" - ? JSON.stringify(msg.content) - : String(msg.content); - } catch { - content = String(msg.content); - } - } - - const normalized: ConversationMessage = { - role: normalizedRole, - content, - }; - - if (typeof msg.name === "string") normalized.name = msg.name; - if (Array.isArray(msg.tool_calls)) { - normalized.tool_calls = - msg.tool_calls as ConversationMessage["tool_calls"]; - } - if (typeof msg.tool_call_id === "string") - normalized.tool_call_id = msg.tool_call_id; - - return normalized; -}; - // 工具函数(统一使用注入函数) const getMessageVariables = (content: string) => { const detected = props.scanVariables(content || "") || []; @@ -2527,266 +2268,32 @@ watch( }, ); -// 导入导出方法 -const importFormats = computed(() => [ - { id: "smart", name: t("contextEditor.importFormats.smart.name"), description: t("contextEditor.importFormats.smart.description") }, - { id: "conversation", name: t("contextEditor.importFormats.conversation.name"), description: t("contextEditor.importFormats.conversation.description") }, - { id: "openai", name: t("contextEditor.importFormats.openai.name"), description: t("contextEditor.importFormats.openai.description") }, - { id: "langfuse", name: t("contextEditor.importFormats.langfuse.name"), description: t("contextEditor.importFormats.langfuse.description") }, -]); +// 导入导出事件处理 +interface ImportSuccessData { + messages: ConversationMessage[]; + tools?: ToolDefinition[]; +} -type ExportFormat = "standard" | "openai" | "template"; +const handleImportSuccess = (data: ImportSuccessData) => { + // 将导入的数据同步到本地状态 + localState.value.messages = data.messages; + localState.value.tools = data.tools || []; -const exportFormats = computed(() => [ - { - id: "standard" as ExportFormat, - name: t("contextEditor.exportFormats.standard.name"), - description: t("contextEditor.exportFormats.standard.description"), - }, - { - id: "openai" as ExportFormat, - name: t("contextEditor.exportFormats.openai.name"), - description: t("contextEditor.exportFormats.openai.description"), - }, - { - id: "template" as ExportFormat, - name: t("contextEditor.exportFormats.template.name"), - description: t("contextEditor.exportFormats.template.description"), - }, -]); + handleStateChange(); -const handleFileUpload = async (event: Event) => { - const file = (event.target as HTMLInputElement).files?.[0]; - if (!file) return; - - try { - loading.value = true; - importError.value = ""; - - const success = await contextEditor.importFromFile(file); - - if (success && contextEditor.currentData.value) { - // 将导入的数据同步到本地状态 - const data = contextEditor.currentData.value; - localState.value.messages = (data.messages || []).map((msg) => - normalizeMessage(msg), - ); - // 不导入 variables,临时变量不持久化 - localState.value.tools = data.tools || []; - - handleStateChange(); - showImportDialog.value = false; - importData.value = ""; - importError.value = ""; - - // 切换到消息编辑标签页 - activeTab.value = "messages"; - announce(t("contextEditor.importSuccess"), "polite"); - } else { - importError.value = t("contextEditor.importFailed"); - } - } catch (err) { - console.error("File upload error:", err); - const errorMsg = err instanceof Error ? err.message : t("contextEditor.importFailed"); - importError.value = errorMsg; - } finally { - loading.value = false; - } + // 切换到消息编辑标签页 + activeTab.value = "messages"; + announce(t("contextEditor.importSuccess"), "polite"); }; -const handleImportSubmit = async () => { - if (!importData.value.trim()) { - importError.value = t("contextEditor.importDataRequired"); - return; - } - - try { - loading.value = true; - importError.value = ""; - const jsonData = JSON.parse(importData.value); - let result; - - switch (selectedImportFormat.value) { - case "smart": - result = contextEditor.smartImport(jsonData); - break; - case "openai": - result = contextEditor.convertFromOpenAI(jsonData); - break; - case "langfuse": - result = contextEditor.convertFromLangFuse(jsonData); - break; - case "conversation": - // 直接设置为对话格式 - if (Array.isArray(jsonData)) { - localState.value.messages = jsonData.map( - (msg: Partial) => - normalizeMessage(msg), - ); - } else if ( - jsonData.messages && - Array.isArray(jsonData.messages) - ) { - localState.value.messages = jsonData.messages.map( - (msg: Partial) => - normalizeMessage(msg), - ); - // 不导入 variables,临时变量不持久化 - localState.value.tools = jsonData.tools || []; - } else { - importError.value = t("contextEditor.invalidConversationFormat"); - return; - } - handleStateChange(); - showImportDialog.value = false; - importData.value = ""; - importError.value = ""; - activeTab.value = "messages"; - announce(t("contextEditor.importSuccess"), "polite"); - return; - default: - importError.value = t("contextEditor.unsupportedImportFormat"); - return; - } - - // 处理转换结果 - if (result && result.success && contextEditor.currentData.value) { - // 将导入的数据同步到本地状态 - const data = contextEditor.currentData.value; - localState.value.messages = (data.messages || []).map((msg) => - normalizeMessage(msg), - ); - // 不导入 variables,临时变量不持久化 - localState.value.tools = data.tools || []; - - handleStateChange(); - showImportDialog.value = false; - importData.value = ""; - importError.value = ""; - activeTab.value = "messages"; - announce(t("contextEditor.importSuccess"), "polite"); - } else { - importError.value = result?.error || t("contextEditor.importFailed"); - } - } catch (err) { - console.error("Import error:", err); - const errorMsg = - err instanceof Error ? err.message : t("contextEditor.invalidJsonFormat"); - importError.value = errorMsg; - } finally { - loading.value = false; - } +const handleExportSuccess = () => { + announce(t("contextEditor.exportSuccess"), "polite"); }; -const handleExportToFile = () => { - try { - loading.value = true; - - // 准备导出数据 - 转换为 StandardPromptData 格式 - const exportData: StandardPromptData = { - messages: localState.value.messages.map((msg) => ({ - role: msg.role, - content: msg.content, - // 保留其他可能的属性 - ...(msg.name && { name: msg.name }), - ...(msg.tool_calls && { tool_calls: msg.tool_calls }), - ...(msg.tool_call_id && { tool_call_id: msg.tool_call_id }), - })), - tools: localState.value.tools, - metadata: { - // 不导出临时变量(会话级别,不持久化) - exportTime: new Date().toISOString(), - version: "1.0", - source: "manual", - origin: "context_editor", - }, - }; - - // 设置导出数据到contextEditor - contextEditor.setData(exportData); - - // 执行导出 - const success = contextEditor.exportToFile( - selectedExportFormat.value as ExportFormat, - `context-export-${Date.now()}`, - ); - - if (success) { - showExportDialog.value = false; - announce(t("contextEditor.exportSuccess"), "polite"); - } else { - throw new Error(t("contextEditor.exportFailed")); - } - } catch (err) { - console.error("Export to file error:", err); - const errorMsg = err instanceof Error ? err.message : t("contextEditor.exportFailed"); - // TODO: 显示错误提示给用户 - announce(`${t("contextEditor.exportFailed")}: ${errorMsg}`, "assertive"); - } finally { - loading.value = false; - } -}; - -const handleExportToClipboard = async () => { - try { - loading.value = true; - - // 准备导出数据 - 转换为 StandardPromptData 格式 - const exportData: StandardPromptData = { - messages: localState.value.messages.map((msg) => ({ - role: msg.role, - content: msg.content, - // 保留其他可能的属性 - ...(msg.name && { name: msg.name }), - ...(msg.tool_calls && { tool_calls: msg.tool_calls }), - ...(msg.tool_call_id && { tool_call_id: msg.tool_call_id }), - })), - tools: localState.value.tools, - metadata: { - // 不导出临时变量(会话级别,不持久化) - exportTime: new Date().toISOString(), - version: "1.0", - source: "manual", - origin: "context_editor", - }, - }; - - // 设置导出数据到contextEditor - contextEditor.setData(exportData); - - // 执行导出到剪贴板 - const success = await contextEditor.exportToClipboard( - selectedExportFormat.value as ExportFormat, - ); - - if (success) { - showExportDialog.value = false; - announce(t("contextEditor.copySuccess"), "polite"); - } else { - throw new Error(t("contextEditor.copyFailed")); - } - } catch (err) { - console.error("Export to clipboard error:", err); - const errorMsg = err instanceof Error ? err.message : t("contextEditor.exportFailed"); - // TODO: 显示错误提示给用户 - announce(`${t("contextEditor.copyFailed")}: ${errorMsg}`, "assertive"); - } finally { - loading.value = false; - } -}; - -const getImportPlaceholder = () => { - switch (selectedImportFormat.value) { - case "openai": - return t("contextEditor.importPlaceholders.openai"); - case "langfuse": - return t("contextEditor.importPlaceholders.langfuse"); - case "conversation": - return t("contextEditor.importPlaceholders.conversation"); - case "smart": - default: - return t("contextEditor.importPlaceholders.smart"); - } +const handleExportError = (message?: string) => { + const fallbackMessage = message || t("contextEditor.exportFailed"); + console.error(fallbackMessage); + announce(fallbackMessage, "assertive"); }; diff --git a/packages/ui/src/components/context-mode/ImportExportDialog.vue b/packages/ui/src/components/context-mode/ImportExportDialog.vue new file mode 100644 index 00000000..2bf23595 --- /dev/null +++ b/packages/ui/src/components/context-mode/ImportExportDialog.vue @@ -0,0 +1,613 @@ + + + + + diff --git a/packages/ui/src/i18n/locales/en-US.ts b/packages/ui/src/i18n/locales/en-US.ts index d081b108..b56479be 100644 --- a/packages/ui/src/i18n/locales/en-US.ts +++ b/packages/ui/src/i18n/locales/en-US.ts @@ -1654,22 +1654,21 @@ export default { // Import/Export formats importFormats: { smart: { name: "Smart Detection", description: "Auto-detect format and convert" }, - conversation: { name: "Conversation", description: "Standard conversation message format" }, openai: { name: "OpenAI", description: "OpenAI API request format" }, langfuse: { name: "LangFuse", description: "LangFuse tracking data format" }, + conversation: { name: "Internal Format", description: "Prompt Optimizer internal JSON structure" }, }, exportFormats: { - standard: { name: "Standard", description: "Internal standard data format" }, + standard: { name: "Internal Format", description: "Prompt Optimizer internal standard format" }, openai: { name: "OpenAI", description: "OpenAI API compatible format" }, - template: { name: "Template", description: "Reusable template format" }, }, - // Import placeholders + // Import placeholders (localized text, JSON snippet fixed in component) importPlaceholders: { - openai: 'OpenAI API request format, e.g.:\n{\n "messages": [...],\n "model": "gpt-4"\n}', - langfuse: 'LangFuse tracking data, e.g.:\n{\n "input": {\n "messages": [...]\n }\n}', - conversation: 'Standard conversation format, e.g.:\n{\n "messages": [\n {"role": "system", "content": "..."},\n {"role": "user", "content": "..."}\n ]\n}', - smart: "Paste any supported JSON format, system will auto-detect", + openai: "OpenAI API request format (example below):", + langfuse: "LangFuse trace payload (example below):", + conversation: "Standard conversation JSON (example below):", + smart: "Paste any supported JSON format (OpenAI, LangFuse, or conversation array). The editor will auto-detect the format.", }, // Console errors (developer logs) diff --git a/packages/ui/src/i18n/locales/zh-CN.ts b/packages/ui/src/i18n/locales/zh-CN.ts index 3dfba5fb..54a57e30 100644 --- a/packages/ui/src/i18n/locales/zh-CN.ts +++ b/packages/ui/src/i18n/locales/zh-CN.ts @@ -1595,22 +1595,21 @@ export default { // Import/Export formats importFormats: { smart: { name: "智能识别", description: "自动检测格式并转换" }, - conversation: { name: "会话格式", description: "标准的会话消息格式" }, openai: { name: "OpenAI", description: "OpenAI API 请求格式" }, langfuse: { name: "LangFuse", description: "LangFuse 追踪数据格式" }, + conversation: { name: "内部格式", description: "Prompt Optimizer 内部标准 JSON 结构" }, }, exportFormats: { - standard: { name: "标准格式", description: "内部标准数据格式" }, + standard: { name: "内部格式", description: "Prompt Optimizer 内部标准数据格式" }, openai: { name: "OpenAI", description: "OpenAI API 兼容格式" }, - template: { name: "模板格式", description: "可复用的模板格式" }, }, - // Import placeholders + // Import placeholders (文本可本地化,JSON 示例固定为英文) importPlaceholders: { - openai: 'OpenAI API 请求格式,例如:\n{\n "messages": [...],\n "model": "gpt-4"\n}', - langfuse: 'LangFuse 追踪数据,例如:\n{\n "input": {\n "messages": [...]\n }\n}', - conversation: '标准会话格式,例如:\n{\n "messages": [\n {"role": "system", "content": "..."},\n {"role": "user", "content": "..."}\n ]\n}', - smart: "粘贴任意支持格式的 JSON 数据,系统将自动识别", + openai: "OpenAI API 请求格式(示例见下方):", + langfuse: "LangFuse 追踪数据格式(示例见下方):", + conversation: "标准会话 JSON 格式(示例见下方):", + smart: "粘贴任意受支持的 JSON(OpenAI、LangFuse 或会话数组),系统会自动识别格式。", }, // Console errors (开发者日志) diff --git a/packages/ui/src/i18n/locales/zh-TW.ts b/packages/ui/src/i18n/locales/zh-TW.ts index a08dd3a3..cc01eda6 100644 --- a/packages/ui/src/i18n/locales/zh-TW.ts +++ b/packages/ui/src/i18n/locales/zh-TW.ts @@ -1559,22 +1559,21 @@ export default { // Import/Export formats importFormats: { smart: { name: "智慧識別", description: "自動偵測格式並轉換" }, - conversation: { name: "會話格式", description: "標準的會話訊息格式" }, openai: { name: "OpenAI", description: "OpenAI API 請求格式" }, langfuse: { name: "LangFuse", description: "LangFuse 追蹤資料格式" }, + conversation: { name: "內部格式", description: "Prompt Optimizer 內部標準 JSON 結構" }, }, exportFormats: { - standard: { name: "標準格式", description: "內部標準資料格式" }, + standard: { name: "內部格式", description: "Prompt Optimizer 內部標準資料格式" }, openai: { name: "OpenAI", description: "OpenAI API 相容格式" }, - template: { name: "範本格式", description: "可複用的範本格式" }, }, - // Import placeholders + // Import placeholders (文字本地化,JSON 範例固定英文) importPlaceholders: { - openai: 'OpenAI API 請求格式,例如:\n{\n "messages": [...],\n "model": "gpt-4"\n}', - langfuse: 'LangFuse 追蹤資料,例如:\n{\n "input": {\n "messages": [...]\n }\n}', - conversation: '標準會話格式,例如:\n{\n "messages": [\n {"role": "system", "content": "..."},\n {"role": "user", "content": "..."}\n ]\n}', - smart: "貼上任意支援格式的 JSON 資料,系統將自動識別", + openai: "OpenAI API 請求格式(下方示例):", + langfuse: "LangFuse 追蹤資料格式(下方示例):", + conversation: "標準會話 JSON 格式(下方示例):", + smart: "貼上任意支援的 JSON(OpenAI、LangFuse 或會話陣列),系統會自動辨識格式。", }, // Console errors (開發者日誌) diff --git a/packages/ui/tests/e2e/context-editor-refactor.spec.ts b/packages/ui/tests/e2e/context-editor-refactor.spec.ts index 0ea21dbc..98e3c5ad 100644 --- a/packages/ui/tests/e2e/context-editor-refactor.spec.ts +++ b/packages/ui/tests/e2e/context-editor-refactor.spec.ts @@ -5,6 +5,7 @@ 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测试 @@ -214,7 +215,18 @@ vi.mock('../../src/composables/useAccessibility', () => ({ }) })) -vi.mock('../../src/composables/useContextEditor', () => ({ +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 }, @@ -222,9 +234,12 @@ vi.mock('../../src/composables/useContextEditor', () => ({ convertFromOpenAI: vi.fn(), convertFromLangFuse: vi.fn(), importFromFile: vi.fn(), - exportToFile: vi.fn().mockResolvedValue(true), + exportToFile: vi.fn().mockReturnValue(true), exportToClipboard: vi.fn().mockResolvedValue(true), - setData: vi.fn() + setData: vi.fn(), + services: { + converter: mockConverter + } }) })) @@ -509,21 +524,12 @@ describe('完整用户流程E2E测试', () => { describe('3. 导入导出和格式转换功能', () => { it('应该支持多种格式的导入功能', async () => { - contextEditorWrapper = mount(ContextEditor, { + const dialogWrapper = mount(ImportExportDialog, { props: { visible: true, - state: { - messages: [], - variables: {}, - tools: [], - showVariablePreview: true, - showToolManager: false, - mode: 'edit' - }, - scanVariables, - replaceVariables, - isPredefinedVariable: () => false, - variableManager: createMockVariableManager() + mode: 'import', + messages: [], + tools: [] }, global: { stubs: {}, @@ -535,40 +541,48 @@ describe('完整用户流程E2E测试', () => { await nextTick() - // 验证导入功能存在 - expect(contextEditorWrapper.vm.handleImport).toBeDefined() - expect(contextEditorWrapper.vm.handleImportSubmit).toBeDefined() - - // 验证支持的导入格式 - const importFormats = contextEditorWrapper.vm.importFormats - expect(importFormats).toEqual([ - { id: 'smart', name: '智能识别', description: '自动检测格式并转换' }, - { id: 'conversation', name: '会话格式', description: '标准的会话消息格式' }, - { id: 'openai', name: 'OpenAI', description: 'OpenAI API 请求格式' }, - { id: 'langfuse', name: 'LangFuse', description: 'LangFuse 追踪数据格式' } - ]) + const formats = dialogWrapper.vm.importFormats + expect(formats.map(f => f.id)).toEqual(['smart', 'openai', 'langfuse', 'conversation']) - // 测试对话格式导入 - const conversationData = { + dialogWrapper.vm.importData = JSON.stringify({ messages: [ - { role: 'system', content: '测试系统消息' }, - { role: 'user', content: '测试用户消息' } + { role: 'system', content: '系统消息' }, + { role: 'user', content: '用户消息' } ], - variables: { testVar: 'testValue' } - } + tools: [] + }) + dialogWrapper.vm.selectedImportFormat = 'conversation' - // 设置导入数据 - contextEditorWrapper.vm.importData = JSON.stringify(conversationData) - contextEditorWrapper.vm.selectedImportFormat = 'conversation' - - // 执行导入 - await contextEditorWrapper.vm.handleImportSubmit() - await nextTick() + await dialogWrapper.vm.handleImportSubmit() - // 验证导入结果 - expect(contextEditorWrapper.emitted('update:state')).toBeTruthy() + 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. 变量管理的跨组件协作', () => { @@ -887,10 +901,11 @@ describe('完整用户流程E2E测试', () => { // 验证最终状态(简化验证) expect(contextEditorWrapper.exists()).toBe(true) expect(contextEditorWrapper.vm).toBeTruthy() - - // 验证导出功能可用 - expect(contextEditorWrapper.vm.handleExportToClipboard).toBeDefined() - expect(contextEditorWrapper.vm.handleExportToFile).toBeDefined() + + // 注意:导出功能已移至 ImportExportDialog 组件 + // 验证导入导出对话框可以打开 + expect(contextEditorWrapper.vm.handleImport).toBeDefined() + expect(contextEditorWrapper.vm.handleExport).toBeDefined() }) it('应该正确处理错误和边界情况', async () => { diff --git a/packages/ui/tests/unit/components/ContextEditor.spec.ts b/packages/ui/tests/unit/components/ContextEditor.spec.ts index 05a744c2..bc5fe348 100644 --- a/packages/ui/tests/unit/components/ContextEditor.spec.ts +++ b/packages/ui/tests/unit/components/ContextEditor.spec.ts @@ -145,9 +145,9 @@ vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string, params?: any) => { const translations = { - 'contextEditor.importPlaceholders.openai': 'OpenAI API 请求格式,例如:\n{\n "messages": [...],\n "model": "gpt-4"\n}', - 'contextEditor.importPlaceholders.langfuse': 'LangFuse 追踪数据,例如:\n{\n "input": {\n "messages": [...]\n }\n}', - 'contextEditor.importPlaceholders.conversation': '标准会话格式,例如:\n{\n "messages": [\n {"role": "system", "content": "..."},\n {"role": "user", "content": "..."}\n ]\n}', + 'contextEditor.importPlaceholders.openai': 'OpenAI API 请求格式(示例如下):', + 'contextEditor.importPlaceholders.langfuse': 'LangFuse 追踪数据格式(示例如下):', + 'contextEditor.importPlaceholders.conversation': '标准会话 JSON 格式(示例如下):', 'contextEditor.importPlaceholders.smart': "粘贴任意支持格式的 JSON 数据,系统将自动识别" } @@ -455,7 +455,7 @@ describe('ContextEditor 综合测试', () => { it('点击导入按钮应该打开导入对话框', async () => { wrapper = await createWrapper() - + await wrapper.vm.handleImport() expect(wrapper.vm.showImportDialog).toBe(true) }) @@ -467,89 +467,12 @@ describe('ContextEditor 综合测试', () => { messages: [{ role: 'user', content: 'test' }] } }) - + await wrapper.vm.handleExport() expect(wrapper.vm.showExportDialog).toBe(true) }) - - it('导入对话框应该显示正确的格式选项', async () => { - wrapper = await createWrapper() - wrapper.vm.showImportDialog = true - await nextTick() - - expect(wrapper.vm.importFormats).toHaveLength(4) - expect(wrapper.vm.importFormats.map(f => f.id)).toEqual(['smart', 'conversation', 'openai', 'langfuse']) - }) - - it('导出对话框应该显示正确的格式选项', async () => { - wrapper = await createWrapper() - wrapper.vm.showExportDialog = true - await nextTick() - - expect(wrapper.vm.exportFormats).toHaveLength(3) - expect(wrapper.vm.exportFormats.map(f => f.id)).toEqual(['standard', 'openai', 'template']) - }) - - it('应该正确处理不同导入格式的占位符', async () => { - wrapper = await createWrapper() - - wrapper.vm.selectedImportFormat = 'openai' - expect(wrapper.vm.getImportPlaceholder()).toContain('OpenAI API') - - wrapper.vm.selectedImportFormat = 'langfuse' - expect(wrapper.vm.getImportPlaceholder()).toContain('LangFuse') - - wrapper.vm.selectedImportFormat = 'conversation' - expect(wrapper.vm.getImportPlaceholder()).toContain('会话格式') - - wrapper.vm.selectedImportFormat = 'smart' - expect(wrapper.vm.getImportPlaceholder()).toContain('自动识别') - }) - - it('导入失败时应该显示错误信息', async () => { - mockContextEditor.smartImport.mockReturnValueOnce({ - success: false, - error: '导入数据格式错误' - }) - - wrapper = await createWrapper() - wrapper.vm.selectedImportFormat = 'smart' - wrapper.vm.importData = 'invalid json' - - await wrapper.vm.handleImportSubmit() - - expect(wrapper.vm.importError).toBeTruthy() - }) - - it('导出失败时应该处理错误', async () => { - mockContextEditor.exportToFile.mockReturnValueOnce(false) - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - - wrapper = await createWrapper({ - state: { - ...defaultProps.state, - messages: [{ role: 'user', content: 'test' }] - } - }) - - await wrapper.vm.handleExportToFile() - - expect(consoleSpy).toHaveBeenCalled() - consoleSpy.mockRestore() - }) - - it('导入成功后应该清空导入状态', async () => { - wrapper = await createWrapper() - wrapper.vm.selectedImportFormat = 'conversation' - wrapper.vm.importData = '{"messages":[{"role":"user","content":"test"}]}' - - await wrapper.vm.handleImportSubmit() - - expect(wrapper.vm.showImportDialog).toBe(false) - expect(wrapper.vm.importData).toBe('') - expect(wrapper.vm.importError).toBe('') - }) + // 注意:导入导出的详细功能测试已移至 ImportExportDialog.spec.ts }) describe('optimizationMode 参数传递', () => { @@ -698,36 +621,8 @@ describe('ContextEditor 综合测试', () => { expect(wrapper.emitted('update:visible')[0]).toEqual([false]) }) }) - - describe('错误处理', () => { - it('导入失败时应该显示错误信息', async () => { - // 这个测试实际走的是JSON.parse失败的分支,测试输入无效JSON的错误处理 - wrapper = await createWrapper() - wrapper.vm.selectedImportFormat = 'smart' - wrapper.vm.importData = 'invalid json' // 无效JSON,触发JSON.parse失败 - - await wrapper.vm.handleImportSubmit() - - // 根据组件代码第1262行:'数据格式错误,请检查JSON格式' - // 但实际错误信息来自JSON.parse的SyntaxError message - expect(wrapper.vm.importError).toContain('Unexpected token') - }) - - it('文件上传失败时应该设置错误状态', async () => { - mockContextEditor.importFromFile.mockResolvedValueOnce(false) - - wrapper = await createWrapper() - - const file = new File(['invalid content'], 'test.json', { type: 'application/json' }) - const event = { target: { files: [file] } } - - await wrapper.vm.handleFileUpload(event) - - expect(wrapper.vm.importError).toBeTruthy() - }) - }) }) // 导出类型以供其他测试使用 export type MockContextEditor = typeof mockContextEditor -export { mockQuickTemplates } \ No newline at end of file +export { mockQuickTemplates } diff --git a/packages/ui/tests/unit/components/ImportExportDialog.spec.ts b/packages/ui/tests/unit/components/ImportExportDialog.spec.ts new file mode 100644 index 00000000..36dcc363 --- /dev/null +++ b/packages/ui/tests/unit/components/ImportExportDialog.spec.ts @@ -0,0 +1,194 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { nextTick } from 'vue' +import ImportExportDialog from '../../../src/components/context-mode/ImportExportDialog.vue' + +const mockConverter = { + toOpenAI: vi.fn() +} + +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(), + services: { + converter: mockConverter + } +} + +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key: string) => key + }) +})) + +vi.mock('../../../src/composables/context/useContextEditor', () => ({ + useContextEditor: () => mockContextEditor +})) + +const naiveStubs = { + NModal: { + name: 'NModal', + template: '
', + props: ['show', 'title', 'preset', 'style'], + emits: ['update:show'] + }, + NButton: { + name: 'NButton', + template: '', + props: ['type', 'disabled', 'loading', 'size', 'secondary'], + emits: ['click'] + }, + NSpace: { + name: 'NSpace', + template: '
', + props: ['size', 'align', 'justify', 'wrap'] + }, + NInput: { + name: 'NInput', + template: '', + props: ['value', 'type', 'placeholder', 'autosize', 'readonly'], + emits: ['update:value'] + }, + NText: { + name: 'NText', + template: '', + props: ['depth', 'type'] + } +} + +type DialogProps = { + visible: boolean; + mode: 'import' | 'export'; + messages: any[]; + tools?: any[]; +} + +const createWrapper = (props?: Partial) => { + return mount(ImportExportDialog, { + props: { + visible: true, + mode: 'import', + messages: [], + tools: [], + ...props + }, + global: { + stubs: naiveStubs + } + }) +} + +describe('ImportExportDialog', () => { + beforeEach(() => { + mockContextEditor.currentData.value = null + mockContextEditor.smartImport.mockReset() + mockContextEditor.convertFromOpenAI.mockReset() + mockContextEditor.convertFromLangFuse.mockReset() + mockContextEditor.importFromFile.mockReset() + mockContextEditor.exportToFile.mockReset() + mockContextEditor.exportToClipboard.mockReset() + mockContextEditor.setData.mockReset() + mockContextEditor.exportToFile.mockReturnValue(true) + mockContextEditor.exportToClipboard.mockResolvedValue(true) + mockConverter.toOpenAI.mockReset() + mockConverter.toOpenAI.mockImplementation((data) => ({ + success: true, + data: { + model: data.model || 'gpt-test', + messages: data.messages, + tools: data.tools + } + })) + }) + + it('应该提供完整的导入格式选项', () => { + const wrapper = createWrapper() + expect(wrapper.vm.importFormats.map(f => f.id)).toEqual(['smart', 'openai', 'langfuse', 'conversation']) + }) + + it('会话格式导入成功后应该触发 import-success 事件', async () => { + const wrapper = createWrapper() + wrapper.vm.selectedImportFormat = 'conversation' + wrapper.vm.importData = JSON.stringify({ + messages: [{ role: 'user', content: 'hello' }] + }) + + await wrapper.vm.handleImportSubmit() + + const events = wrapper.emitted('import-success') + expect(events).toBeTruthy() + expect(events?.[0]?.[0]?.messages).toHaveLength(1) + expect(wrapper.vm.importError).toBe('') + }) + + it('无效 JSON 数据会设置错误信息', async () => { + const wrapper = createWrapper() + wrapper.vm.importData = 'invalid json' + wrapper.vm.selectedImportFormat = 'smart' + + await wrapper.vm.handleImportSubmit() + + expect(wrapper.vm.importError.length).toBeGreaterThan(0) + }) + + it('导出预览应该与实际导出结构一致', async () => { + const wrapper = createWrapper({ + mode: 'export', + messages: [{ role: 'user', content: 'test' }], + tools: [{ type: 'function', function: { name: 'tool', description: 'desc', parameters: {} } }] + }) + + await nextTick() + + const preview = JSON.parse(wrapper.vm.exportPreviewData) + expect(preview.messages).toHaveLength(1) + expect(preview.tools).toHaveLength(1) + expect(preview.metadata.origin).toBe('import_export_dialog') + }) + + it('导出成功会触发 export-success 并清除错误信息', async () => { + const wrapper = createWrapper({ + mode: 'export', + messages: [{ role: 'user', content: 'test' }] + }) + + await wrapper.vm.handleExportToFile() + + expect(wrapper.emitted('export-success')).toBeTruthy() + expect(wrapper.vm.exportError).toBe('') + }) + + it('导出失败会展示错误并触发 export-error', async () => { + mockContextEditor.exportToClipboard.mockResolvedValueOnce(false) + const wrapper = createWrapper({ + mode: 'export', + messages: [{ role: 'user', content: 'test' }] + }) + + await wrapper.vm.handleExportToClipboard() + + expect(wrapper.vm.exportError).toContain('contextEditor.copyFailed') + expect(wrapper.emitted('export-error')).toBeTruthy() + }) + + it('选择 OpenAI 格式时预览应基于转换结果', async () => { + const wrapper = createWrapper({ + mode: 'export', + messages: [{ role: 'user', content: 'preview' }] + }) + + wrapper.vm.selectedExportFormat = 'openai' + await nextTick() + + const preview = JSON.parse(wrapper.vm.exportPreviewData) + expect(preview.model).toBe('gpt-test') + expect(preview.metadata).toBeUndefined() + }) +})