@zombie-mermaid/ascii-renderer 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +5392 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/__tests__/ascii-arrowhead-direction-1083.test.ts +51 -0
- package/src/__tests__/ascii-canvas-first-claim-wins-1093.test.ts +89 -0
- package/src/__tests__/ascii-canvas-size-offset-1093.test.ts +95 -0
- package/src/__tests__/ascii-canvas-write.test.ts +132 -0
- package/src/__tests__/ascii-chain-edge-overlap-1067.test.ts +100 -0
- package/src/__tests__/ascii-charset-border-junctions.test.ts +63 -0
- package/src/__tests__/ascii-cjk-width.test.ts +150 -0
- package/src/__tests__/ascii-class-box-occupancy.test.ts +462 -0
- package/src/__tests__/ascii-class-column-width-488-489.test.ts +639 -0
- package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +187 -0
- package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +114 -0
- package/src/__tests__/ascii-class-diagram-compartments.test.ts +70 -0
- package/src/__tests__/ascii-class-label-box-collision.test.ts +60 -0
- package/src/__tests__/ascii-class-label-row-collision-531.test.ts +135 -0
- package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +57 -0
- package/src/__tests__/ascii-class-padding.test.ts +104 -0
- package/src/__tests__/ascii-class-parent-alignment-971.test.ts +139 -0
- package/src/__tests__/ascii-class-parent-alignment-972.test.ts +208 -0
- package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +169 -0
- package/src/__tests__/ascii-combining-mark-width.test.ts +70 -0
- package/src/__tests__/ascii-coords-overlay.test.ts +64 -0
- package/src/__tests__/ascii-decision-lr-box-start.test.ts +106 -0
- package/src/__tests__/ascii-display-width-unit.test.ts +154 -0
- package/src/__tests__/ascii-draw-arrows-coverage.test.ts +224 -0
- package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +105 -0
- package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +72 -0
- package/src/__tests__/ascii-edge-ending-glyphs.test.ts +206 -0
- package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +95 -0
- package/src/__tests__/ascii-edge-routing-fixes.test.ts +178 -0
- package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +103 -0
- package/src/__tests__/ascii-edge-style-consistency-1067.test.ts +89 -0
- package/src/__tests__/ascii-edge-styles.test.ts +149 -0
- package/src/__tests__/ascii-emoji-cluster-width.test.ts +101 -0
- package/src/__tests__/ascii-er-box-occupancy.test.ts +251 -0
- package/src/__tests__/ascii-er-cardinality.test.ts +40 -0
- package/src/__tests__/ascii-er-corner-glyphs.test.ts +249 -0
- package/src/__tests__/ascii-er-jog-stray-line.test.ts +111 -0
- package/src/__tests__/ascii-er-label-padding.test.ts +118 -0
- package/src/__tests__/ascii-er-padding.test.ts +114 -0
- package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +458 -0
- package/src/__tests__/ascii-er-relationship-overwrite.test.ts +325 -0
- package/src/__tests__/ascii-er-stray-connectors.test.ts +344 -0
- package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +98 -0
- package/src/__tests__/ascii-er-vertical-one-marker.test.ts +110 -0
- package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +168 -0
- package/src/__tests__/ascii-lane-search.test.ts +207 -0
- package/src/__tests__/ascii-multibox-cjk-width.test.ts +184 -0
- package/src/__tests__/ascii-multiline.test.ts +288 -0
- package/src/__tests__/ascii-padding-edge-cases.test.ts +154 -0
- package/src/__tests__/ascii-pathfinder-route-edge.test.ts +184 -0
- package/src/__tests__/ascii-sequence-alt-else-label.test.ts +236 -0
- package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +217 -0
- package/src/__tests__/ascii-sequence-box-group.test.ts +159 -0
- package/src/__tests__/ascii-sequence-cjk-width.test.ts +235 -0
- package/src/__tests__/ascii-sequence-create-destroy.test.ts +114 -0
- package/src/__tests__/ascii-sequence-form-invariants.test.ts +432 -0
- package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +219 -0
- package/src/__tests__/ascii-sequence-notes.test.ts +61 -0
- package/src/__tests__/ascii-sequence-padding.test.ts +119 -0
- package/src/__tests__/ascii-sequence-self-arrow.test.ts +206 -0
- package/src/__tests__/ascii-shape-diamond.test.ts +38 -0
- package/src/__tests__/ascii-shape-rectangle.test.ts +258 -0
- package/src/__tests__/ascii-shape-rounded.test.ts +36 -0
- package/src/__tests__/ascii-shapes-circle.test.ts +41 -0
- package/src/__tests__/ascii-shapes-hexagon.test.ts +42 -0
- package/src/__tests__/ascii-shapes-special.test.ts +344 -0
- package/src/__tests__/ascii-shapes-stadium.test.ts +217 -0
- package/src/__tests__/ascii-shapes-state.test.ts +224 -0
- package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +130 -0
- package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +90 -0
- package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +152 -0
- package/src/__tests__/ascii-subgraph-title-padding.test.ts +77 -0
- package/src/__tests__/ascii-territory-unit.test.ts +219 -0
- package/src/__tests__/ascii-validate.test.ts +220 -0
- package/src/__tests__/ascii.test.ts +325 -0
- package/src/__tests__/class-arrow-directions.test.ts +505 -0
- package/src/__tests__/draw-lines.test.ts +93 -0
- package/src/__tests__/edge-cell-styles.test.ts +278 -0
- package/src/__tests__/grid-occupancy.test.ts +240 -0
- package/src/__tests__/helpers/ascii-form.ts +142 -0
- package/src/__tests__/helpers/terminal-display-width.ts +74 -0
- package/src/__tests__/pathfinder.test.ts +239 -0
- package/src/__tests__/testdata/ascii/ampersand_lhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_lhs_and_rhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_rhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_td_fanin.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_td_fanout.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_without_edge.txt +18 -0
- package/src/__tests__/testdata/ascii/back_reference_from_child.txt +10 -0
- package/src/__tests__/testdata/ascii/backlink_from_bottom.txt +22 -0
- package/src/__tests__/testdata/ascii/backlink_from_top.txt +22 -0
- package/src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt +20 -0
- package/src/__tests__/testdata/ascii/cls_all_relationships.txt +19 -0
- package/src/__tests__/testdata/ascii/cls_annotation.txt +29 -0
- package/src/__tests__/testdata/ascii/cls_association.txt +14 -0
- package/src/__tests__/testdata/ascii/cls_basic.txt +15 -0
- package/src/__tests__/testdata/ascii/cls_dependency.txt +14 -0
- package/src/__tests__/testdata/ascii/cls_inheritance.txt +20 -0
- package/src/__tests__/testdata/ascii/cls_methods.txt +21 -0
- package/src/__tests__/testdata/ascii/comments.txt +23 -0
- package/src/__tests__/testdata/ascii/custom_padding.txt +10 -0
- package/src/__tests__/testdata/ascii/duplicate_labels.txt +19 -0
- package/src/__tests__/testdata/ascii/er_attributes.txt +21 -0
- package/src/__tests__/testdata/ascii/er_basic.txt +8 -0
- package/src/__tests__/testdata/ascii/er_identifying.txt +18 -0
- package/src/__tests__/testdata/ascii/flowchart_tb_simple.txt +29 -0
- package/src/__tests__/testdata/ascii/graph_bt_direction.txt +28 -0
- package/src/__tests__/testdata/ascii/graph_tb_direction.txt +26 -0
- package/src/__tests__/testdata/ascii/nested_subgraphs_with_labels.txt +36 -0
- package/src/__tests__/testdata/ascii/preserve_order_of_definition.txt +23 -0
- package/src/__tests__/testdata/ascii/self_reference.txt +10 -0
- package/src/__tests__/testdata/ascii/self_reference_with_edge.txt +10 -0
- package/src/__tests__/testdata/ascii/seq_basic.txt +17 -0
- package/src/__tests__/testdata/ascii/seq_multiple_messages.txt +25 -0
- package/src/__tests__/testdata/ascii/seq_self_message.txt +18 -0
- package/src/__tests__/testdata/ascii/single_node.txt +8 -0
- package/src/__tests__/testdata/ascii/single_node_longer_name.txt +8 -0
- package/src/__tests__/testdata/ascii/subgraph_complex_mixed.txt +38 -0
- package/src/__tests__/testdata/ascii/subgraph_complex_nested.txt +49 -0
- package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +47 -0
- package/src/__tests__/testdata/ascii/subgraph_empty.txt +10 -0
- package/src/__tests__/testdata/ascii/subgraph_mixed_nodes.txt +20 -0
- package/src/__tests__/testdata/ascii/subgraph_mixed_nodes_td.txt +48 -0
- package/src/__tests__/testdata/ascii/subgraph_multiple_edges.txt +32 -0
- package/src/__tests__/testdata/ascii/subgraph_multiple_nodes.txt +16 -0
- package/src/__tests__/testdata/ascii/subgraph_nested.txt +24 -0
- package/src/__tests__/testdata/ascii/subgraph_nested_with_external.txt +30 -0
- package/src/__tests__/testdata/ascii/subgraph_node_outside_lr.txt +17 -0
- package/src/__tests__/testdata/ascii/subgraph_single_node.txt +16 -0
- package/src/__tests__/testdata/ascii/subgraph_td_direction.txt +26 -0
- package/src/__tests__/testdata/ascii/subgraph_td_multiple.txt +44 -0
- package/src/__tests__/testdata/ascii/subgraph_td_multiple_paddingy.txt +42 -0
- package/src/__tests__/testdata/ascii/subgraph_three_levels_nested.txt +32 -0
- package/src/__tests__/testdata/ascii/subgraph_three_separate.txt +24 -0
- package/src/__tests__/testdata/ascii/subgraph_two_separate.txt +20 -0
- package/src/__tests__/testdata/ascii/subgraph_with_labels.txt +20 -0
- package/src/__tests__/testdata/ascii/three_nodes.txt +9 -0
- package/src/__tests__/testdata/ascii/three_nodes_single_line.txt +8 -0
- package/src/__tests__/testdata/ascii/two_layer_single_graph.txt +19 -0
- package/src/__tests__/testdata/ascii/two_layer_single_graph_longer_names.txt +19 -0
- package/src/__tests__/testdata/ascii/two_nodes_linked.txt +8 -0
- package/src/__tests__/testdata/ascii/two_nodes_longer_names.txt +8 -0
- package/src/__tests__/testdata/ascii/two_root_nodes.txt +19 -0
- package/src/__tests__/testdata/ascii/two_root_nodes_longer_names.txt +19 -0
- package/src/__tests__/testdata/ascii/two_single_root_nodes.txt +19 -0
- package/src/__tests__/testdata/unicode/ampersand_lhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_lhs_and_rhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_rhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_without_edge.txt +18 -0
- package/src/__tests__/testdata/unicode/back_reference_from_child.txt +10 -0
- package/src/__tests__/testdata/unicode/backlink_from_bottom.txt +22 -0
- package/src/__tests__/testdata/unicode/backlink_from_top.txt +22 -0
- package/src/__tests__/testdata/unicode/cls_all_relationships.txt +19 -0
- package/src/__tests__/testdata/unicode/cls_annotation.txt +29 -0
- package/src/__tests__/testdata/unicode/cls_association.txt +14 -0
- package/src/__tests__/testdata/unicode/cls_basic.txt +15 -0
- package/src/__tests__/testdata/unicode/cls_dependency.txt +14 -0
- package/src/__tests__/testdata/unicode/cls_inheritance.txt +20 -0
- package/src/__tests__/testdata/unicode/cls_methods.txt +21 -0
- package/src/__tests__/testdata/unicode/comments.txt +23 -0
- package/src/__tests__/testdata/unicode/duplicate_labels.txt +19 -0
- package/src/__tests__/testdata/unicode/er_attributes.txt +21 -0
- package/src/__tests__/testdata/unicode/er_basic.txt +8 -0
- package/src/__tests__/testdata/unicode/er_identifying.txt +18 -0
- package/src/__tests__/testdata/unicode/graph_bt_direction.txt +28 -0
- package/src/__tests__/testdata/unicode/preserve_order_of_definition.txt +23 -0
- package/src/__tests__/testdata/unicode/self_reference.txt +10 -0
- package/src/__tests__/testdata/unicode/self_reference_with_edge.txt +10 -0
- package/src/__tests__/testdata/unicode/seq_basic.txt +17 -0
- package/src/__tests__/testdata/unicode/seq_multiple_messages.txt +25 -0
- package/src/__tests__/testdata/unicode/seq_self_message.txt +18 -0
- package/src/__tests__/testdata/unicode/single_node.txt +8 -0
- package/src/__tests__/testdata/unicode/single_node_longer_name.txt +8 -0
- package/src/__tests__/testdata/unicode/three_nodes.txt +9 -0
- package/src/__tests__/testdata/unicode/three_nodes_single_line.txt +8 -0
- package/src/__tests__/testdata/unicode/two_layer_single_graph.txt +19 -0
- package/src/__tests__/testdata/unicode/two_layer_single_graph_longer_names.txt +19 -0
- package/src/__tests__/testdata/unicode/two_nodes_linked.txt +8 -0
- package/src/__tests__/testdata/unicode/two_nodes_longer_names.txt +8 -0
- package/src/__tests__/testdata/unicode/two_root_nodes.txt +19 -0
- package/src/__tests__/testdata/unicode/two_root_nodes_longer_names.txt +19 -0
- package/src/__tests__/testdata/unicode/two_single_root_nodes.txt +19 -0
- package/src/__tests__/xychart-ascii.test.ts +376 -0
- package/src/ansi.ts +490 -0
- package/src/canvas.ts +757 -0
- package/src/class-diagram.ts +2001 -0
- package/src/converter.ts +446 -0
- package/src/coords.ts +58 -0
- package/src/display-width.ts +151 -0
- package/src/draw-arrows.ts +593 -0
- package/src/draw-boxes.ts +267 -0
- package/src/draw-bundles.ts +611 -0
- package/src/draw-lines.ts +174 -0
- package/src/draw-subgraphs.ts +108 -0
- package/src/draw.ts +350 -0
- package/src/edge-bundling.ts +435 -0
- package/src/edge-cell-styles.ts +209 -0
- package/src/edge-routing.ts +1070 -0
- package/src/er-diagram.ts +1488 -0
- package/src/flowchart.ts +94 -0
- package/src/grid-occupancy.ts +234 -0
- package/src/grid.ts +1309 -0
- package/src/hyperlinks.ts +248 -0
- package/src/index.ts +163 -0
- package/src/lane-search.ts +68 -0
- package/src/multiline-utils.ts +82 -0
- package/src/pathfinder.ts +448 -0
- package/src/registry.ts +88 -0
- package/src/sequence.ts +1318 -0
- package/src/shapes/circle.ts +31 -0
- package/src/shapes/corners.ts +273 -0
- package/src/shapes/diamond.ts +31 -0
- package/src/shapes/hexagon.ts +35 -0
- package/src/shapes/index.ts +123 -0
- package/src/shapes/rectangle.ts +199 -0
- package/src/shapes/rounded.ts +31 -0
- package/src/shapes/special.ts +360 -0
- package/src/shapes/stadium.ts +122 -0
- package/src/shapes/state.ts +204 -0
- package/src/shapes/types.ts +78 -0
- package/src/territory.ts +136 -0
- package/src/types.ts +454 -0
- package/src/validate.ts +189 -0
- package/src/xychart.ts +1085 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct coverage for pathfinder.ts's routeEdge (and the tryDirectPath fast
|
|
3
|
+
* path it wraps) — previously untested (PR #178 review). routeEdge is the
|
|
4
|
+
* shared seam edge-routing.ts's determinePath and edge-bundling.ts's
|
|
5
|
+
* routeBundledEdges both route through: try an unobstructed direct L-shaped
|
|
6
|
+
* path first, falling back to A* search, and require a render-wide
|
|
7
|
+
* pathBudget rather than silently degrading when one is absent.
|
|
8
|
+
*
|
|
9
|
+
* The "try both L orientations" behavior below is the fix for the
|
|
10
|
+
* arrival-vs-departure direction bug the review found: `dir` picks a
|
|
11
|
+
* *preferred* corner order, but when that orientation is blocked the other
|
|
12
|
+
* orientation is tried too, so routeEdge produces a correct direct path
|
|
13
|
+
* even when a caller's `dir` reflects an arrival anchor rather than the
|
|
14
|
+
* true departure direction (see edge-bundling.ts's routeBundledEdges).
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect } from 'vitest'
|
|
17
|
+
import { routeEdge, createPathBudget } from '../pathfinder.ts'
|
|
18
|
+
import {
|
|
19
|
+
Up,
|
|
20
|
+
Down,
|
|
21
|
+
Left,
|
|
22
|
+
Right,
|
|
23
|
+
Middle,
|
|
24
|
+
UpperRight,
|
|
25
|
+
requireCardinalDirection,
|
|
26
|
+
} from '../types.ts'
|
|
27
|
+
import type { AsciiGraph, PathBudget } from '../types.ts'
|
|
28
|
+
import { createGrid, placeBlock, type Grid } from '../grid-occupancy.ts'
|
|
29
|
+
|
|
30
|
+
function makeGraph(grid: Grid, pathBudget: PathBudget | undefined): AsciiGraph {
|
|
31
|
+
return {
|
|
32
|
+
nodes: [],
|
|
33
|
+
edges: [],
|
|
34
|
+
canvas: [],
|
|
35
|
+
roleCanvas: [],
|
|
36
|
+
grid,
|
|
37
|
+
columnWidth: new Map(),
|
|
38
|
+
rowHeight: new Map(),
|
|
39
|
+
subgraphs: [],
|
|
40
|
+
config: {
|
|
41
|
+
useAscii: false,
|
|
42
|
+
paddingX: 5,
|
|
43
|
+
paddingY: 5,
|
|
44
|
+
boxBorderPadding: 1,
|
|
45
|
+
graphDirection: 'TD',
|
|
46
|
+
},
|
|
47
|
+
offsetX: 0,
|
|
48
|
+
offsetY: 0,
|
|
49
|
+
bundles: [],
|
|
50
|
+
pathBudget,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('routeEdge', () => {
|
|
55
|
+
it('takes the horizontal-first direct L when dir is Right', () => {
|
|
56
|
+
const graph = makeGraph(createGrid(), createPathBudget())
|
|
57
|
+
const path = routeEdge(
|
|
58
|
+
graph,
|
|
59
|
+
{ x: 0, y: 0 },
|
|
60
|
+
{ x: 4, y: 4 },
|
|
61
|
+
requireCardinalDirection(Right),
|
|
62
|
+
)
|
|
63
|
+
expect(path).toEqual([
|
|
64
|
+
{ x: 0, y: 0 },
|
|
65
|
+
{ x: 4, y: 0 },
|
|
66
|
+
{ x: 4, y: 4 },
|
|
67
|
+
])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('takes the vertical-first direct L when dir is Down', () => {
|
|
71
|
+
const graph = makeGraph(createGrid(), createPathBudget())
|
|
72
|
+
const path = routeEdge(
|
|
73
|
+
graph,
|
|
74
|
+
{ x: 0, y: 0 },
|
|
75
|
+
{ x: 4, y: 4 },
|
|
76
|
+
requireCardinalDirection(Down),
|
|
77
|
+
)
|
|
78
|
+
expect(path).toEqual([
|
|
79
|
+
{ x: 0, y: 0 },
|
|
80
|
+
{ x: 0, y: 4 },
|
|
81
|
+
{ x: 4, y: 4 },
|
|
82
|
+
])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('falls back to the other L orientation when the preferred one is blocked', () => {
|
|
86
|
+
// dir=Right prefers the {4,0} corner; occupy it so only the {0,4}
|
|
87
|
+
// corner (vertical-first) is clear. This is exactly the scenario the
|
|
88
|
+
// arrival-vs-departure bug hit: a caller passes a `dir` that doesn't
|
|
89
|
+
// match the true departure axis, but the direct path is still found
|
|
90
|
+
// because both orientations are tried.
|
|
91
|
+
const grid = createGrid()
|
|
92
|
+
placeBlock(grid, { x: 4, y: 0 }, 1)
|
|
93
|
+
const graph = makeGraph(grid, createPathBudget())
|
|
94
|
+
const path = routeEdge(
|
|
95
|
+
graph,
|
|
96
|
+
{ x: 0, y: 0 },
|
|
97
|
+
{ x: 4, y: 4 },
|
|
98
|
+
requireCardinalDirection(Right),
|
|
99
|
+
)
|
|
100
|
+
expect(path).toEqual([
|
|
101
|
+
{ x: 0, y: 0 },
|
|
102
|
+
{ x: 0, y: 4 },
|
|
103
|
+
{ x: 4, y: 4 },
|
|
104
|
+
])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('falls back to A* when both direct L orientations are blocked', () => {
|
|
108
|
+
const grid = createGrid()
|
|
109
|
+
placeBlock(grid, { x: 4, y: 0 }, 1)
|
|
110
|
+
placeBlock(grid, { x: 0, y: 4 }, 1)
|
|
111
|
+
const graph = makeGraph(grid, createPathBudget())
|
|
112
|
+
const path = routeEdge(
|
|
113
|
+
graph,
|
|
114
|
+
{ x: 0, y: 0 },
|
|
115
|
+
{ x: 4, y: 4 },
|
|
116
|
+
requireCardinalDirection(Right),
|
|
117
|
+
)
|
|
118
|
+
// Not a 3-point direct L — A* found a route around the two blockers.
|
|
119
|
+
expect(path).not.toBeNull()
|
|
120
|
+
expect(path!.length).toBeGreaterThan(3)
|
|
121
|
+
expect(path![0]).toEqual({ x: 0, y: 0 })
|
|
122
|
+
expect(path![path!.length - 1]).toEqual({ x: 4, y: 4 })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('falls back to A* (trivial straight line) when from/to are already axis-aligned', () => {
|
|
126
|
+
const graph = makeGraph(createGrid(), createPathBudget())
|
|
127
|
+
const path = routeEdge(
|
|
128
|
+
graph,
|
|
129
|
+
{ x: 0, y: 0 },
|
|
130
|
+
{ x: 0, y: 6 },
|
|
131
|
+
requireCardinalDirection(Down),
|
|
132
|
+
)
|
|
133
|
+
expect(path).toEqual([
|
|
134
|
+
{ x: 0, y: 0 },
|
|
135
|
+
{ x: 0, y: 6 },
|
|
136
|
+
])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('returns null when the destination is unreachable through free cells', () => {
|
|
140
|
+
// Occupy all 4 neighbors of the destination — no legal final hop exists
|
|
141
|
+
// from any direction, so neither the direct path nor A* can reach it.
|
|
142
|
+
const grid = createGrid()
|
|
143
|
+
placeBlock(grid, { x: 9, y: 10 }, 1)
|
|
144
|
+
placeBlock(grid, { x: 11, y: 10 }, 1)
|
|
145
|
+
placeBlock(grid, { x: 10, y: 9 }, 1)
|
|
146
|
+
placeBlock(grid, { x: 10, y: 11 }, 1)
|
|
147
|
+
const graph = makeGraph(grid, createPathBudget())
|
|
148
|
+
const path = routeEdge(
|
|
149
|
+
graph,
|
|
150
|
+
{ x: 0, y: 0 },
|
|
151
|
+
{ x: 10, y: 10 },
|
|
152
|
+
requireCardinalDirection(Right),
|
|
153
|
+
)
|
|
154
|
+
expect(path).toBeNull()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('throws when graph.pathBudget is not set, instead of silently searching unbudgeted', () => {
|
|
158
|
+
const graph = makeGraph(createGrid(), undefined)
|
|
159
|
+
expect(() =>
|
|
160
|
+
routeEdge(
|
|
161
|
+
graph,
|
|
162
|
+
{ x: 0, y: 0 },
|
|
163
|
+
{ x: 4, y: 4 },
|
|
164
|
+
requireCardinalDirection(Right),
|
|
165
|
+
),
|
|
166
|
+
).toThrow(/pathBudget/)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('requireCardinalDirection', () => {
|
|
171
|
+
it('passes through each of the 4 cardinal directions', () => {
|
|
172
|
+
for (const d of [Up, Down, Left, Right]) {
|
|
173
|
+
expect(requireCardinalDirection(d)).toBe(d)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('throws for a diagonal direction', () => {
|
|
178
|
+
expect(() => requireCardinalDirection(UpperRight)).toThrow(/cardinal/)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('throws for Middle', () => {
|
|
182
|
+
expect(() => requireCardinalDirection(Middle)).toThrow(/cardinal/)
|
|
183
|
+
})
|
|
184
|
+
})
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII sequence diagram — alt/else block label truncation regression tests
|
|
3
|
+
// (issue #352)
|
|
4
|
+
//
|
|
5
|
+
// The alt/loop/opt/par block's wall (bLeft/bRight) was sized purely from the
|
|
6
|
+
// span of the lifelines its messages touch (plus self-arrow extent, fixed
|
|
7
|
+
// for #68). Header ("alt [label]") and divider ("[else label]") text is
|
|
8
|
+
// drawn starting at bLeft + 1 and hard-clipped at bRight — so a label longer
|
|
9
|
+
// than that message-driven span lost its tail with no ellipsis or other
|
|
10
|
+
// marker, and because the cut lands mid-word the truncated text can still
|
|
11
|
+
// read as a complete (but wrong) phrase, e.g. "alt [credentials" instead of
|
|
12
|
+
// "alt [credentials valid]".
|
|
13
|
+
//
|
|
14
|
+
// The fix measures the longest label among the block's header and every
|
|
15
|
+
// divider up front and widens the wall (growing the canvas too, if needed)
|
|
16
|
+
// to fit it, mirroring the self-arrow widening done for #68.
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect } from 'vitest'
|
|
20
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
21
|
+
|
|
22
|
+
describe('ASCII sequence diagrams – alt/else block labels (issue #352)', () => {
|
|
23
|
+
it('does not truncate a long alt/else label mid-word (exact issue repro)', () => {
|
|
24
|
+
const result = renderMermaidASCII(
|
|
25
|
+
`
|
|
26
|
+
sequenceDiagram
|
|
27
|
+
participant A
|
|
28
|
+
participant B
|
|
29
|
+
participant C
|
|
30
|
+
alt credentials valid
|
|
31
|
+
A->>B: x
|
|
32
|
+
else credentials rejected
|
|
33
|
+
A->>B: y
|
|
34
|
+
end
|
|
35
|
+
`,
|
|
36
|
+
{ useAscii: false },
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
// Full, unclipped labels must appear.
|
|
40
|
+
expect(result).toContain('alt [credentials valid]')
|
|
41
|
+
expect(result).toContain('[credentials rejected]')
|
|
42
|
+
|
|
43
|
+
// The exact mid-word-truncated strings from the issue must be gone —
|
|
44
|
+
// checked with a trailing wall/junction character so a match against
|
|
45
|
+
// the (correct) full label's shared prefix doesn't false-positive.
|
|
46
|
+
expect(result).not.toContain('┌alt [credentials┐')
|
|
47
|
+
expect(result).not.toContain('[credentials rej┤')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('does not let the widened wall swallow an uninvolved participant lifeline (#387)', () => {
|
|
51
|
+
// C has no messages inside the block, so widening the wall to fit the
|
|
52
|
+
// long label must push C's lifeline clear of it rather than land on
|
|
53
|
+
// top of/next to it. Before this fix, the wall's right edge landed one
|
|
54
|
+
// column past C's already-placed lifeline, drawing the block's own
|
|
55
|
+
// wall and C's lifeline back-to-back on every interior row with no gap
|
|
56
|
+
// between them — two adjacent "│" characters that read as one thing
|
|
57
|
+
// enclosing a participant the block has nothing to do with.
|
|
58
|
+
const result = renderMermaidASCII(
|
|
59
|
+
`
|
|
60
|
+
sequenceDiagram
|
|
61
|
+
participant A
|
|
62
|
+
participant B
|
|
63
|
+
participant C
|
|
64
|
+
alt credentials valid
|
|
65
|
+
A->>B: x
|
|
66
|
+
else credentials rejected
|
|
67
|
+
A->>B: y
|
|
68
|
+
end
|
|
69
|
+
`,
|
|
70
|
+
{ useAscii: false },
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
for (const line of result.split('\n')) {
|
|
74
|
+
expect(line).not.toMatch(/││/)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('leaves short labels that already fit unchanged (no regression)', () => {
|
|
79
|
+
const result = renderMermaidASCII(
|
|
80
|
+
`
|
|
81
|
+
sequenceDiagram
|
|
82
|
+
participant A
|
|
83
|
+
participant B
|
|
84
|
+
participant C
|
|
85
|
+
alt yes
|
|
86
|
+
A->>B: x
|
|
87
|
+
else no
|
|
88
|
+
A->>B: y
|
|
89
|
+
end
|
|
90
|
+
`,
|
|
91
|
+
{ useAscii: false },
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
// Matches the issue's own "short labels, for reference" expected output.
|
|
95
|
+
expect(result).toContain('alt [yes]───────┐')
|
|
96
|
+
expect(result).toContain('[no]╌╌╌╌╌╌╌╌╌╌╌╌┤')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('widens the wall for a very long single-condition label', () => {
|
|
100
|
+
const longLabel =
|
|
101
|
+
'this condition label is extremely long and needs the canvas to grow to fit it properly'
|
|
102
|
+
const result = renderMermaidASCII(
|
|
103
|
+
`
|
|
104
|
+
sequenceDiagram
|
|
105
|
+
participant A
|
|
106
|
+
participant B
|
|
107
|
+
alt ${longLabel}
|
|
108
|
+
A->>B: x
|
|
109
|
+
end
|
|
110
|
+
`,
|
|
111
|
+
{ useAscii: false },
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
expect(result).toContain(`alt [${longLabel}]`)
|
|
115
|
+
|
|
116
|
+
// The header line's own right wall must close immediately after the
|
|
117
|
+
// label — proves the wall grew to fit rather than the label being cut.
|
|
118
|
+
const headerLine = result
|
|
119
|
+
.split('\n')
|
|
120
|
+
.find((l) => l.includes(`alt [${longLabel}]`))
|
|
121
|
+
expect(headerLine).toBeDefined()
|
|
122
|
+
expect(headerLine!.endsWith('┐')).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('widens the wall to fit the longest of several else branches with varying label lengths', () => {
|
|
126
|
+
const result = renderMermaidASCII(
|
|
127
|
+
`
|
|
128
|
+
sequenceDiagram
|
|
129
|
+
participant A
|
|
130
|
+
participant B
|
|
131
|
+
alt short
|
|
132
|
+
A->>B: w
|
|
133
|
+
else a moderately long condition
|
|
134
|
+
A->>B: x
|
|
135
|
+
else an even longer condition than the previous one
|
|
136
|
+
A->>B: y
|
|
137
|
+
else tiny
|
|
138
|
+
A->>B: z
|
|
139
|
+
end
|
|
140
|
+
`,
|
|
141
|
+
{ useAscii: false },
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
expect(result).toContain('alt [short]')
|
|
145
|
+
expect(result).toContain('[a moderately long condition]')
|
|
146
|
+
expect(result).toContain('[an even longer condition than the previous one]')
|
|
147
|
+
expect(result).toContain('[tiny]')
|
|
148
|
+
|
|
149
|
+
// None of the labels should be cut short — checked with the closing
|
|
150
|
+
// bracket so a match against the (correct) full label's shared prefix
|
|
151
|
+
// doesn't false-positive.
|
|
152
|
+
expect(result).not.toContain('[a moderately long conditio]')
|
|
153
|
+
expect(result).not.toContain('[an even longer condition than the previous]')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('widens loop/opt block walls too, not just alt', () => {
|
|
157
|
+
const loopResult = renderMermaidASCII(
|
|
158
|
+
`
|
|
159
|
+
sequenceDiagram
|
|
160
|
+
participant A
|
|
161
|
+
participant B
|
|
162
|
+
loop a fairly long loop condition label here
|
|
163
|
+
A->>B: x
|
|
164
|
+
end
|
|
165
|
+
`,
|
|
166
|
+
{ useAscii: false },
|
|
167
|
+
)
|
|
168
|
+
expect(loopResult).toContain(
|
|
169
|
+
'loop [a fairly long loop condition label here]',
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const optResult = renderMermaidASCII(
|
|
173
|
+
`
|
|
174
|
+
sequenceDiagram
|
|
175
|
+
participant A
|
|
176
|
+
participant B
|
|
177
|
+
opt a fairly long optional condition label here
|
|
178
|
+
A->>B: x
|
|
179
|
+
end
|
|
180
|
+
`,
|
|
181
|
+
{ useAscii: false },
|
|
182
|
+
)
|
|
183
|
+
expect(optResult).toContain(
|
|
184
|
+
'opt [a fairly long optional condition label here]',
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('handles a block with no header label at all (bare "par")', () => {
|
|
189
|
+
// `block.label` is '' (falsy) here — exercises the `block.label ? ... :
|
|
190
|
+
// block.type` branch that a labelled alt/loop never reaches.
|
|
191
|
+
const result = renderMermaidASCII(
|
|
192
|
+
`
|
|
193
|
+
sequenceDiagram
|
|
194
|
+
participant A
|
|
195
|
+
participant B
|
|
196
|
+
par
|
|
197
|
+
A->>B: x
|
|
198
|
+
and
|
|
199
|
+
A->>B: y
|
|
200
|
+
end
|
|
201
|
+
`,
|
|
202
|
+
{ useAscii: false },
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
expect(result).toContain('┌par')
|
|
206
|
+
expect(result).not.toContain('par [')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('handles a bare else divider with no label text', () => {
|
|
210
|
+
// `divider.label` is '' (falsy) here — exercises the `if (divider.label)`
|
|
211
|
+
// false branch that a labelled else never reaches.
|
|
212
|
+
const result = renderMermaidASCII(
|
|
213
|
+
`
|
|
214
|
+
sequenceDiagram
|
|
215
|
+
participant A
|
|
216
|
+
participant B
|
|
217
|
+
alt some long condition that needs the wall widened
|
|
218
|
+
A->>B: x
|
|
219
|
+
else
|
|
220
|
+
A->>B: y
|
|
221
|
+
end
|
|
222
|
+
`,
|
|
223
|
+
{ useAscii: false },
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
expect(result).toContain(
|
|
227
|
+
'alt [some long condition that needs the wall widened]',
|
|
228
|
+
)
|
|
229
|
+
// The bare divider must still render as a plain divider row — checked
|
|
230
|
+
// positively (a dashed line actually exists) as well as negatively (it
|
|
231
|
+
// carries no "[...]" label of its own). The negative check alone would
|
|
232
|
+
// still pass if the renderer silently dropped the divider entirely.
|
|
233
|
+
expect(result).toMatch(/├╌+┤/)
|
|
234
|
+
expect(result).not.toMatch(/├\[.*\]/)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII sequence diagram — block-wall lifeline clearance (issue #353)
|
|
3
|
+
//
|
|
4
|
+
// `loop`/`alt`/`opt`/etc. share a single wall-extent calculation
|
|
5
|
+
// (src/ascii/sequence.ts's "DRAW: blocks" pass): a fixed BLOCK_WALL_MARGIN
|
|
6
|
+
// padding around the leftmost/rightmost lifeline the block's own messages
|
|
7
|
+
// touch. That calculation only ever looks at lifelines the block's messages
|
|
8
|
+
// reach — so the fixed margin could coincidentally land a wall exactly on a
|
|
9
|
+
// *different*, untouched lifeline's column, subsuming it (wall glyph and
|
|
10
|
+
// lifeline glyph sharing the same cell) for the block's whole vertical span.
|
|
11
|
+
//
|
|
12
|
+
// This bit `alt` in the issue's repro (a diagram combining `loop`, `alt`,
|
|
13
|
+
// and `opt`, where only `alt` doesn't reach the diagram's rightmost
|
|
14
|
+
// lifeline) while `loop`/`opt` happened to clear it, because their own
|
|
15
|
+
// messages already reached that lifeline and so were naturally padded past
|
|
16
|
+
// it by the same margin. It is not a per-block-type bug: all three share
|
|
17
|
+
// identical wall-calculation code.
|
|
18
|
+
//
|
|
19
|
+
// The fix pulls a wall back short of an untouched lifeline rather than
|
|
20
|
+
// pushing it past one — verified against real mermaid.js's own SVG output
|
|
21
|
+
// for this exact diagram (see scripts/lib/real-mermaid.ts): `loop`/`opt`
|
|
22
|
+
// there enclose `Database` because their own messages touch it directly,
|
|
23
|
+
// but `alt` — which never messages `Database` — stops well short of it
|
|
24
|
+
// (124px of real clearance, not a few px of overshoot), even though
|
|
25
|
+
// `loop`/`opt` in the same diagram extend ~11px *past* Database's lifeline
|
|
26
|
+
// to enclose it. Real mermaid never widens a block's wall to enclose a
|
|
27
|
+
// lifeline its own messages don't touch. An earlier version of this test
|
|
28
|
+
// asserted the opposite (`alt` should match `loop`/`opt` by enclosing
|
|
29
|
+
// Database) — that was a reasonable-sounding but unverified guess at the
|
|
30
|
+
// "consistent" fix; it wasn't checked against real mermaid at the time.
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
import { describe, it, expect } from 'vitest'
|
|
34
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
35
|
+
|
|
36
|
+
/** Column indices (0-based) of every lifeline, read off the actor header's
|
|
37
|
+
* bottom border row (the one carrying `┬` junctions). */
|
|
38
|
+
function lifelineColumns(lines: string[]): number[] {
|
|
39
|
+
const row = lines[2]!
|
|
40
|
+
const cols: number[] = []
|
|
41
|
+
for (let i = 0; i < row.length; i++) if (row[i] === '┬') cols.push(i)
|
|
42
|
+
return cols
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Right-wall column of a block, read from the row containing `marker`
|
|
46
|
+
* (its own top-border/header row, e.g. "┌alt [...]" or a divider row like
|
|
47
|
+
* "├[else label]"). The block's own right corner/junction is the last
|
|
48
|
+
* box-drawing glyph on that row. */
|
|
49
|
+
function blockWallRightColumn(lines: string[], marker: string): number {
|
|
50
|
+
const row = lines.find((l) => l.includes(marker))
|
|
51
|
+
expect(row, `expected a line containing "${marker}"`).toBeDefined()
|
|
52
|
+
const candidates = ['┐', '┤', '┘']
|
|
53
|
+
let col = -1
|
|
54
|
+
for (const ch of candidates) {
|
|
55
|
+
const idx = row!.lastIndexOf(ch)
|
|
56
|
+
if (idx > col) col = idx
|
|
57
|
+
}
|
|
58
|
+
expect(col, `expected a right-wall glyph on line: ${row}`).toBeGreaterThan(-1)
|
|
59
|
+
return col
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('ASCII sequence diagrams – block wall lifeline clearance (issue #353)', () => {
|
|
63
|
+
it('clears the rightmost lifeline consistently across loop/alt/opt in the same diagram', () => {
|
|
64
|
+
const src = `
|
|
65
|
+
sequenceDiagram
|
|
66
|
+
participant U as User
|
|
67
|
+
participant C as Client
|
|
68
|
+
participant A as Auth Service
|
|
69
|
+
participant D as Database
|
|
70
|
+
|
|
71
|
+
U->>C: click sign in
|
|
72
|
+
C->>A: POST /authorize
|
|
73
|
+
|
|
74
|
+
loop token refresh
|
|
75
|
+
A->>A: validate signature<br/>and check expiry
|
|
76
|
+
A->>D: lookup session
|
|
77
|
+
D-->>A: session row
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
alt credentials valid
|
|
81
|
+
A->>A: mint access token<br/>mint refresh token
|
|
82
|
+
A-->>C: 200 + tokens
|
|
83
|
+
C-->>U: redirect to app
|
|
84
|
+
else credentials rejected
|
|
85
|
+
A->>A: increment<br/>failure counter
|
|
86
|
+
A-->>C: 401 unauthorized
|
|
87
|
+
C-->>U: show error
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
opt remember me
|
|
91
|
+
C->>A: POST /persist
|
|
92
|
+
A->>D: store long-lived token
|
|
93
|
+
end
|
|
94
|
+
`
|
|
95
|
+
const result = renderMermaidASCII(src, { useAscii: false })
|
|
96
|
+
const lines = result.split('\n')
|
|
97
|
+
|
|
98
|
+
const llCols = lifelineColumns(lines)
|
|
99
|
+
const rightmostLL = Math.max(...llCols) // Database
|
|
100
|
+
const authServiceLL = llCols[2]! // touched by alt directly
|
|
101
|
+
|
|
102
|
+
const loopRight = blockWallRightColumn(lines, 'loop [token refresh]')
|
|
103
|
+
const altRight = blockWallRightColumn(lines, 'alt [credentials valid]')
|
|
104
|
+
const optRight = blockWallRightColumn(lines, 'opt [remember me]')
|
|
105
|
+
|
|
106
|
+
// None of the three walls may land on any lifeline column — the
|
|
107
|
+
// original bug manifested as alt's right wall landing exactly on the
|
|
108
|
+
// Database lifeline's column.
|
|
109
|
+
for (const wallCol of [loopRight, altRight, optRight]) {
|
|
110
|
+
expect(llCols).not.toContain(wallCol)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// loop/opt both message Database directly, so both enclose it with
|
|
114
|
+
// clearance — matching real mermaid.
|
|
115
|
+
expect(loopRight).toBeGreaterThan(rightmostLL)
|
|
116
|
+
expect(optRight).toBeGreaterThan(rightmostLL)
|
|
117
|
+
|
|
118
|
+
// alt never messages Database — real mermaid stops alt's wall well
|
|
119
|
+
// short of it rather than widening to enclose it, so alt must NOT
|
|
120
|
+
// match loop/opt's right column here. It still clears the lifeline it
|
|
121
|
+
// does touch (Auth Service).
|
|
122
|
+
expect(altRight).toBeGreaterThan(authServiceLL)
|
|
123
|
+
expect(altRight).toBeLessThan(rightmostLL)
|
|
124
|
+
expect(altRight).not.toBe(loopRight)
|
|
125
|
+
expect(altRight).not.toBe(optRight)
|
|
126
|
+
|
|
127
|
+
// The Database lifeline must still be drawn as a lifeline (not
|
|
128
|
+
// subsumed into the alt block's wall) for a row inside the alt block's
|
|
129
|
+
// span, e.g. its "mint access token" self-arrow row.
|
|
130
|
+
const selfArrowRow = lines.find((l) => l.includes('mint access token'))
|
|
131
|
+
expect(selfArrowRow).toBeDefined()
|
|
132
|
+
expect(selfArrowRow![rightmostLL]).toBe('│')
|
|
133
|
+
// ...and that lifeline sits outside (to the right of) alt's own right
|
|
134
|
+
// wall on that row — two distinct glyphs, not one, and not enclosed.
|
|
135
|
+
expect(altRight).toBeLessThan(rightmostLL)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('keeps a consistent right wall across every else-branch of a multi-branch alt', () => {
|
|
139
|
+
// Branch labels/content of increasing width so a wall computed only
|
|
140
|
+
// from the *first* branch (rather than reused consistently across all
|
|
141
|
+
// of them) would drift from branch to branch.
|
|
142
|
+
const src = `
|
|
143
|
+
sequenceDiagram
|
|
144
|
+
participant A
|
|
145
|
+
participant B
|
|
146
|
+
participant C
|
|
147
|
+
|
|
148
|
+
alt first branch
|
|
149
|
+
A->>B: short
|
|
150
|
+
else second branch is quite a bit longer than the first
|
|
151
|
+
B->>C: also a fairly long message label here
|
|
152
|
+
else third
|
|
153
|
+
A->>A: a long self-arrow label in the third branch
|
|
154
|
+
end
|
|
155
|
+
`
|
|
156
|
+
const result = renderMermaidASCII(src, { useAscii: false })
|
|
157
|
+
const lines = result.split('\n')
|
|
158
|
+
const llCols = lifelineColumns(lines)
|
|
159
|
+
|
|
160
|
+
const headerRight = blockWallRightColumn(lines, 'alt [first branch]')
|
|
161
|
+
const else1Right = blockWallRightColumn(
|
|
162
|
+
lines,
|
|
163
|
+
'[second branch is quite a bit longer than the first]',
|
|
164
|
+
)
|
|
165
|
+
const else2Right = blockWallRightColumn(lines, '[third]')
|
|
166
|
+
|
|
167
|
+
// The wall stays at the same column for the header and every divider —
|
|
168
|
+
// it must be computed once from the whole block's extent, not
|
|
169
|
+
// per-branch (which would produce a jagged/misaligned right edge).
|
|
170
|
+
expect(else1Right).toBe(headerRight)
|
|
171
|
+
expect(else2Right).toBe(headerRight)
|
|
172
|
+
|
|
173
|
+
// And, as above, it must not coincide with any lifeline column.
|
|
174
|
+
for (const wallCol of [headerRight, else1Right, else2Right]) {
|
|
175
|
+
expect(llCols).not.toContain(wallCol)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('does not let the pulled-back wall overwrite a self-message label (CodeRabbit)', () => {
|
|
180
|
+
// A short self-arrow label on the only touched actor, with an
|
|
181
|
+
// untouched actor sitting just past the label's own extent: the
|
|
182
|
+
// pull-back fix above can compute a wall column that lands inside the
|
|
183
|
+
// self-arrow's own label span rather than after it, and the later
|
|
184
|
+
// block-border draw then silently overwrites the label's first
|
|
185
|
+
// character(s). B is untouched by the loop's only message (a self-arrow
|
|
186
|
+
// on A) and sits close enough (default lifeline spacing) to trigger the
|
|
187
|
+
// pull-back for a short label.
|
|
188
|
+
const src = `
|
|
189
|
+
sequenceDiagram
|
|
190
|
+
participant A
|
|
191
|
+
participant B
|
|
192
|
+
loop x
|
|
193
|
+
A->>A: abc
|
|
194
|
+
end
|
|
195
|
+
`
|
|
196
|
+
const result = renderMermaidASCII(src, { useAscii: false })
|
|
197
|
+
const lines = result.split('\n')
|
|
198
|
+
|
|
199
|
+
// The label must survive intact and contiguous — a pull-back that
|
|
200
|
+
// clips into it would replace its leading character(s) with the
|
|
201
|
+
// block's own border glyph instead.
|
|
202
|
+
const labelRow = lines.find((l) => l.includes('abc'))
|
|
203
|
+
expect(labelRow).toBeDefined()
|
|
204
|
+
|
|
205
|
+
// The block's own right wall must sit at or after the label's last
|
|
206
|
+
// column, never inside its span.
|
|
207
|
+
const llCols = lifelineColumns(lines)
|
|
208
|
+
const wallRight = blockWallRightColumn(lines, 'loop [x]')
|
|
209
|
+
const labelEndCol = labelRow!.lastIndexOf('abc') + 'abc'.length - 1
|
|
210
|
+
expect(wallRight).toBeGreaterThanOrEqual(labelEndCol)
|
|
211
|
+
// ...and strictly before B's own column — not just "not exactly on
|
|
212
|
+
// it," which would also pass if the wall were pushed past B instead
|
|
213
|
+
// (enclosing the untouched lifeline, the opposite regression this
|
|
214
|
+
// suite exists to catch).
|
|
215
|
+
expect(wallRight).toBeLessThan(llCols[1]!)
|
|
216
|
+
})
|
|
217
|
+
})
|