import { describe, expect, it } from 'vitest' import { spreadBranchNodes } from '@/views/system/workflow/designer/utils' /** 构造一个画布节点 */ function node(id, type, x, y, width, height, text) { return { id, type, x, y, width, height, text: text ?? { value: id, x, y } } } function edge(id, sourceNodeId, targetNodeId) { return { id, sourceNodeId, targetNodeId, offset: 15 } } /** 默认出口表:source → 出线目标 */ function defaultExits(entries) { return new Map(Object.entries(entries).map(([source, targets]) => [source, new Set(targets)])) } describe('一键整理的布局后处理', () => { it('旁支与默认出口排在同一列(连线从它正中穿过)时,推到该连线的外侧', () => { const graph = { nodes: [ node('amount_gateway', 'fms-exclusive-gateway', 200, 300, 68, 68), node('gm_approve', 'fms-user-task', 200, 400, 160, 48), node('finance_review', 'fms-user-task', 200, 500, 160, 48), ], edges: [ edge('e3', 'amount_gateway', 'gm_approve'), edge('e4', 'amount_gateway', 'finance_review'), edge('e5', 'gm_approve', 'finance_review'), ], } const moved = spreadBranchNodes(graph) expect(moved.has('gm_approve')).toBe(true) // 线在 x=200(金额判断与财务复核同列),旁支被推到线右侧、留白 120 expect(graph.nodes[1].x).toBe(400) // 源、目标本身不动 expect(graph.nodes[0].x).toBe(200) expect(graph.nodes[2].x).toBe(200) }) it('文本坐标跟着节点一起搬,否则框动了字还留在原地', () => { const graph = { nodes: [ node('amount_gateway', 'fms-exclusive-gateway', 200, 300, 68, 68, { value: '金额判断', x: 200, y: 300 }), node('gm_approve', 'fms-user-task', 200, 400, 160, 48, { value: '总经理审批', x: 200, y: 400 }), node('finance_review', 'fms-user-task', 200, 500, 160, 48, { value: '财务复核', x: 200, y: 500 }), ], edges: [ edge('e3', 'amount_gateway', 'gm_approve'), edge('e4', 'amount_gateway', 'finance_review'), edge('e5', 'gm_approve', 'finance_review'), ], } spreadBranchNodes(graph) expect(graph.nodes[1].text.x).toBe(graph.nodes[1].x) expect(graph.nodes[0].text.x).toBe(200) }) it('网关旁支与主干贴得太近时,推到固定留白处(含旁支下游整条)', () => { const graph = { nodes: [ node('split', 'fms-exclusive-gateway', 0, 0, 68, 68), node('branch', 'fms-user-task', 120, 200, 160, 48), node('branch_2', 'fms-user-task', 120, 320, 160, 48), node('join', 'fms-user-task', 0, 440, 160, 48), ], edges: [ edge('e1', 'split', 'branch'), edge('e2', 'branch', 'branch_2'), edge('e3', 'branch_2', 'join'), edge('e4', 'split', 'join'), ], } const moved = spreadBranchNodes(graph, { defaultExitTargets: defaultExits({ split: ['join'] }) }) // 间距只有 6px,补到 120:120 + (120 - 6) = 234 expect(moved.has('branch')).toBe(true) expect(moved.has('branch_2')).toBe(true) expect(graph.nodes[1].x).toBe(234) expect(graph.nodes[2].x).toBe(234) // 与网关同列的默认出口不动 expect(graph.nodes[3].x).toBe(0) }) it('旁支已经让开足够距离时不动它', () => { const graph = { nodes: [ node('split', 'fms-exclusive-gateway', 0, 0, 68, 68), node('branch', 'fms-user-task', 300, 200, 160, 48), node('join', 'fms-user-task', 0, 400, 160, 48), ], edges: [ edge('e1', 'split', 'branch'), edge('e2', 'branch', 'join'), edge('e3', 'split', 'join'), ], } const moved = spreadBranchNodes(graph, { defaultExitTargets: defaultExits({ split: ['join'] }) }) expect(moved.size).toBe(0) expect(graph.nodes[1].x).toBe(300) }) it('并行网关对齐到三条分支的水平中心,中间那条直上直下', () => { const graph = { nodes: [ node('split', 'fms-parallel-gateway', 0, 0, 68, 68), node('a', 'fms-user-task', 0, 200, 160, 48), node('b', 'fms-user-task', 260, 200, 160, 48), node('c', 'fms-user-task', 520, 200, 160, 48), node('join', 'fms-parallel-gateway', 260, 400, 68, 68), ], edges: [ edge('e1', 'split', 'a'), edge('e2', 'split', 'b'), edge('e3', 'split', 'c'), edge('e4', 'a', 'join'), edge('e5', 'b', 'join'), edge('e6', 'c', 'join'), ], } const moved = spreadBranchNodes(graph) expect(moved.has('split')).toBe(true) // 三条分支的中间那条在 260,分叉点与汇合点都对齐到这里 expect(graph.nodes[0].x).toBe(260) expect(graph.nodes[4].x).toBe(260) // 分支节点自己不动 expect(graph.nodes[1].x).toBe(0) expect(graph.nodes[3].x).toBe(520) }) it('并行网关下面两条分支时,网关落在两条分支中间', () => { const graph = { nodes: [ node('split', 'fms-parallel-gateway', 0, 0, 68, 68), node('a', 'fms-user-task', 0, 200, 160, 48), node('b', 'fms-user-task', 260, 200, 160, 48), node('join', 'fms-parallel-gateway', 0, 400, 68, 68), ], edges: [edge('e1', 'split', 'a'), edge('e2', 'split', 'b'), edge('e3', 'a', 'join'), edge('e4', 'b', 'join')], } spreadBranchNodes(graph) expect(graph.nodes[0].x).toBe(130) expect(graph.nodes[3].x).toBe(130) }) it('主干排成两列时拉直成一条竖线,挂在主干上的旁支跟着搬', () => { // dagre 的常见输出:并行段前面那段主干在 0,并行段之后在 260 const graph = { nodes: [ node('start', 'fms-start', 0, 0, 52, 52), node('check', 'fms-user-task', 0, 200, 160, 48), node('gm', 'fms-user-task', 234, 300, 160, 48), node('split', 'fms-parallel-gateway', 260, 400, 68, 68), node('a', 'fms-user-task', 0, 600, 160, 48), node('b', 'fms-user-task', 260, 600, 160, 48), node('c', 'fms-user-task', 520, 600, 160, 48), node('join', 'fms-parallel-gateway', 260, 800, 68, 68), node('end', 'fms-end', 260, 1000, 52, 52), ], edges: [ edge('e1', 'start', 'check'), edge('e2', 'check', 'split'), edge('e3', 'check', 'gm'), edge('e4', 'gm', 'split'), edge('e5', 'split', 'a'), edge('e6', 'split', 'b'), edge('e7', 'split', 'c'), edge('e8', 'a', 'join'), edge('e9', 'b', 'join'), edge('e10', 'c', 'join'), edge('e11', 'join', 'end'), ], } spreadBranchNodes(graph, { defaultExitTargets: defaultExits({ check: ['split'] }) }) // 主轴取主干 x 的中位数 = 260(并行段中心),前半段整段拉过来 expect(graph.nodes.map((item) => [item.id, item.x])).toEqual([ ['start', 260], ['check', 260], // 旁支跟着主干走,保持它相对主干的偏移(234 - 0) ['gm', 494], ['split', 260], ['a', 0], ['b', 260], ['c', 520], ['join', 260], ['end', 260], ]) }) it('条件网关的出线落在不同层时不居中(默认出口继续直下)', () => { const graph = { nodes: [ node('gateway', 'fms-exclusive-gateway', 0, 0, 68, 68), node('branch', 'fms-user-task', 300, 200, 160, 48), node('next', 'fms-user-task', 0, 400, 160, 48), ], edges: [ edge('e1', 'gateway', 'branch'), edge('e2', 'gateway', 'next'), edge('e3', 'branch', 'next'), ], } spreadBranchNodes(graph, { defaultExitTargets: defaultExits({ gateway: ['next'] }) }) expect(graph.nodes[0].x).toBe(0) }) })