diff --git a/src/test/java/com/yunhai/flowcenter/service/DiagramLayoutServiceTest.java b/src/test/java/com/yunhai/flowcenter/service/DiagramLayoutServiceTest.java new file mode 100644 index 0000000..cb889b5 --- /dev/null +++ b/src/test/java/com/yunhai/flowcenter/service/DiagramLayoutServiceTest.java @@ -0,0 +1,147 @@ +package com.yunhai.flowcenter.service; + +import com.yunhai.flowcenter.engine.FlowDefModel; +import com.yunhai.flowcenter.model.FlowModels; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 布局单源 golden 用例(2026-10-03 三视图统一):PNG 与 DiagramVo.positions 共同消费这份几何。 + * 数值断言 ±1 容差(int 除法);关系断言(列距/居中/归零)与常量解耦,防调参误报。 + */ +class DiagramLayoutServiceTest { + + private final DiagramLayoutService service = new DiagramLayoutService(); + + private FlowModels.NodeVo node(String id, String type) { + FlowModels.NodeVo n = new FlowModels.NodeVo(); + n.setId(id); + n.setType(type); + n.setLabel(id); + return n; + } + + private FlowModels.EdgeVo edge(String from, String to) { + FlowModels.EdgeVo e = new FlowModels.EdgeVo(); + e.setFrom(from); + e.setTo(to); + return e; + } + + private Map layout(List nodes, FlowModels.EdgeVo... edges) { + return service.layout(nodes, new ArrayList<>(Arrays.asList(edges))); + } + + private int cy(int[] b) { + return b[1] + b[3] / 2; + } + + @Test + void linear_xPitchByColumn_andZeroMin() { + List nodes = new ArrayList<>(Arrays.asList( + node("start", FlowDefModel.TYPE_START), + node("a", FlowDefModel.TYPE_APPROVAL), + node("b", FlowDefModel.TYPE_APPROVAL), + node("end", FlowDefModel.TYPE_END))); + Map box = layout(nodes, edge("start", "a"), edge("a", "b"), edge("b", "end")); + for (FlowModels.NodeVo n : nodes) { + assertNotNull(box.get(n.getId()), "缺坐标:" + n.getId()); + } + // 同宽节点(审批→审批)x 差 = 列距 SLOT_W+GAP_X(±1 容差) + assertEquals(DiagramLayoutService.SLOT_W + DiagramLayoutService.GAP_X, box.get("b")[0] - box.get("a")[0], 1); + // 归零:全部坐标非负且最小 x/y 为 0 + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + for (int[] b : box.values()) { + assertTrue(b[0] >= 0 && b[1] >= 0); + minX = Math.min(minX, b[0]); + minY = Math.min(minY, b[1]); + } + assertEquals(0, minX); + assertEquals(0, minY); + } + + @Test + void conditionBranches_sameColumnBlockVerticallyCentered() { + List nodes = new ArrayList<>(Arrays.asList( + node("start", FlowDefModel.TYPE_START), + node("c", FlowDefModel.TYPE_CONDITION), + node("a", FlowDefModel.TYPE_APPROVAL), + node("b", FlowDefModel.TYPE_APPROVAL), + node("end", FlowDefModel.TYPE_END))); + Map box = layout(nodes, + edge("start", "c"), edge("c", "a"), edge("c", "b"), edge("a", "end"), edge("b", "end")); + // 两分支同列不同行,列块垂直居中:两盒中心均值 ≈ 内容区中线(单行列的中心同值) + assertEquals(box.get("a")[0], box.get("b")[0]); + assertTrue(box.get("a")[1] != box.get("b")[1]); + assertEquals(cy(box.get("c")), (cy(box.get("a")) + cy(box.get("b"))) / 2, 1); + } + + @Test + void parallelGroup_splitAndJoinShareCenterline() { + List nodes = new ArrayList<>(Arrays.asList( + node("start", FlowDefModel.TYPE_START), + node("s", FlowDefModel.TYPE_PARALLEL_SPLIT), + node("a", FlowDefModel.TYPE_APPROVAL), + node("b", FlowDefModel.TYPE_APPROVAL), + node("j", FlowDefModel.TYPE_PARALLEL_JOIN), + node("end", FlowDefModel.TYPE_END))); + Map box = layout(nodes, + edge("start", "s"), edge("s", "a"), edge("s", "b"), + edge("a", "j"), edge("b", "j"), edge("j", "end")); + // split 与 join 同一水平中线,且即两分支的对称轴(centerParallelGroups 生效) + assertEquals(cy(box.get("s")), cy(box.get("j")), 1); + assertEquals(cy(box.get("s")), (cy(box.get("a")) + cy(box.get("b"))) / 2, 1); + } + + @Test + void nestedParallel_groupsCentered_noNegativeY() { + List nodes = new ArrayList<>(Arrays.asList( + node("start", FlowDefModel.TYPE_START), + node("os", FlowDefModel.TYPE_PARALLEL_SPLIT), + node("is", FlowDefModel.TYPE_PARALLEL_SPLIT), + node("c", FlowDefModel.TYPE_APPROVAL), + node("p", FlowDefModel.TYPE_APPROVAL), + node("q", FlowDefModel.TYPE_APPROVAL), + node("ij", FlowDefModel.TYPE_PARALLEL_JOIN), + node("oj", FlowDefModel.TYPE_PARALLEL_JOIN), + node("end", FlowDefModel.TYPE_END))); + Map box = layout(nodes, + edge("start", "os"), + edge("os", "is"), edge("os", "c"), + edge("is", "p"), edge("is", "q"), + edge("p", "ij"), edge("q", "ij"), + edge("ij", "oj"), edge("c", "oj"), + edge("oj", "end")); + // 内层组:is 与 {p,q} 居中;外层组:os/oj 同中线即分支对称轴 + assertEquals(cy(box.get("is")), (cy(box.get("p")) + cy(box.get("q"))) / 2, 1); + assertEquals(cy(box.get("os")), cy(box.get("oj")), 1); + // 全图无 y<0(centerParallelGroups 末段抬回) + for (int[] b : box.values()) { + assertTrue(b[1] >= 0, "出现负 y:" + b[1]); + } + } + + @Test + void cycle_defensive_fallbackLevelStillPositionsAllNodes() { + List nodes = new ArrayList<>(Arrays.asList( + node("start", FlowDefModel.TYPE_START), + node("a", FlowDefModel.TYPE_APPROVAL), + node("b", FlowDefModel.TYPE_APPROVAL))); + Map box = assertDoesNotThrow(() -> layout(nodes, + edge("start", "a"), edge("a", "b"), edge("b", "a"))); + // 环兜底:不抛异常,所有节点都有坐标 + for (FlowModels.NodeVo n : nodes) { + assertNotNull(box.get(n.getId()), "缺坐标:" + n.getId()); + } + } +}