|
| 1 | +import assert from 'node:assert/strict' |
| 2 | +import { readFileSync } from 'node:fs' |
| 3 | +import { test } from 'node:test' |
| 4 | +import { compileFunction } from 'node:vm' |
| 5 | +import { compileScript, parse } from '@vue/compiler-sfc' |
| 6 | +import ts from 'typescript' |
| 7 | +import * as Vue from 'vue' |
| 8 | + |
| 9 | +const filename = new URL('../src/views/knowledge-detail/problem/components/AssociateParagraphDialog.vue', import.meta.url) |
| 10 | +const { descriptor } = parse(readFileSync(filename, 'utf8')) |
| 11 | +const script = compileScript(descriptor, { id: 'problem-association-test' }) |
| 12 | +const { outputText } = ts.transpileModule(script.content, { |
| 13 | + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, |
| 14 | +}) |
| 15 | + |
| 16 | +// 执行实际弹窗的 setup 和交互方法,仅替换页面上下文、请求与无 DOM 的模板引用。 |
| 17 | +function createDialog(initialParagraphs = [], { associate, unassociate } = {}) { |
| 18 | + const calls = [] |
| 19 | + const events = [] |
| 20 | + const messages = [] |
| 21 | + const resourceApi = { |
| 22 | + document: { getAllDocuments: async () => [] }, |
| 23 | + problem: { |
| 24 | + getProblemDetail: async (...args) => { |
| 25 | + calls.push(['detail', ...args]) |
| 26 | + return initialParagraphs |
| 27 | + }, |
| 28 | + putBatchAssociateProblems: async (...args) => { |
| 29 | + calls.push(['associate', ...args]) |
| 30 | + await associate?.(...args) |
| 31 | + }, |
| 32 | + }, |
| 33 | + paragraph: { |
| 34 | + putUnassociateParagraphProblem: async (...args) => { |
| 35 | + calls.push(['unassociate', ...args]) |
| 36 | + await unassociate?.(...args) |
| 37 | + }, |
| 38 | + }, |
| 39 | + } |
| 40 | + const dependencies = { |
| 41 | + vue: { ...Vue, useTemplateRef: () => Vue.ref() }, |
| 42 | + '@/components/mk-search-list/index.vue': {}, |
| 43 | + '@/api/enums': { DOCUMENT_RESOURCE_TYPE: { DOCUMENT: 'DOCUMENT', IMAGE: 'IMAGE' } }, |
| 44 | + '@/constants/knowledge': { KNOWLEDGE_TYPE_KEY: {}, KNOWLEDGE_TYPE_MAP: {} }, |
| 45 | + '@/utils/common': { getFileUrl: () => '' }, |
| 46 | + '@/utils/icon': { getFileIconUrl: () => '' }, |
| 47 | + '@/utils/message': { MsgSuccess: (message) => messages.push(message) }, |
| 48 | + '@/views/knowledge-detail/context': { |
| 49 | + useKnowledgeDetailContext: () => ({ knowledgeId: Vue.ref('knowledge-a'), resourceApi }), |
| 50 | + }, |
| 51 | + } |
| 52 | + const exports = {} |
| 53 | + compileFunction(outputText, ['require', 'exports'])((id) => { |
| 54 | + assert.ok(Object.hasOwn(dependencies, id), `Unexpected dependency: ${id}`) |
| 55 | + return dependencies[id] |
| 56 | + }, exports) |
| 57 | + const dialog = exports.default.setup({}, { expose() {}, emit: (event) => events.push(event) }) |
| 58 | + return { dialog, calls, events, messages } |
| 59 | +} |
| 60 | + |
| 61 | +const webParagraph = { id: 'paragraph-web', document_id: 'document-pdf' } |
| 62 | +const textParagraph = { id: 'paragraph-text', document_id: 'document-text' } |
| 63 | +const imageParagraph = { id: 'paragraph-image', document_id: 'document-image' } |
| 64 | + |
| 65 | +test('取消唯一已关联分段后确认,提交解除关联并刷新列表', async () => { |
| 66 | + const { dialog, calls, events } = createDialog([webParagraph]) |
| 67 | + await dialog.open(['problem-a']) |
| 68 | + dialog.handleToggleParagraph(webParagraph) |
| 69 | + assert.equal(dialog.selectedParagraphs.value.length, 0) |
| 70 | + await dialog.handleSubmit() |
| 71 | + assert.deepEqual(calls, [ |
| 72 | + ['detail', 'knowledge-a', 'problem-a'], |
| 73 | + ['unassociate', 'knowledge-a', 'document-pdf', 'paragraph-web', 'problem-a'], |
| 74 | + ]) |
| 75 | + assert.equal(dialog.visible.value, false) |
| 76 | + assert.deepEqual(events, ['success']) |
| 77 | +}) |
| 78 | + |
| 79 | +test('同时新增和取消跨文档分段,只提交差异并保留未变更关联', async () => { |
| 80 | + const { dialog, calls } = createDialog([webParagraph, textParagraph]) |
| 81 | + await dialog.open(['problem-a']) |
| 82 | + dialog.handleToggleParagraph(webParagraph) |
| 83 | + dialog.handleToggleParagraph(imageParagraph) |
| 84 | + await dialog.handleSubmit() |
| 85 | + assert.deepEqual(calls.slice(1), [ |
| 86 | + [ |
| 87 | + 'associate', |
| 88 | + 'knowledge-a', |
| 89 | + { |
| 90 | + problem_id_list: ['problem-a'], |
| 91 | + paragraph_list: [{ paragraph_id: 'paragraph-image', document_id: 'document-image' }], |
| 92 | + }, |
| 93 | + ], |
| 94 | + ['unassociate', 'knowledge-a', 'document-pdf', 'paragraph-web', 'problem-a'], |
| 95 | + ]) |
| 96 | +}) |
| 97 | + |
| 98 | +test('恢复原勾选后确认不发写请求,取消关闭也不保存草稿', async () => { |
| 99 | + const { dialog, calls, events } = createDialog([webParagraph]) |
| 100 | + await dialog.open(['problem-a']) |
| 101 | + dialog.handleToggleParagraph(webParagraph) |
| 102 | + dialog.handleToggleParagraph(webParagraph) |
| 103 | + await dialog.handleSubmit() |
| 104 | + assert.equal(dialog.visible.value, false) |
| 105 | + assert.equal(calls.length, 1) |
| 106 | + assert.deepEqual(events, []) |
| 107 | + dialog.handleClosed() |
| 108 | + await dialog.open(['problem-a']) |
| 109 | + dialog.handleToggleParagraph(webParagraph) |
| 110 | + dialog.visible.value = false |
| 111 | + dialog.handleClosed() |
| 112 | + await dialog.open(['problem-a']) |
| 113 | + assert.equal(dialog.isSelected(webParagraph.id), true) |
| 114 | + assert.ok(calls.every(([method]) => method === 'detail')) |
| 115 | +}) |
| 116 | + |
| 117 | +test('批量问题继续只添加所选分段,不查询或解除各问题原有关联', async () => { |
| 118 | + const { dialog, calls } = createDialog([webParagraph]) |
| 119 | + await dialog.open(['problem-a', 'problem-b']) |
| 120 | + dialog.handleToggleParagraph(imageParagraph) |
| 121 | + await dialog.handleSubmit() |
| 122 | + assert.deepEqual(calls, [ |
| 123 | + [ |
| 124 | + 'associate', |
| 125 | + 'knowledge-a', |
| 126 | + { |
| 127 | + problem_id_list: ['problem-a', 'problem-b'], |
| 128 | + paragraph_list: [{ paragraph_id: 'paragraph-image', document_id: 'document-image' }], |
| 129 | + }, |
| 130 | + ], |
| 131 | + ]) |
| 132 | +}) |
| 133 | + |
| 134 | +test('解除失败保留选择,恢复提交状态并允许重试', async () => { |
| 135 | + let failing = true |
| 136 | + const { dialog, calls, events, messages } = createDialog([webParagraph], { |
| 137 | + unassociate: async () => { |
| 138 | + if (failing) throw new Error('Failed to unlink') |
| 139 | + }, |
| 140 | + }) |
| 141 | + await dialog.open(['problem-a']) |
| 142 | + dialog.handleToggleParagraph(webParagraph) |
| 143 | + await dialog.handleSubmit() |
| 144 | + assert.equal(dialog.visible.value, true) |
| 145 | + assert.equal(dialog.isSelected(webParagraph.id), false) |
| 146 | + assert.equal(dialog.operationLoading.value, false) |
| 147 | + assert.deepEqual(events, []) |
| 148 | + assert.deepEqual(messages, []) |
| 149 | + failing = false |
| 150 | + await dialog.handleSubmit() |
| 151 | + assert.equal(calls.filter(([method]) => method === 'unassociate').length, 2) |
| 152 | + assert.equal(dialog.visible.value, false) |
| 153 | + assert.deepEqual(events, ['success']) |
| 154 | +}) |
| 155 | + |
| 156 | +test('部分成功后重试跳过已解除项,重新勾选已解除项时能补回关联', async () => { |
| 157 | + for (const reselect of [false, true]) { |
| 158 | + let failing = true |
| 159 | + const { dialog, calls } = createDialog([webParagraph, textParagraph], { |
| 160 | + unassociate: async (_knowledgeId, _documentId, paragraphId) => { |
| 161 | + if (paragraphId === textParagraph.id && failing) throw new Error('Failed to unlink') |
| 162 | + }, |
| 163 | + }) |
| 164 | + await dialog.open(['problem-a']) |
| 165 | + dialog.handleToggleParagraph(webParagraph) |
| 166 | + dialog.handleToggleParagraph(textParagraph) |
| 167 | + await dialog.handleSubmit() |
| 168 | + assert.equal(dialog.visible.value, true) |
| 169 | + failing = false |
| 170 | + if (reselect) dialog.handleToggleParagraph(webParagraph) |
| 171 | + const completedCalls = calls.length |
| 172 | + await dialog.handleSubmit() |
| 173 | + const expectedCalls = [['unassociate', 'knowledge-a', 'document-text', 'paragraph-text', 'problem-a']] |
| 174 | + if (reselect) |
| 175 | + expectedCalls.unshift([ |
| 176 | + 'associate', |
| 177 | + 'knowledge-a', |
| 178 | + { |
| 179 | + problem_id_list: ['problem-a'], |
| 180 | + paragraph_list: [{ paragraph_id: 'paragraph-web', document_id: 'document-pdf' }], |
| 181 | + }, |
| 182 | + ]) |
| 183 | + assert.deepEqual(calls.slice(completedCalls), expectedCalls) |
| 184 | + assert.equal(dialog.visible.value, false) |
| 185 | + } |
| 186 | +}) |
| 187 | + |
| 188 | +test('新增成功但解除失败后,可以取消刚新增的关联并再次提交', async () => { |
| 189 | + let failing = true |
| 190 | + const { dialog, calls } = createDialog([webParagraph], { |
| 191 | + unassociate: async () => { |
| 192 | + if (failing) throw new Error('Failed to unlink') |
| 193 | + }, |
| 194 | + }) |
| 195 | + await dialog.open(['problem-a']) |
| 196 | + dialog.handleToggleParagraph(webParagraph) |
| 197 | + dialog.handleToggleParagraph(imageParagraph) |
| 198 | + await dialog.handleSubmit() |
| 199 | + assert.equal(dialog.visible.value, true) |
| 200 | + failing = false |
| 201 | + dialog.handleToggleParagraph(imageParagraph) |
| 202 | + const completedCalls = calls.length |
| 203 | + await dialog.handleSubmit() |
| 204 | + assert.deepEqual(calls.slice(completedCalls), [ |
| 205 | + ['unassociate', 'knowledge-a', 'document-pdf', 'paragraph-web', 'problem-a'], |
| 206 | + ['unassociate', 'knowledge-a', 'document-image', 'paragraph-image', 'problem-a'], |
| 207 | + ]) |
| 208 | + assert.equal(dialog.visible.value, false) |
| 209 | +}) |
0 commit comments