176 lines
5.7 KiB
JavaScript
176 lines
5.7 KiB
JavaScript
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()
|
||
})
|
||
})
|