@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,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for the draw-arrows.ts sibling of the #153 bug fixed in
|
|
3
|
+
* edge-routing.ts's determineLabelLine (see
|
|
4
|
+
* ascii-edge-routing-single-point-path.test.ts).
|
|
5
|
+
*
|
|
6
|
+
* `pathfinder.ts`'s `getPath` can legitimately return a single-point path
|
|
7
|
+
* when a routed edge's preferred from/to grid coordinates coincide (e.g.
|
|
8
|
+
* closely-spaced/adjacent nodes). `drawArrow`'s `drawPath` helper only
|
|
9
|
+
* pushes a drawn line segment for `i >= 1` in `edge.path`, so a one-element
|
|
10
|
+
* path produces zero segments — and `drawArrow` used to index unconditionally
|
|
11
|
+
* into the first/last entries of the resulting (empty) `linesDrawn` /
|
|
12
|
+
* `lineDirs` arrays to draw the box-start connector and end arrowhead,
|
|
13
|
+
* throwing "Cannot read properties of undefined" instead of rendering.
|
|
14
|
+
*
|
|
15
|
+
* Fixed by skipping the box-start connector and end arrowhead when no line
|
|
16
|
+
* segment was drawn.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, it, expect } from 'vitest'
|
|
19
|
+
import { drawArrow } from '../draw-arrows.ts'
|
|
20
|
+
import { Down } from '../types.ts'
|
|
21
|
+
import { mkCanvas, mkRoleCanvas } from '../canvas.ts'
|
|
22
|
+
import type { AsciiEdge, AsciiGraph, AsciiNode } from '../types.ts'
|
|
23
|
+
import { createGrid } from '../grid-occupancy.ts'
|
|
24
|
+
|
|
25
|
+
function makeNode(name: string, x: number, y: number): AsciiNode {
|
|
26
|
+
return {
|
|
27
|
+
name,
|
|
28
|
+
displayLabel: name,
|
|
29
|
+
shape: 'rectangle',
|
|
30
|
+
index: 0,
|
|
31
|
+
gridCoord: { x, y },
|
|
32
|
+
drawingCoord: { x, y },
|
|
33
|
+
drawing: mkCanvas(2, 2),
|
|
34
|
+
drawn: false,
|
|
35
|
+
styleClassName: '',
|
|
36
|
+
styleClass: { name: '', styles: {} },
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeGraph(nodes: AsciiNode[]): AsciiGraph {
|
|
41
|
+
return {
|
|
42
|
+
nodes,
|
|
43
|
+
edges: [],
|
|
44
|
+
canvas: mkCanvas(10, 10),
|
|
45
|
+
roleCanvas: mkRoleCanvas(10, 10),
|
|
46
|
+
grid: createGrid(),
|
|
47
|
+
columnWidth: new Map(),
|
|
48
|
+
rowHeight: new Map(),
|
|
49
|
+
subgraphs: [],
|
|
50
|
+
config: {
|
|
51
|
+
useAscii: false,
|
|
52
|
+
paddingX: 5,
|
|
53
|
+
paddingY: 5,
|
|
54
|
+
boxBorderPadding: 1,
|
|
55
|
+
graphDirection: 'TD',
|
|
56
|
+
},
|
|
57
|
+
offsetX: 0,
|
|
58
|
+
offsetY: 0,
|
|
59
|
+
bundles: [],
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('drawArrow handles a single-point edge path', () => {
|
|
64
|
+
it('does not throw when edge.path has exactly one point', () => {
|
|
65
|
+
const from = makeNode('A', 0, 0)
|
|
66
|
+
const to = makeNode('B', 4, 0)
|
|
67
|
+
const graph = makeGraph([from, to])
|
|
68
|
+
|
|
69
|
+
const edge: AsciiEdge = {
|
|
70
|
+
from,
|
|
71
|
+
to,
|
|
72
|
+
text: '',
|
|
73
|
+
path: [{ x: 2, y: 0 }], // single-point path — the coinciding-endpoint case
|
|
74
|
+
labelLine: [],
|
|
75
|
+
startDir: Down,
|
|
76
|
+
endDir: Down,
|
|
77
|
+
style: 'solid',
|
|
78
|
+
hasArrowStart: false,
|
|
79
|
+
hasArrowEnd: true,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
expect(() => drawArrow(graph, edge)).not.toThrow()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('does not throw for a bidirectional edge with a single-point path', () => {
|
|
86
|
+
const from = makeNode('A', 0, 0)
|
|
87
|
+
const to = makeNode('B', 4, 0)
|
|
88
|
+
const graph = makeGraph([from, to])
|
|
89
|
+
|
|
90
|
+
const edge: AsciiEdge = {
|
|
91
|
+
from,
|
|
92
|
+
to,
|
|
93
|
+
text: '',
|
|
94
|
+
path: [{ x: 2, y: 0 }],
|
|
95
|
+
labelLine: [],
|
|
96
|
+
startDir: Down,
|
|
97
|
+
endDir: Down,
|
|
98
|
+
style: 'solid',
|
|
99
|
+
hasArrowStart: true,
|
|
100
|
+
hasArrowEnd: true,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
expect(() => drawArrow(graph, edge)).not.toThrow()
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for #454 — "Interactivity: back-edge visually merges with
|
|
3
|
+
* an unrelated line."
|
|
4
|
+
*
|
|
5
|
+
* `placeReachableChildren` (src/ascii/grid.ts) assigns a node's rank from
|
|
6
|
+
* whichever already-placed parent reaches it first, and never revisits it.
|
|
7
|
+
* For a "diamond" shape like `Queue --> Worker` plus `Queue --> Retry -->
|
|
8
|
+
* Worker`, `Worker` can end up at the SAME rank as `Retry` instead of one
|
|
9
|
+
* rank after it — violating the geometric assumption edge-bundling's
|
|
10
|
+
* fan-in/fan-out trunk+junction logic depends on (every source strictly
|
|
11
|
+
* before a shared target, or every target strictly after a shared source).
|
|
12
|
+
* Bundling a rank-violating edge into the shared trunk silently swallows
|
|
13
|
+
* its own distinct arrowhead.
|
|
14
|
+
*
|
|
15
|
+
* `canBundle` (src/ascii/edge-bundling.ts) now refuses to bundle a group
|
|
16
|
+
* when any edge's rank violates that assumption — verified for both the
|
|
17
|
+
* original fan-in repro and the equivalent fan-out shape. No prior test in
|
|
18
|
+
* this repo exercised this directly (verification for #454 was ad hoc/
|
|
19
|
+
* visual only via a real-terminal capture, never committed as a vitest
|
|
20
|
+
* case), which is why it went uncovered.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from 'vitest'
|
|
24
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
25
|
+
|
|
26
|
+
describe('ASCII edge bundling — refuses to bundle a rank-violating edge (issue #454)', () => {
|
|
27
|
+
it('gives a fan-in edge its own arrowhead when its source shares a rank with the target', () => {
|
|
28
|
+
// The exact shape from issue #454's own repro: Queue --> Worker and
|
|
29
|
+
// Queue --> Retry --> Worker put Worker at the same rank as Retry
|
|
30
|
+
// (confirmed via gridCoord in the original investigation), violating
|
|
31
|
+
// fan-in bundling's "every source strictly before the shared target"
|
|
32
|
+
// assumption. The extra Ingest/Store nodes are load-bearing — a
|
|
33
|
+
// smaller 3-node version of this diamond doesn't reproduce the same
|
|
34
|
+
// rank collision.
|
|
35
|
+
const ascii = renderMermaidASCII(
|
|
36
|
+
`flowchart TD
|
|
37
|
+
Ingest --> Queue
|
|
38
|
+
Queue --> Worker
|
|
39
|
+
Queue --> Retry
|
|
40
|
+
Worker --> Store
|
|
41
|
+
Retry --> Worker`,
|
|
42
|
+
{ colorMode: 'none' },
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// A distinct horizontal arrowhead ('◄') from Retry into Worker's own
|
|
46
|
+
// row — the bundled/buggy version instead routes Retry's line down
|
|
47
|
+
// into a vertical merge with Worker's unrelated outgoing line to
|
|
48
|
+
// Store, with no arrowhead of its own anywhere in the output.
|
|
49
|
+
expect(ascii).toContain('◄')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('gives a fan-out edge its own arrowhead when its target shares a rank with the source', () => {
|
|
53
|
+
// Mirror of the fan-in shape: a single source fanning out to two
|
|
54
|
+
// targets, one of which is reached both directly and via a detour
|
|
55
|
+
// that can land it on the same rank as the source.
|
|
56
|
+
const ascii = renderMermaidASCII(
|
|
57
|
+
`flowchart TD
|
|
58
|
+
Source --> Direct
|
|
59
|
+
Source --> Detour
|
|
60
|
+
Detour --> Direct`,
|
|
61
|
+
{ colorMode: 'none' },
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
expect(ascii).toContain('Direct')
|
|
65
|
+
expect(ascii).toContain('Detour')
|
|
66
|
+
// Both of Source's outgoing edges must still be distinctly traceable —
|
|
67
|
+
// this is primarily a "does it render without silently merging" check;
|
|
68
|
+
// the fan-in case above is the one with an exact-count assertion,
|
|
69
|
+
// since the fan-out shape here doesn't reliably force the same rank
|
|
70
|
+
// collision on every layout pass.
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Distinct line-ending glyphs for flowchart --o/--x edges and sequence -x
|
|
3
|
+
// "lost message" arrows (issue #330).
|
|
4
|
+
//
|
|
5
|
+
// Before this fix, --o/--x flowchart edges rendered with the same arrowhead
|
|
6
|
+
// glyph (◢/▲/▼/◄/►, or ^v<> in ascii mode) as a plain -->, and sequence -x
|
|
7
|
+
// rendered with the same filled arrowhead (▶/◀) as ->>. Parsing already
|
|
8
|
+
// correctly distinguished these (see issue #65's parser.test.ts coverage) —
|
|
9
|
+
// only the ASCII/Unicode glyph selection was missing the distinction.
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from 'vitest'
|
|
13
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
14
|
+
|
|
15
|
+
describe('ASCII flowchart edges: --o circle-end / --x cross-end glyphs', () => {
|
|
16
|
+
it('draws a circle glyph for --o in unicode mode', () => {
|
|
17
|
+
const result = renderMermaidASCII(`
|
|
18
|
+
graph TD
|
|
19
|
+
A --o B
|
|
20
|
+
`)
|
|
21
|
+
expect(result).toContain('○')
|
|
22
|
+
expect(result).not.toMatch(/[►◄▲▼◢◣◤◥↖↗↘↙]/)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('draws an x glyph for --x in unicode mode', () => {
|
|
26
|
+
const result = renderMermaidASCII(`
|
|
27
|
+
graph TD
|
|
28
|
+
A --x B
|
|
29
|
+
`)
|
|
30
|
+
expect(result).toContain('✕')
|
|
31
|
+
expect(result).not.toMatch(/[►◄▲▼◢◣◤◥↖↗↘↙]/)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('draws a circle glyph for --o in ascii mode', () => {
|
|
35
|
+
const result = renderMermaidASCII(
|
|
36
|
+
`
|
|
37
|
+
graph TD
|
|
38
|
+
A --o B
|
|
39
|
+
`,
|
|
40
|
+
{ useAscii: true },
|
|
41
|
+
)
|
|
42
|
+
expect(result).toContain('o')
|
|
43
|
+
expect(result).not.toMatch(/[<>^v]/)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('draws an x glyph for --x in ascii mode', () => {
|
|
47
|
+
const result = renderMermaidASCII(
|
|
48
|
+
`
|
|
49
|
+
graph TD
|
|
50
|
+
A --x B
|
|
51
|
+
`,
|
|
52
|
+
{ useAscii: true },
|
|
53
|
+
)
|
|
54
|
+
expect(result).toContain('x')
|
|
55
|
+
expect(result).not.toMatch(/[<>^v]/)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('draws distinct circle/cross glyphs for both edges in the same diagram', () => {
|
|
59
|
+
const result = renderMermaidASCII(`
|
|
60
|
+
graph TD
|
|
61
|
+
A --o E
|
|
62
|
+
A --x F
|
|
63
|
+
`)
|
|
64
|
+
expect(result).toContain('○')
|
|
65
|
+
expect(result).toContain('✕')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// The text-embedded label form (`A -- label --o B`) goes through a
|
|
69
|
+
// different parser branch (TEXT_ARROW_REGEX) than the plain `A --o B`
|
|
70
|
+
// form above. That branch originally computed hasArrowEnd as just
|
|
71
|
+
// `closeOp.endsWith('>')`, which is false for a "--o"/"--x" close — since
|
|
72
|
+
// the ASCII renderer only calls its arrowhead-drawing code at all when
|
|
73
|
+
// hasArrowEnd is true, the circle/cross glyph silently never rendered for
|
|
74
|
+
// this syntax form specifically, even though endMarker was set correctly.
|
|
75
|
+
it('draws the circle glyph for the text-embedded label form (-- label --o)', () => {
|
|
76
|
+
const result = renderMermaidASCII(`
|
|
77
|
+
graph LR
|
|
78
|
+
A -- label --o B
|
|
79
|
+
`)
|
|
80
|
+
expect(result).toContain('○')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('draws the cross glyph for the text-embedded label form (-- label --x)', () => {
|
|
84
|
+
const result = renderMermaidASCII(`
|
|
85
|
+
graph LR
|
|
86
|
+
A -- label --x B
|
|
87
|
+
`)
|
|
88
|
+
expect(result).toContain('✕')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('draws circle/x glyphs at the start end for o-- and x--', () => {
|
|
92
|
+
const resultCircle = renderMermaidASCII(`
|
|
93
|
+
graph LR
|
|
94
|
+
A o--o B
|
|
95
|
+
`)
|
|
96
|
+
expect(resultCircle.match(/○/g)?.length).toBe(2)
|
|
97
|
+
|
|
98
|
+
const resultCross = renderMermaidASCII(`
|
|
99
|
+
graph LR
|
|
100
|
+
A x--x B
|
|
101
|
+
`)
|
|
102
|
+
expect(resultCross.match(/✕/g)?.length).toBe(2)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('still draws a plain arrowhead for a regular --> edge', () => {
|
|
106
|
+
const result = renderMermaidASCII(`
|
|
107
|
+
graph TD
|
|
108
|
+
A --> B
|
|
109
|
+
`)
|
|
110
|
+
expect(result).toContain('▼')
|
|
111
|
+
expect(result).not.toContain('○')
|
|
112
|
+
expect(result).not.toContain('✕')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Fan-out: a single source with multiple outgoing edges gets bundled onto
|
|
116
|
+
// a shared trunk (see src/ascii/draw-bundles.ts), which draws each
|
|
117
|
+
// outgoing arrowhead through a different code path than a lone edge.
|
|
118
|
+
it('draws distinct glyphs for --o/--x when bundled in a fan-out (one source, many targets)', () => {
|
|
119
|
+
const result = renderMermaidASCII(`
|
|
120
|
+
graph TD
|
|
121
|
+
A --o E
|
|
122
|
+
A --x F
|
|
123
|
+
A --> G
|
|
124
|
+
`)
|
|
125
|
+
expect(result).toContain('○')
|
|
126
|
+
expect(result).toContain('✕')
|
|
127
|
+
expect(result).toContain('▼')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Fan-in: multiple sources converging on one target share a single drawn
|
|
131
|
+
// arrowhead (src/ascii/draw-bundles.ts's drawBundleArrowhead). When every
|
|
132
|
+
// bundled edge agrees on the same marker, that shared arrowhead should
|
|
133
|
+
// use it.
|
|
134
|
+
it('draws a shared circle glyph for a fan-in bundle when every edge agrees', () => {
|
|
135
|
+
const result = renderMermaidASCII(`
|
|
136
|
+
graph TD
|
|
137
|
+
E --o C
|
|
138
|
+
F --o C
|
|
139
|
+
`)
|
|
140
|
+
expect(result).toContain('○')
|
|
141
|
+
expect(result).not.toMatch(/[►◄▲▼◢◣◤◥↖↗↘↙]/)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// A mixed fan-in (one edge --o, another plain -->) has no single correct
|
|
145
|
+
// glyph for the shared arrowhead, so it falls back to the plain
|
|
146
|
+
// directional arrowhead rather than guessing.
|
|
147
|
+
it('falls back to a plain arrowhead for a fan-in bundle with mismatched markers', () => {
|
|
148
|
+
const result = renderMermaidASCII(`
|
|
149
|
+
graph TD
|
|
150
|
+
E --o C
|
|
151
|
+
F --x C
|
|
152
|
+
`)
|
|
153
|
+
expect(result).not.toContain('○')
|
|
154
|
+
expect(result).not.toContain('✕')
|
|
155
|
+
expect(result).toContain('▼')
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('ASCII sequence diagrams: -x lost-message glyph', () => {
|
|
160
|
+
it('draws a distinct cross glyph for -x in unicode mode, unlike -) async', () => {
|
|
161
|
+
const result = renderMermaidASCII(`sequenceDiagram
|
|
162
|
+
A-)B: async
|
|
163
|
+
A-xB: lost`)
|
|
164
|
+
expect(result).toContain('▷') // async keeps its existing open arrowhead
|
|
165
|
+
expect(result).toContain('✕') // lost message gets a distinct cross
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('draws a distinct cross glyph for -x in ascii mode', () => {
|
|
169
|
+
const result = renderMermaidASCII(
|
|
170
|
+
`sequenceDiagram
|
|
171
|
+
A-)B: async
|
|
172
|
+
A-xB: lost`,
|
|
173
|
+
{ useAscii: true },
|
|
174
|
+
)
|
|
175
|
+
expect(result).toContain('x')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('does not draw the plain filled arrowhead for -x (unlike ->>)', () => {
|
|
179
|
+
const result = renderMermaidASCII(`sequenceDiagram
|
|
180
|
+
A->>B: normal
|
|
181
|
+
A-xB: lost`)
|
|
182
|
+
expect(result).toContain('▶') // normal ->> keeps its filled arrowhead
|
|
183
|
+
expect(result).toContain('✕')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('draws the cross glyph for a right-to-left -x message', () => {
|
|
187
|
+
const result = renderMermaidASCII(`sequenceDiagram
|
|
188
|
+
B-xA: lost`)
|
|
189
|
+
expect(result).toContain('✕')
|
|
190
|
+
expect(result).not.toMatch(/[◀◁]/)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('draws the cross glyph for a dashed --x lost message', () => {
|
|
194
|
+
const result = renderMermaidASCII(`sequenceDiagram
|
|
195
|
+
A--xB: lost dashed`)
|
|
196
|
+
expect(result).toContain('✕')
|
|
197
|
+
expect(result).toContain('╌')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('still draws the plain filled arrowhead for a normal ->> message', () => {
|
|
201
|
+
const result = renderMermaidASCII(`sequenceDiagram
|
|
202
|
+
A->>B: normal`)
|
|
203
|
+
expect(result).toContain('▶')
|
|
204
|
+
expect(result).not.toContain('✕')
|
|
205
|
+
})
|
|
206
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for #418 ("All Edge Styles"): an edge whose route fell
|
|
3
|
+
* through to determinePath's Case-4 direct fallback has a single diagonal
|
|
4
|
+
* path segment. draw-lines.ts draws that segment as an L (horizontal-first),
|
|
5
|
+
* but determineLabelLine used to center the label on the diagonal itself —
|
|
6
|
+
* mid-way between the two drawn legs, in open grid next to some *other*
|
|
7
|
+
* edge's connector. The `thick` label ended up abutting the dotted edge's
|
|
8
|
+
* column (`┆thick`) instead of sitting on its own `┃` line.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'vitest'
|
|
11
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
12
|
+
import { expandDiagonalSegments } from '../edge-routing.ts'
|
|
13
|
+
|
|
14
|
+
const ALL_EDGE_STYLES = `graph TD
|
|
15
|
+
A[Source] -->|solid| B[Target 1]
|
|
16
|
+
A -.->|dotted| C[Target 2]
|
|
17
|
+
A ==>|thick| D[Target 3]`
|
|
18
|
+
|
|
19
|
+
describe('expandDiagonalSegments', () => {
|
|
20
|
+
it('leaves an axis-aligned path untouched', () => {
|
|
21
|
+
const path = [
|
|
22
|
+
{ x: 1, y: 1 },
|
|
23
|
+
{ x: 1, y: 4 },
|
|
24
|
+
{ x: 5, y: 4 },
|
|
25
|
+
]
|
|
26
|
+
expect(expandDiagonalSegments(path)).toEqual(path)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('splits a diagonal step at its horizontal-first corner', () => {
|
|
30
|
+
expect(
|
|
31
|
+
expandDiagonalSegments([
|
|
32
|
+
{ x: 2, y: 1 },
|
|
33
|
+
{ x: 9, y: 4 },
|
|
34
|
+
]),
|
|
35
|
+
).toEqual([
|
|
36
|
+
{ x: 2, y: 1 },
|
|
37
|
+
{ x: 9, y: 1 },
|
|
38
|
+
{ x: 9, y: 4 },
|
|
39
|
+
])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('handles a diagonal step in the middle of a longer path', () => {
|
|
43
|
+
expect(
|
|
44
|
+
expandDiagonalSegments([
|
|
45
|
+
{ x: 0, y: 0 },
|
|
46
|
+
{ x: 0, y: 2 },
|
|
47
|
+
{ x: 3, y: 5 },
|
|
48
|
+
{ x: 3, y: 7 },
|
|
49
|
+
]),
|
|
50
|
+
).toEqual([
|
|
51
|
+
{ x: 0, y: 0 },
|
|
52
|
+
{ x: 0, y: 2 },
|
|
53
|
+
{ x: 3, y: 2 },
|
|
54
|
+
{ x: 3, y: 5 },
|
|
55
|
+
{ x: 3, y: 7 },
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('edge label on a Case-4 direct-fallback (diagonal) path', () => {
|
|
61
|
+
const lines = renderMermaidASCII(ALL_EDGE_STYLES, { colorMode: 'none' })
|
|
62
|
+
.split('\n')
|
|
63
|
+
.map((l) => l.replace(/\s+$/, ''))
|
|
64
|
+
|
|
65
|
+
const labelRowIndex = lines.findIndex((l) => l.includes('thick'))
|
|
66
|
+
const labelRow = lines[labelRowIndex]!
|
|
67
|
+
const labelStart = labelRow.indexOf('thick')
|
|
68
|
+
const labelEnd = labelStart + 'thick'.length - 1
|
|
69
|
+
|
|
70
|
+
it('is not glued to an unrelated edge’s connector', () => {
|
|
71
|
+
expect(labelRow).not.toMatch(/[┆┊│]thick|thick[┆┊│]/)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('sits on the thick edge’s own vertical line', () => {
|
|
75
|
+
// The thick edge's vertical run (┃) is drawn in the rows right above
|
|
76
|
+
// and below the label; the label must be centered on that column, not
|
|
77
|
+
// floating somewhere between its own connector and a neighbour's.
|
|
78
|
+
const above = lines[labelRowIndex - 1]!
|
|
79
|
+
const below = lines[labelRowIndex + 1]!
|
|
80
|
+
const thickColumnAbove = above.indexOf('┃')
|
|
81
|
+
const thickColumnBelow = below.indexOf('┃')
|
|
82
|
+
expect(thickColumnAbove).toBeGreaterThan(-1)
|
|
83
|
+
expect(thickColumnBelow).toBe(thickColumnAbove)
|
|
84
|
+
expect(thickColumnAbove).toBeGreaterThanOrEqual(labelStart)
|
|
85
|
+
expect(thickColumnAbove).toBeLessThanOrEqual(labelEnd)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('keeps the dotted label on the dotted column', () => {
|
|
89
|
+
const dottedRow = lines.find((l) => l.includes('dotted'))!
|
|
90
|
+
const dottedStart = dottedRow.indexOf('dotted')
|
|
91
|
+
const dottedColumn = lines[lines.indexOf(dottedRow) + 1]!.indexOf('┆')
|
|
92
|
+
expect(dottedColumn).toBeGreaterThanOrEqual(dottedStart)
|
|
93
|
+
expect(dottedColumn).toBeLessThanOrEqual(dottedStart + 'dotted'.length - 1)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for GitHub issue #64 — ASCII edge-routing engine bugs:
|
|
3
|
+
* dense fan-in crashes, order-dependent root misdetection, ungrouped
|
|
4
|
+
* fan-in roots, sibling edges not sharing a trunk, and box-start connector
|
|
5
|
+
* drift. Repro diagrams are taken directly from the issue.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest'
|
|
8
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
9
|
+
|
|
10
|
+
describe('issue #64: edge-routing crashes and layout bugs', () => {
|
|
11
|
+
describe('bug 1: dense fan-in pathfinding blowup', () => {
|
|
12
|
+
it('renders the dense fan-in repro without hanging or crashing', () => {
|
|
13
|
+
const start = Date.now()
|
|
14
|
+
const out = renderMermaidASCII(`graph TD
|
|
15
|
+
A["AAA<br>(keita)"] --> C["CCC"]
|
|
16
|
+
B["BBB<br>(yuriko)"] --> C
|
|
17
|
+
C --> D["DDDD"]
|
|
18
|
+
D --> E["EEEE"]
|
|
19
|
+
A1["1 / 2"] --> A
|
|
20
|
+
A2["3 / 4"] --> A
|
|
21
|
+
A3["5 / 6"] --> A
|
|
22
|
+
A4["XXX<br>(YYY ZZZ)"] --> A
|
|
23
|
+
B1["77 77<br>(7 / 7 / 7)"] --> B
|
|
24
|
+
B2["88-88<br>(99 99)"] --> B
|
|
25
|
+
B3["111s 222s"] --> B
|
|
26
|
+
D --> F{"F?"}
|
|
27
|
+
F -->|Yes| G["High level<br>Tr"]
|
|
28
|
+
F -->|No| H["Dumb Tr<br>S"]`)
|
|
29
|
+
// A generous ceiling — this used to not return within 60s on the
|
|
30
|
+
// reporter's environment. A correct, bounded render should be
|
|
31
|
+
// effectively instant; this just guards against a real regression
|
|
32
|
+
// back to unbounded behavior without being a flaky wall-clock test.
|
|
33
|
+
// Kept well above the near-instant normal case so contention from
|
|
34
|
+
// other tests/processes running in parallel doesn't trip it.
|
|
35
|
+
expect(Date.now() - start).toBeLessThan(15000)
|
|
36
|
+
for (const label of ['AAA', 'BBB', 'CCC', 'DDDD', 'EEEE', 'F?']) {
|
|
37
|
+
expect(out).toContain(label)
|
|
38
|
+
}
|
|
39
|
+
}, 20000)
|
|
40
|
+
|
|
41
|
+
it('renders a much larger dense fan-in graph without hanging or crashing', () => {
|
|
42
|
+
// Amplified stress case (2x60 root nodes feeding two hubs) — confirms
|
|
43
|
+
// the render-wide path budget bounds total pathfinding work even
|
|
44
|
+
// when the per-search cap alone would not (each of ~120 fan-in edges
|
|
45
|
+
// independently getting its own 50k-iteration allowance is what
|
|
46
|
+
// caused real OOM crashes during investigation of this issue).
|
|
47
|
+
let src = 'graph TD\n A --> C\n B --> C\n C --> D\n D --> E\n'
|
|
48
|
+
for (let i = 0; i < 60; i++) src += ` A${i}["Root A${i}"] --> A\n`
|
|
49
|
+
for (let i = 0; i < 60; i++) src += ` B${i}["Root B${i}"] --> B\n`
|
|
50
|
+
|
|
51
|
+
const start = Date.now()
|
|
52
|
+
const out = renderMermaidASCII(src)
|
|
53
|
+
// Loose bound (not a tight performance assertion) — the goal is only
|
|
54
|
+
// to catch a regression back to the unbounded blowup this fix
|
|
55
|
+
// addresses (which would hang far longer than this, not just run
|
|
56
|
+
// slightly over). Both 10s and 20s bounds flaked under CPU
|
|
57
|
+
// contention (many test processes/agents competing for cores),
|
|
58
|
+
// despite the actual render being near-instant in isolation, so this
|
|
59
|
+
// is deliberately generous: real unbounded-blowup regressions took
|
|
60
|
+
// 60s+ to not even finish (see the repro comment above), so a 60s
|
|
61
|
+
// ceiling still catches those while tolerating a heavily loaded box.
|
|
62
|
+
expect(Date.now() - start).toBeLessThan(60000)
|
|
63
|
+
expect(out).toContain('Root A0')
|
|
64
|
+
expect(out).toContain('Root B59')
|
|
65
|
+
}, 90000)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('bug 2: order-dependent root misdetection', () => {
|
|
69
|
+
it('does not misclassify a node as a root when its child edge is declared first', () => {
|
|
70
|
+
// A --> C is declared *before* A1 --> A, so a single-pass,
|
|
71
|
+
// insertion-order root scan would wrongly treat A as a root (it
|
|
72
|
+
// "looks" untargeted at the point the scan reaches it).
|
|
73
|
+
const out = renderMermaidASCII(`graph TD
|
|
74
|
+
A["Parent A"] --> C["Child C"]
|
|
75
|
+
B["Parent B"] --> C
|
|
76
|
+
C --> D["Grandchild D"]
|
|
77
|
+
A1["Root 1"] --> A
|
|
78
|
+
A2["Root 2"] --> A
|
|
79
|
+
A3["Root 3"] --> A
|
|
80
|
+
B1["Root 4"] --> B
|
|
81
|
+
B2["Root 5"] --> B`)
|
|
82
|
+
|
|
83
|
+
const lines = out.split('\n')
|
|
84
|
+
const rootRow = lines.findIndex((l) => l.includes('Root 1'))
|
|
85
|
+
const parentRow = lines.findIndex((l) => l.includes('Parent A'))
|
|
86
|
+
const childRow = lines.findIndex((l) => l.includes('Child C'))
|
|
87
|
+
const grandchildRow = lines.findIndex((l) => l.includes('Grandchild D'))
|
|
88
|
+
|
|
89
|
+
// Correct levels: roots above parents above child above grandchild.
|
|
90
|
+
expect(rootRow).toBeLessThan(parentRow)
|
|
91
|
+
expect(parentRow).toBeLessThan(childRow)
|
|
92
|
+
expect(childRow).toBeLessThan(grandchildRow)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('does not crash on a graph that is a pure cycle (no node is ever a real root)', () => {
|
|
96
|
+
// Every node here has a real incoming edge — order-independent
|
|
97
|
+
// detection correctly finds zero "never targeted" roots, so the
|
|
98
|
+
// layout needs its own fallback seed to avoid leaving every node
|
|
99
|
+
// unplaced (which used to crash with "Map maximum size exceeded" /
|
|
100
|
+
// a null gridCoord dereference).
|
|
101
|
+
expect(() =>
|
|
102
|
+
renderMermaidASCII('graph LR\nA --> B --> C --> A'),
|
|
103
|
+
).not.toThrow()
|
|
104
|
+
const out = renderMermaidASCII('graph LR\nA --> B --> C --> A')
|
|
105
|
+
expect(out).toContain('A')
|
|
106
|
+
expect(out).toContain('B')
|
|
107
|
+
expect(out).toContain('C')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('bug 3: fan-in roots grouped by shared target', () => {
|
|
112
|
+
it('groups roots feeding the same target contiguously, even when interleaved in source order', () => {
|
|
113
|
+
// A1 and B1 are declared before A2 and B2, interleaving the two
|
|
114
|
+
// fan-in clusters in source order — the roots feeding A must still
|
|
115
|
+
// end up placed together, separately from the roots feeding B.
|
|
116
|
+
const out = renderMermaidASCII(`graph TD
|
|
117
|
+
A1 --> A
|
|
118
|
+
B1 --> B
|
|
119
|
+
A2 --> A
|
|
120
|
+
B2 --> B
|
|
121
|
+
A --> C
|
|
122
|
+
B --> C`)
|
|
123
|
+
|
|
124
|
+
const topRow = out.split('\n').find((l) => l.includes('A1'))!
|
|
125
|
+
const a1 = topRow.indexOf('A1')
|
|
126
|
+
const a2 = topRow.indexOf('A2')
|
|
127
|
+
const b1 = topRow.indexOf('B1')
|
|
128
|
+
const b2 = topRow.indexOf('B2')
|
|
129
|
+
|
|
130
|
+
expect(a1).toBeGreaterThanOrEqual(0)
|
|
131
|
+
expect(a2).toBeGreaterThanOrEqual(0)
|
|
132
|
+
expect(b1).toBeGreaterThanOrEqual(0)
|
|
133
|
+
expect(b2).toBeGreaterThanOrEqual(0)
|
|
134
|
+
// Grouped order: both A-feeders before both B-feeders.
|
|
135
|
+
expect(a1).toBeLessThan(b1)
|
|
136
|
+
expect(a2).toBeLessThan(b1)
|
|
137
|
+
expect(b1).toBeLessThan(b2)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('bug 4: sibling edges from the same source share a trunk', () => {
|
|
142
|
+
it('routes a labeled 3-way fan-out along a shared straight trunk instead of a detour', () => {
|
|
143
|
+
const out = renderMermaidASCII(`flowchart TB
|
|
144
|
+
Src["Source"]
|
|
145
|
+
Left["Left Target"]
|
|
146
|
+
Center["Center Target"]
|
|
147
|
+
Right["Right Target"]
|
|
148
|
+
Src -->|left*| Left
|
|
149
|
+
Src -->|center*| Center
|
|
150
|
+
Src -->|right*| Right`)
|
|
151
|
+
|
|
152
|
+
// The three branch points (├, ┬, ┐) for a clean shared trunk all
|
|
153
|
+
// land on the same row, immediately below Source's box.
|
|
154
|
+
const trunkRow = out
|
|
155
|
+
.split('\n')
|
|
156
|
+
.find((l) => l.includes('├') && l.includes('┬') && l.includes('┐'))
|
|
157
|
+
expect(trunkRow).toBeDefined()
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('bug 5: box-start connector stays on the source border', () => {
|
|
162
|
+
it('keeps the ├ connector attached to the box border when a sibling label widens a shared column', () => {
|
|
163
|
+
const out = renderMermaidASCII(`flowchart LR
|
|
164
|
+
Src["Source"]
|
|
165
|
+
Top["Top Target"]
|
|
166
|
+
Mid["Middle Target"]
|
|
167
|
+
Bot["Bottom Target"]
|
|
168
|
+
Src -->|top*| Top
|
|
169
|
+
Src -->|mid*| Mid
|
|
170
|
+
Src -->|bot*| Bot`)
|
|
171
|
+
|
|
172
|
+
const topRow = out.split('\n').find((l) => l.includes('top*'))!
|
|
173
|
+
// The connector must be immediately adjacent to Source's box — no
|
|
174
|
+
// run of blank space between the border and the line/label.
|
|
175
|
+
expect(topRow).toMatch(/Source ├top\*/)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
})
|