Files
workspace/code/fms/fms-vue/tests/unit/workflow-canvas-layout.spec.js
2026-09-23 23:05:01 +08:00

176 lines
5.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
})
})