import { describe, expect, it } from 'vitest' import { nextTick } from 'vue' import { mount } from '@vue/test-utils' import WorkflowCanvas from '@/views/system/workflow/designer/WorkflowCanvas.vue' // jsdom 不实现 SVG 测量,LogicFlow 的节点文本(autoWrap)会用到 if (typeof SVGElement !== 'undefined' && !SVGElement.prototype.getBBox) { SVGElement.prototype.getBBox = () => ({ x: 0, y: 0, width: 0, height: 0 }) } /** 一个坐标明显重叠的节点 */ function stackedNode(nodeKey, nodeType, name) { return { nodeKey, nodeType, name, x: 300, y: 300, config: {}, actorRules: [] } } /** 三个节点全叠在同一个坐标上,布局后应该被拉开成纵向流程 */ function messyDraft() { return { schemaVersion: 1, definitionId: 'expense_approval', name: '费用报销审批', nodes: [ stackedNode('start', 'start', '开始'), stackedNode('user_task_1', 'user_task', '部门负责人审批'), stackedNode('end', 'end', '结束'), ], edges: [ { edgeKey: 'edge_1', from: 'start', to: 'user_task_1', name: '', condition: null }, { edgeKey: 'edge_2', from: 'user_task_1', to: 'end', name: '', condition: null }, ], } } describe('画布一键整理(@logicflow/layout 的 dagre 插件)', () => { it('把重叠节点按纵向流程拉开,并把新坐标回报给上层', async () => { const wrapper = mount(WorkflowCanvas, { props: { draft: messyDraft() }, attachTo: document.body, }) await nextTick() const changed = wrapper.vm.autoLayout() expect(changed).toBe(true) // 布局结果必须回报给上层,否则草稿里的 x/y 还是旧的 const emitted = wrapper.emitted('graph-change') expect(emitted).toBeTruthy() const graph = emitted[emitted.length - 1][0] const byId = Object.fromEntries(graph.nodes.map((item) => [item.id, item])) // 纵向(rankdir: TB):开始在上、结束在下,且三个节点不再重叠 expect(byId.start.y).toBeLessThan(byId.user_task_1.y) expect(byId.user_task_1.y).toBeLessThan(byId.end.y) expect(new Set(graph.nodes.map((item) => `${item.x},${item.y}`)).size).toBe(3) wrapper.unmount() }) it('布局只改坐标:节点与连线的编码、拓扑原样保留', async () => { const wrapper = mount(WorkflowCanvas, { props: { draft: messyDraft() }, attachTo: document.body, }) await nextTick() wrapper.vm.autoLayout() const emitted = wrapper.emitted('graph-change') const graph = emitted[emitted.length - 1][0] // 编码稳定,上层才能按 nodeKey / edgeKey 回填审批人与条件 expect(graph.nodes.map((item) => item.id).toSorted()).toEqual(['end', 'start', 'user_task_1']) expect(graph.edges.map((item) => item.id).toSorted()).toEqual(['edge_1', 'edge_2']) expect(graph.edges.map((item) => [item.sourceNodeId, item.targetNodeId])).toEqual([ ['start', 'user_task_1'], ['user_task_1', 'end'], ]) wrapper.unmount() }) }) /** 派发一个带 ctrl 的按键(LogicFlow 内置快捷键没有剪切,这条我们自己监听) */ function pressCtrl(target, key) { target.dispatchEvent(new KeyboardEvent('keydown', { key, ctrlKey: true, bubbles: true, cancelable: true })) } /** 派发一个不带修饰键的按键(删除) */ function pressKey(target, key) { target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true })) } describe('画布快捷键:剪切(ctrl+x)', () => { it('删掉选中节点,与它相连的边一并消失', async () => { const wrapper = mount(WorkflowCanvas, { props: { draft: messyDraft() }, attachTo: document.body, }) await nextTick() wrapper.vm.focusNode('user_task_1') await nextTick() pressCtrl(wrapper.find('.workflow-canvas').element, 'x') await nextTick() const graph = wrapper.emitted('graph-change').at(-1)[0] expect(graph.nodes.map((item) => item.id)).toEqual(['start', 'end']) expect(graph.edges).toEqual([]) wrapper.unmount() }) it('没有选中元素时不动画布', async () => { const wrapper = mount(WorkflowCanvas, { props: { draft: messyDraft() }, attachTo: document.body, }) await nextTick() pressCtrl(wrapper.find('.workflow-canvas').element, 'x') await nextTick() expect(wrapper.emitted('graph-change')).toBeUndefined() wrapper.unmount() }) it('焦点不在画布上也能连着剪(删完一次不用再点画布)', async () => { const wrapper = mount(WorkflowCanvas, { props: { draft: messyDraft() }, attachTo: document.body, }) await nextTick() // 事件从 body 冒泡:快捷键监听在 document 上,不受画布焦点影响 wrapper.vm.focusNode('user_task_1') await nextTick() pressCtrl(document.body, 'x') await nextTick() wrapper.vm.focusNode('end') await nextTick() pressCtrl(document.body, 'x') await nextTick() const graph = wrapper.emitted('graph-change').at(-1)[0] expect(graph.nodes.map((item) => item.id)).toEqual(['start']) wrapper.unmount() }) it('焦点不在画布上时,内置的复制/粘贴/删除照样能用', async () => { const wrapper = mount(WorkflowCanvas, { props: { draft: messyDraft() }, attachTo: document.body, }) await nextTick() wrapper.vm.focusNode('user_task_1') await nextTick() pressCtrl(document.body, 'c') await nextTick() pressCtrl(document.body, 'v') await nextTick() let graph = wrapper.emitted('graph-change').at(-1)[0] expect(graph.nodes.length).toBe(4) pressKey(document.body, 'Backspace') await nextTick() graph = wrapper.emitted('graph-change').at(-1)[0] expect(graph.nodes.length).toBe(3) wrapper.unmount() }) })