@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,639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for #488 and #489 — `renderClassAscii`'s column layout
|
|
3
|
+
* sized each class's horizontal slot purely from the box's own content, so
|
|
4
|
+
* anything a relationship needed beside a narrow box had nowhere to go:
|
|
5
|
+
*
|
|
6
|
+
* - #488: a long label on a single-letter class (the "All 6 Relationship
|
|
7
|
+
* Types" sample) collided with its neighbours' labels and the territory
|
|
8
|
+
* pass truncated all of them to `…`, even though the diagram could simply
|
|
9
|
+
* have spread its columns further apart.
|
|
10
|
+
* - #489: more than two relationships between a narrow pair fanned their
|
|
11
|
+
* per-pair column offsets (the #448 fix) wider than the box, and
|
|
12
|
+
* clamping every offset back inside the box collapsed distinct
|
|
13
|
+
* relationships onto one connection point — the later one silently
|
|
14
|
+
* overwrote the earlier one's line, arrowhead, and label, exactly the
|
|
15
|
+
* #448 failure mode at a higher relationship count.
|
|
16
|
+
*
|
|
17
|
+
* The fix reserves each class's slot from what its relationships overhang
|
|
18
|
+
* past the box (labels centered on their lanes, and the full spread of a
|
|
19
|
+
* fanned group), keeps every relationship's *lane* distinct, and joins a
|
|
20
|
+
* lane that sits outside its box back to a border anchor with a short jog.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from 'vitest'
|
|
24
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
25
|
+
import { assertUniformDisplayWidth } from './helpers/terminal-display-width.ts'
|
|
26
|
+
|
|
27
|
+
/** Same source as the "Class: All 6 Relationship Types" sample (samples-data.ts). */
|
|
28
|
+
const ALL_SIX = `classDiagram
|
|
29
|
+
A <|-- B : inheritance
|
|
30
|
+
C *-- D : composition
|
|
31
|
+
E o-- F : aggregation
|
|
32
|
+
G --> H : association
|
|
33
|
+
I ..> J : dependency
|
|
34
|
+
K ..|> L : realization`
|
|
35
|
+
|
|
36
|
+
const ALL_SIX_LABELS = [
|
|
37
|
+
'inheritance',
|
|
38
|
+
'composition',
|
|
39
|
+
'aggregation',
|
|
40
|
+
'association',
|
|
41
|
+
'dependency',
|
|
42
|
+
'realization',
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/** The #489 repro: four relationships between two width-5 boxes. */
|
|
46
|
+
const FOUR_BETWEEN_NARROW_PAIR = `classDiagram
|
|
47
|
+
class A
|
|
48
|
+
class B
|
|
49
|
+
A --> B : one
|
|
50
|
+
A --> B : two
|
|
51
|
+
A --> B : three
|
|
52
|
+
A --> B : four`
|
|
53
|
+
|
|
54
|
+
/** Column of the first occurrence of `needle` on the first line containing it. */
|
|
55
|
+
function columnOf(lines: string[], needle: string): number {
|
|
56
|
+
const line = lines.find((l) => l.includes(needle))
|
|
57
|
+
if (line === undefined) throw new Error(`"${needle}" not found`)
|
|
58
|
+
return line.indexOf(needle)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('ASCII class diagram — column width reserves room for relationship labels (issue #488)', () => {
|
|
62
|
+
it('renders every label of the "All 6 Relationship Types" sample in full, with no ellipsis', () => {
|
|
63
|
+
const ascii = renderMermaidASCII(ALL_SIX, { colorMode: 'none' })
|
|
64
|
+
for (const label of ALL_SIX_LABELS) {
|
|
65
|
+
expect(ascii).toContain(label)
|
|
66
|
+
}
|
|
67
|
+
expect(ascii).not.toContain('…')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('keeps each label centered under its own class column, in source order', () => {
|
|
71
|
+
const lines = renderMermaidASCII(ALL_SIX, { colorMode: 'none' }).split('\n')
|
|
72
|
+
const labelCols = ALL_SIX_LABELS.map((l) => columnOf(lines, l))
|
|
73
|
+
// Left to right, matching the classes' order in the level.
|
|
74
|
+
expect([...labelCols].sort((a, b) => a - b)).toEqual(labelCols)
|
|
75
|
+
|
|
76
|
+
// Each label straddles the column its relationship's line runs along:
|
|
77
|
+
// the marker glyph directly above/below it sits within the label's span.
|
|
78
|
+
const markerRow = lines.find((l) => l.includes('△'))!
|
|
79
|
+
const markerCols = [...markerRow].flatMap((ch, i) =>
|
|
80
|
+
['△', '◆', '◇', '│', '┊'].includes(ch) ? [i] : [],
|
|
81
|
+
)
|
|
82
|
+
expect(markerCols).toHaveLength(6)
|
|
83
|
+
ALL_SIX_LABELS.forEach((label, i) => {
|
|
84
|
+
const start = labelCols[i]!
|
|
85
|
+
const end = start + label.length - 1
|
|
86
|
+
expect(markerCols[i]).toBeGreaterThanOrEqual(start)
|
|
87
|
+
expect(markerCols[i]).toBeLessThanOrEqual(end)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('leaves a class whose box is already wider than its labels at the default gap', () => {
|
|
92
|
+
// Nothing overhangs these boxes, so the reservation must not move them:
|
|
93
|
+
// the two top borders stay exactly one horizontal gap (4 cells) apart.
|
|
94
|
+
const ascii = renderMermaidASCII(
|
|
95
|
+
`classDiagram
|
|
96
|
+
class Teacher {
|
|
97
|
+
+name String
|
|
98
|
+
}
|
|
99
|
+
class Student {
|
|
100
|
+
+name String
|
|
101
|
+
}
|
|
102
|
+
class Course {
|
|
103
|
+
+title String
|
|
104
|
+
}
|
|
105
|
+
Teacher --> Course : teaches
|
|
106
|
+
Student --> Course : enrolled in`,
|
|
107
|
+
{ colorMode: 'none' },
|
|
108
|
+
)
|
|
109
|
+
const topBorder = ascii.split('\n')[0]!
|
|
110
|
+
expect(topBorder).toMatch(/^┌─+┐ {4}┌─+┐\s*$/)
|
|
111
|
+
expect(ascii).toContain('teaches')
|
|
112
|
+
expect(ascii).toContain('enrolled in')
|
|
113
|
+
expect(ascii).not.toContain('…')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('ASCII class diagram — more than two relationships between a narrow pair (issue #489)', () => {
|
|
118
|
+
it('renders all four labels in full, each on its own lane', () => {
|
|
119
|
+
const ascii = renderMermaidASCII(FOUR_BETWEEN_NARROW_PAIR, {
|
|
120
|
+
colorMode: 'none',
|
|
121
|
+
})
|
|
122
|
+
const lines = ascii.split('\n')
|
|
123
|
+
expect(ascii).not.toContain('…')
|
|
124
|
+
|
|
125
|
+
// All four on one row, left to right, none clipped by the canvas edge
|
|
126
|
+
// (the failed local attempt in #489 lost `one` entirely and left `tw`).
|
|
127
|
+
const labelRow = lines.find((l) => l.includes('one'))!
|
|
128
|
+
expect(labelRow).toMatch(/\bone\b.*\btwo\b.*\bthree\b.*\bfour\b/)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('gives every relationship its own arrowhead on the target border', () => {
|
|
132
|
+
const lines = renderMermaidASCII(FOUR_BETWEEN_NARROW_PAIR, {
|
|
133
|
+
colorMode: 'none',
|
|
134
|
+
}).split('\n')
|
|
135
|
+
|
|
136
|
+
const arrowRow = lines.findIndex((l) => l.includes('▼'))
|
|
137
|
+
expect(arrowRow).toBeGreaterThanOrEqual(0)
|
|
138
|
+
const arrowCols = [...lines[arrowRow]!].flatMap((ch, i) =>
|
|
139
|
+
ch === '▼' ? [i] : [],
|
|
140
|
+
)
|
|
141
|
+
expect(arrowCols).toHaveLength(4)
|
|
142
|
+
|
|
143
|
+
// Every arrowhead sits directly on top of B's top border — attached to
|
|
144
|
+
// the box, not floating in the space its lane fanned out into.
|
|
145
|
+
const bTop = lines[arrowRow + 1]!
|
|
146
|
+
expect(bTop).toMatch(/┌─+┐/)
|
|
147
|
+
const boxStart = bTop.indexOf('┌')
|
|
148
|
+
const boxEnd = bTop.indexOf('┐')
|
|
149
|
+
for (const col of arrowCols) {
|
|
150
|
+
expect(col).toBeGreaterThanOrEqual(boxStart)
|
|
151
|
+
expect(col).toBeLessThanOrEqual(boxEnd)
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('draws a jog from each fanned-out lane back to the box border', () => {
|
|
156
|
+
const lines = renderMermaidASCII(FOUR_BETWEEN_NARROW_PAIR, {
|
|
157
|
+
colorMode: 'none',
|
|
158
|
+
}).split('\n')
|
|
159
|
+
// Row directly under A: a trunk of corners/tees running out to the
|
|
160
|
+
// outer lanes on both sides.
|
|
161
|
+
const aBottom = lines.findIndex((l) => l.includes('└───┘'))
|
|
162
|
+
const jogRow = lines[aBottom + 1]!
|
|
163
|
+
expect(jogRow).toMatch(/┌─+┬.*┬─+┐/)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('does not add jogs to a group that already fits inside its boxes', () => {
|
|
167
|
+
// A reciprocal pair on boxes wide enough for both lanes (the #448 MVC
|
|
168
|
+
// shape) keeps the plain three-segment route — no junction glyphs.
|
|
169
|
+
const ascii = renderMermaidASCII(
|
|
170
|
+
`classDiagram
|
|
171
|
+
class Model {
|
|
172
|
+
+getData() Map
|
|
173
|
+
+setData(key, val) void
|
|
174
|
+
}
|
|
175
|
+
class View {
|
|
176
|
+
+render() void
|
|
177
|
+
+update() void
|
|
178
|
+
}
|
|
179
|
+
Model ..> View : notifies
|
|
180
|
+
View --> Model : reads`,
|
|
181
|
+
{ colorMode: 'none' },
|
|
182
|
+
)
|
|
183
|
+
expect(ascii).toContain('notifies')
|
|
184
|
+
expect(ascii).toContain('reads')
|
|
185
|
+
expect(ascii).not.toMatch(/[┬┴┼]/)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// ----------------------------------------------------------------------------
|
|
190
|
+
// Additional coverage identified in review of PR #512 — real gaps the tests
|
|
191
|
+
// above don't exercise: a true zero-relationship baseline, a single (non-
|
|
192
|
+
// grouped) relationship's own reservation, a group whose members' labels are
|
|
193
|
+
// wildly different lengths, two independent multi-relationship groups in one
|
|
194
|
+
// diagram, double-width (CJK) label characters, one class's reach across
|
|
195
|
+
// several *unrelated* relationships (max, not sum), and the interaction with
|
|
196
|
+
// PR #514's detour routing.
|
|
197
|
+
// ----------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
describe('ASCII class diagram — column reservation baseline and single-relationship cases', () => {
|
|
200
|
+
it('adds no extra width for classes with zero relationships (default gap only)', () => {
|
|
201
|
+
// No relationships at all means `columnReach` never gets a nonzero left
|
|
202
|
+
// or right for either class — the slot must stay exactly the box width,
|
|
203
|
+
// same as if the reservation feature didn't exist.
|
|
204
|
+
const ascii = renderMermaidASCII(
|
|
205
|
+
`classDiagram
|
|
206
|
+
class Foo
|
|
207
|
+
class Bar`,
|
|
208
|
+
{ colorMode: 'none' },
|
|
209
|
+
)
|
|
210
|
+
const topBorder = ascii.split('\n')[0]!
|
|
211
|
+
expect(topBorder).toMatch(/^┌─+┐ {4}┌─+┐\s*$/)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('reserves room for a single (ungrouped) relationship label on a narrow box', () => {
|
|
215
|
+
// Group size 1 — `relColumnOffset`/`relGroupSpread` are never populated
|
|
216
|
+
// for this pair (the pair-grouping pass only fires for `group.length >=
|
|
217
|
+
// 2`), so any extra room here must come from `columnReach` alone, not
|
|
218
|
+
// from the fan-out logic #489 added. A single letter box is 5 cells wide
|
|
219
|
+
// ("┌───┐"); a label many times that width has nowhere to go without
|
|
220
|
+
// the slot growing past the box on at least one side.
|
|
221
|
+
const ascii = renderMermaidASCII(
|
|
222
|
+
`classDiagram
|
|
223
|
+
class A
|
|
224
|
+
class B
|
|
225
|
+
A --> B : a label far wider than either narrow box`,
|
|
226
|
+
{ colorMode: 'none' },
|
|
227
|
+
)
|
|
228
|
+
const lines = ascii.split('\n')
|
|
229
|
+
expect(ascii).not.toContain('…')
|
|
230
|
+
expect(ascii).toContain('a label far wider than either narrow box')
|
|
231
|
+
|
|
232
|
+
// The label's lane must carry zero fan offset (no group to fan within):
|
|
233
|
+
// it sits centered under column 0 relative to both boxes, i.e. the same
|
|
234
|
+
// column A and B's own centers occupy.
|
|
235
|
+
const aTop = lines.find((l) => l.includes('┌───┐'))!
|
|
236
|
+
const labelRow = lines.find((l) => l.includes('a label far wider'))!
|
|
237
|
+
const aCenter = aTop.indexOf('┌') + 2 // "┌───┐" is 5 wide, center at +2
|
|
238
|
+
const labelText = 'a label far wider than either narrow box'
|
|
239
|
+
const labelStart = labelRow.indexOf(labelText)
|
|
240
|
+
const labelCenter = labelStart + Math.floor(labelText.length / 2)
|
|
241
|
+
expect(Math.abs(labelCenter - aCenter)).toBeLessThanOrEqual(1)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
describe('ASCII class diagram — a group whose members have wildly different label lengths', () => {
|
|
246
|
+
const SOURCE = `classDiagram
|
|
247
|
+
class A
|
|
248
|
+
class B
|
|
249
|
+
A --> B : hi
|
|
250
|
+
B --> A : this is a much much longer label than the other one`
|
|
251
|
+
|
|
252
|
+
it('renders both labels in full — the group spacing must key off the widest member', () => {
|
|
253
|
+
// If the fan-out step were sized off the shortest label (or an average)
|
|
254
|
+
// instead of the group's widest, the long label would overlap `hi`'s
|
|
255
|
+
// lane (or the boxes) and the territory pass would truncate one or both
|
|
256
|
+
// to an ellipsis.
|
|
257
|
+
const ascii = renderMermaidASCII(SOURCE, { colorMode: 'none' })
|
|
258
|
+
expect(ascii).not.toContain('…')
|
|
259
|
+
expect(ascii).toContain('hi')
|
|
260
|
+
expect(ascii).toContain(
|
|
261
|
+
'this is a much much longer label than the other one',
|
|
262
|
+
)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('keeps the two labels non-overlapping on their shared row', () => {
|
|
266
|
+
const lines = renderMermaidASCII(SOURCE, { colorMode: 'none' }).split('\n')
|
|
267
|
+
const row = lines.find((l) => l.includes('hi'))!
|
|
268
|
+
expect(row).toContain('this is a much much longer label than the other one')
|
|
269
|
+
const hiStart = row.indexOf('hi')
|
|
270
|
+
const hiEnd = hiStart + 1
|
|
271
|
+
const longStart = row.indexOf(
|
|
272
|
+
'this is a much much longer label than the other one',
|
|
273
|
+
)
|
|
274
|
+
const longEnd =
|
|
275
|
+
longStart +
|
|
276
|
+
'this is a much much longer label than the other one'.length -
|
|
277
|
+
1
|
|
278
|
+
const overlaps = hiStart <= longEnd && longStart <= hiEnd
|
|
279
|
+
expect(overlaps).toBe(false)
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('ASCII class diagram — two independent multi-relationship groups in one diagram', () => {
|
|
284
|
+
it("does not let one group's wide labels leak into a separate pair's spacing", () => {
|
|
285
|
+
// A<->B carries a much wider label pair than C<->D. If group state leaked
|
|
286
|
+
// (e.g. a shared "widest label seen so far" instead of one keyed per
|
|
287
|
+
// pair), C/D would be pushed further apart than they need to be on their
|
|
288
|
+
// own — this compares the actual C-D spacing against an isolated C/D-only
|
|
289
|
+
// rendering with identical C/D relationships and nothing else.
|
|
290
|
+
const combined = renderMermaidASCII(
|
|
291
|
+
`classDiagram
|
|
292
|
+
class A
|
|
293
|
+
class B
|
|
294
|
+
class C
|
|
295
|
+
class D
|
|
296
|
+
A --> B : short
|
|
297
|
+
B --> A : another short one that is somewhat longer than short
|
|
298
|
+
C --> D : x
|
|
299
|
+
D --> C : y`,
|
|
300
|
+
{ colorMode: 'none' },
|
|
301
|
+
).split('\n')
|
|
302
|
+
const isolated = renderMermaidASCII(
|
|
303
|
+
`classDiagram
|
|
304
|
+
class C
|
|
305
|
+
class D
|
|
306
|
+
C --> D : x
|
|
307
|
+
D --> C : y`,
|
|
308
|
+
{ colorMode: 'none' },
|
|
309
|
+
).split('\n')
|
|
310
|
+
|
|
311
|
+
const cRowCombined = combined.find((l) => l.includes('│ C │'))!
|
|
312
|
+
const cRowIsolated = isolated.find((l) => l.includes('│ C │'))!
|
|
313
|
+
const distanceCombined =
|
|
314
|
+
cRowCombined.indexOf('D') - cRowCombined.indexOf('C')
|
|
315
|
+
const distanceIsolated =
|
|
316
|
+
cRowIsolated.indexOf('D') - cRowIsolated.indexOf('C')
|
|
317
|
+
expect(distanceCombined).toBe(distanceIsolated)
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
describe('ASCII class diagram — double-width (CJK) relationship labels', () => {
|
|
322
|
+
it('measures column reservation by display width, not UTF-16 length', () => {
|
|
323
|
+
// Each CJK character here is 2 terminal columns wide but 1 UTF-16 code
|
|
324
|
+
// unit — if `labelCellWidth` measured `.length` instead of `displayWidth`
|
|
325
|
+
// (the same class of bug already fixed for box content in #182 and for
|
|
326
|
+
// sequence-diagram labels in #334), X/Y's reserved slot would come out
|
|
327
|
+
// roughly half what the label actually needs. A sibling pair (W -> Z)
|
|
328
|
+
// beside X -> Y gives that under-reservation somewhere to collide with:
|
|
329
|
+
// with the bug, the CJK label runs into W/Z's column and the territory
|
|
330
|
+
// pass truncates it to an ellipsis; with correct display-width
|
|
331
|
+
// measurement, both labels render in full, each in its own lane.
|
|
332
|
+
const ascii = renderMermaidASCII(
|
|
333
|
+
`classDiagram
|
|
334
|
+
class X
|
|
335
|
+
class W
|
|
336
|
+
class Y
|
|
337
|
+
class Z
|
|
338
|
+
X --> Y : 图表关系测试标签更多字
|
|
339
|
+
W --> Z : short`,
|
|
340
|
+
{ colorMode: 'none' },
|
|
341
|
+
)
|
|
342
|
+
expect(ascii).not.toContain('…')
|
|
343
|
+
expect(ascii).toContain('图表关系测试标签更多字')
|
|
344
|
+
expect(ascii).toContain('short')
|
|
345
|
+
assertUniformDisplayWidth(ascii)
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
describe('ASCII class diagram — one class spanning several unrelated relationships (max, not sum)', () => {
|
|
350
|
+
it("reserves room for a class's widest relationship only once, not once per relationship", () => {
|
|
351
|
+
// A appears as the "to" of one long-labeled relationship and the "from"
|
|
352
|
+
// of a second, short-labeled one to a different class entirely (not a
|
|
353
|
+
// fan-out group — these are two distinct pairs, `P::A` and `A::Q`).
|
|
354
|
+
// `columnReach` folds every relationship touching a class together with
|
|
355
|
+
// `Math.max`; if that were `+=` instead, A's reserved slot would grow
|
|
356
|
+
// with the *count* of its relationships rather than staying pinned to
|
|
357
|
+
// the single widest one, and A's column would drift right of where it
|
|
358
|
+
// sits when the long relationship is the only one touching it at all.
|
|
359
|
+
const combined = renderMermaidASCII(
|
|
360
|
+
`classDiagram
|
|
361
|
+
class P
|
|
362
|
+
class A
|
|
363
|
+
class Q
|
|
364
|
+
class R
|
|
365
|
+
P --> A : the long label that needs a lot of horizontal room
|
|
366
|
+
A --> Q : short
|
|
367
|
+
Q --> R : also short`,
|
|
368
|
+
{ colorMode: 'none' },
|
|
369
|
+
).split('\n')
|
|
370
|
+
const isolated = renderMermaidASCII(
|
|
371
|
+
`classDiagram
|
|
372
|
+
class P
|
|
373
|
+
class A
|
|
374
|
+
P --> A : the long label that needs a lot of horizontal room`,
|
|
375
|
+
{ colorMode: 'none' },
|
|
376
|
+
).split('\n')
|
|
377
|
+
|
|
378
|
+
const aRowCombined = combined.find((l) => l.includes('│ A │'))!
|
|
379
|
+
const aRowIsolated = isolated.find((l) => l.includes('│ A │'))!
|
|
380
|
+
expect(aRowCombined.indexOf('A')).toBe(aRowIsolated.indexOf('A'))
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
describe('ASCII class diagram — column reservation composes with detour routing (issue #514 interaction)', () => {
|
|
385
|
+
// A -> B -> C is a straight chain, but A -> C also exists and — with
|
|
386
|
+
// matching label lengths on all three relationships — A, B, and C end up
|
|
387
|
+
// column-aligned, so A -> C's straight vertical path collides with B and
|
|
388
|
+
// must detour (the same shape as the #487 MVC repro, but with narrow,
|
|
389
|
+
// single-word boxes that also need `columnReach` to grow their slots for
|
|
390
|
+
// these labels). D -> E is an unrelated, short-labeled pair placed beside
|
|
391
|
+
// A/B/C by the layout — if A/B/C's slots were NOT widened for their long
|
|
392
|
+
// labels, D/E would land close enough to collide with them. This is the
|
|
393
|
+
// composition PR #512's column-width pass and PR #514's label-aware
|
|
394
|
+
// detour routing must both get right at once.
|
|
395
|
+
const SOURCE = `classDiagram
|
|
396
|
+
class A
|
|
397
|
+
class B
|
|
398
|
+
class C
|
|
399
|
+
class D
|
|
400
|
+
class E
|
|
401
|
+
A --> B : a moderately long label here
|
|
402
|
+
A --> C : a moderately long label there
|
|
403
|
+
B --> C : a moderately long label too
|
|
404
|
+
D --> E : x`
|
|
405
|
+
|
|
406
|
+
it('reserves room for every label while one relationship detours around the middle class', () => {
|
|
407
|
+
const ascii = renderMermaidASCII(SOURCE, { colorMode: 'none' })
|
|
408
|
+
const lines = ascii.split('\n')
|
|
409
|
+
expect(ascii).not.toContain('…')
|
|
410
|
+
for (const label of [
|
|
411
|
+
'a moderately long label here',
|
|
412
|
+
'a moderately long label there',
|
|
413
|
+
'a moderately long label too',
|
|
414
|
+
'x',
|
|
415
|
+
]) {
|
|
416
|
+
expect(ascii).toContain(label)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// B's box borders give its column footprint; the detoured relationship's
|
|
420
|
+
// label ("...there") must land clear of B's footprint, not overlapping
|
|
421
|
+
// or truncated by it — the same "past the border, not inside it" check
|
|
422
|
+
// the #487 suite uses for the MVC repro.
|
|
423
|
+
const bTopRow = lines.findIndex(
|
|
424
|
+
(l, i) =>
|
|
425
|
+
l.includes('┌───┐') && (lines[i + 1]?.includes('│ B │') ?? false),
|
|
426
|
+
)
|
|
427
|
+
expect(bTopRow).toBeGreaterThanOrEqual(0)
|
|
428
|
+
const bRightBorder = lines[bTopRow]!.indexOf('┐')
|
|
429
|
+
|
|
430
|
+
const thereRow = lines.findIndex((l) =>
|
|
431
|
+
l.includes('a moderately long label there'),
|
|
432
|
+
)
|
|
433
|
+
expect(thereRow).toBeGreaterThanOrEqual(0)
|
|
434
|
+
const thereStart = lines[thereRow]!.indexOf('a moderately long label there')
|
|
435
|
+
expect(thereStart).toBeGreaterThan(bRightBorder)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it("does not let D/E's short-labeled pair collide with A/B/C's reserved columns", () => {
|
|
439
|
+
// If A/B/C's own slots weren't widened for their long labels, D -> E's
|
|
440
|
+
// short "x" would land inside the space one of A/B/C's long labels
|
|
441
|
+
// needs, corrupting it mid-string (this is exactly what a broken
|
|
442
|
+
// reservation produces: e.g. "a moderat x long label here" instead of
|
|
443
|
+
// "a moderately long label here") rather than a clean ellipsis
|
|
444
|
+
// truncation — so this checks the label text is byte-for-byte intact,
|
|
445
|
+
// not just present as a substring.
|
|
446
|
+
const ascii = renderMermaidASCII(SOURCE, { colorMode: 'none' })
|
|
447
|
+
expect(ascii).toContain('a moderately long label here')
|
|
448
|
+
expect(ascii).toContain('a moderately long label there')
|
|
449
|
+
expect(ascii).toContain('a moderately long label too')
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
// ----------------------------------------------------------------------------
|
|
454
|
+
// Precise patch-coverage gaps identified from PR #512's codecov report: the
|
|
455
|
+
// 8 tests above are legitimate but happen not to execute several specific
|
|
456
|
+
// lines/branches codecov tracks as new-in-this-diff. Each test below targets
|
|
457
|
+
// one exact gap (see the codecov `patch` DA:/BRDA: cross-reference against
|
|
458
|
+
// `git diff main...HEAD -- src/ascii/class-diagram.ts`), not a generic
|
|
459
|
+
// "more coverage" addition.
|
|
460
|
+
// ----------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
describe('ASCII class diagram — plain-ASCII jog rendering (drawJog useAscii branch)', () => {
|
|
463
|
+
it('draws the fanned-lane jog with ASCII dashes/pipes, not box-drawing corners, under useAscii', () => {
|
|
464
|
+
// `drawJog`'s `useAscii` branch (setJogCell with the ASCII line glyph on
|
|
465
|
+
// both the anchor and the lane column, then an early return before the
|
|
466
|
+
// corner-glyph arithmetic) is only reachable when a jog is actually
|
|
467
|
+
// drawn (a fanned-out lane, same #489 shape as FOUR_BETWEEN_NARROW_PAIR)
|
|
468
|
+
// *and* rendering in ASCII mode. Every existing jog test renders in the
|
|
469
|
+
// default Unicode mode, so this branch — and the plain-ASCII jog cells
|
|
470
|
+
// it writes — went untested.
|
|
471
|
+
const ascii = renderMermaidASCII(FOUR_BETWEEN_NARROW_PAIR, {
|
|
472
|
+
colorMode: 'none',
|
|
473
|
+
useAscii: true,
|
|
474
|
+
})
|
|
475
|
+
expect(ascii).not.toContain('…')
|
|
476
|
+
// Plain-ASCII jog cells use '-' (horizontal) — not the Unicode box
|
|
477
|
+
// corners ('┌','┐','┴', etc.) the default-mode jog draws instead.
|
|
478
|
+
expect(ascii).toMatch(/-{2,}/)
|
|
479
|
+
expect(ascii).not.toMatch(/[┌┐└┘┬┴┼]/)
|
|
480
|
+
for (const label of ['one', 'two', 'three', 'four']) {
|
|
481
|
+
expect(ascii).toContain(label)
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
describe('ASCII class diagram — a detour that clears to the left of its source column', () => {
|
|
487
|
+
// `findClearColumn` tries the column to the right of the source lane
|
|
488
|
+
// before the one to the left, so every existing detour test (including
|
|
489
|
+
// the #514-interaction suite above) happens to clear on the right —
|
|
490
|
+
// `clearSide: 'left'` and the mirrored exit/entry corner-glyph branches it
|
|
491
|
+
// feeds are never exercised. Shifting B's box one column right of A/C's
|
|
492
|
+
// shared connection column (via an asymmetric-width label on B --> C)
|
|
493
|
+
// makes the space immediately right of A's lane blocked for longer than
|
|
494
|
+
// the space immediately left of it, so the search finds its clearance on
|
|
495
|
+
// the left instead — confirmed by rendering and reading the actual
|
|
496
|
+
// routed column positions before writing this test.
|
|
497
|
+
const SOURCE = `classDiagram
|
|
498
|
+
class A
|
|
499
|
+
class B
|
|
500
|
+
class C
|
|
501
|
+
A --> B : a moderately long label here
|
|
502
|
+
A --> C : a moderately long label there
|
|
503
|
+
B --> C : a moderately long label tooXXX`
|
|
504
|
+
|
|
505
|
+
it('routes the detour trunk left of the source lane and still renders every label intact', () => {
|
|
506
|
+
const ascii = renderMermaidASCII(SOURCE, { colorMode: 'none' })
|
|
507
|
+
const lines = ascii.split('\n')
|
|
508
|
+
expect(ascii).not.toContain('…')
|
|
509
|
+
expect(ascii).toContain('a moderately long label here')
|
|
510
|
+
expect(ascii).toContain('a moderately long label there')
|
|
511
|
+
expect(ascii).toContain('a moderately long label tooXXX')
|
|
512
|
+
|
|
513
|
+
// A's own lane column (its box center).
|
|
514
|
+
const aRow = lines.find((l) => l.includes('│ A │'))!
|
|
515
|
+
const fromCX = aRow.indexOf('A')
|
|
516
|
+
|
|
517
|
+
// The row directly above B's top border carries the detour's vertical
|
|
518
|
+
// trunk segment (a lone '│' at the route column, undisturbed by any
|
|
519
|
+
// label text): with a left detour that column sits left of fromCX,
|
|
520
|
+
// never on or right of it (a right detour, the only case the existing
|
|
521
|
+
// #514-interaction suite covers, would put it right of fromCX instead).
|
|
522
|
+
const bTopRow = lines.findIndex(
|
|
523
|
+
(l, i) =>
|
|
524
|
+
l.includes('┌───┐') && (lines[i + 1]?.includes('│ B │') ?? false),
|
|
525
|
+
)
|
|
526
|
+
expect(bTopRow).toBeGreaterThan(0)
|
|
527
|
+
const trunkRow = lines[bTopRow - 1]!
|
|
528
|
+
const trunkCol = trunkRow.indexOf('│')
|
|
529
|
+
expect(trunkCol).toBeGreaterThanOrEqual(0)
|
|
530
|
+
expect(trunkCol).toBeLessThan(fromCX)
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it('joins the trunk to C with a rightward entry jog, since the trunk lands left of C too', () => {
|
|
534
|
+
// Exercises the entry-jog's `routeX < toAnchorX` branch (the trunk is
|
|
535
|
+
// left of where it must enter C, so the jog runs rightward into C) —
|
|
536
|
+
// every existing detour test's trunk lands at or right of its target
|
|
537
|
+
// anchor instead, taking the opposite branch.
|
|
538
|
+
const ascii = renderMermaidASCII(SOURCE, { colorMode: 'none' })
|
|
539
|
+
const lines = ascii.split('\n')
|
|
540
|
+
|
|
541
|
+
const cTopRow = lines.findIndex(
|
|
542
|
+
(l, i) =>
|
|
543
|
+
l.includes('┌───┐') && (lines[i + 1]?.includes('│ C │') ?? false),
|
|
544
|
+
)
|
|
545
|
+
expect(cTopRow).toBeGreaterThan(0)
|
|
546
|
+
const entryRow = lines[cTopRow - 1]!
|
|
547
|
+
// `routeX < toAnchorX` draws '└' at the trunk column and '┐' at the
|
|
548
|
+
// anchor (jogging right into C); the mirrored `else` branch (every
|
|
549
|
+
// other detour test's case) draws '┘' at the trunk column instead — so
|
|
550
|
+
// the corner glyph itself, not just "something precedes the arrow",
|
|
551
|
+
// distinguishes the two branches.
|
|
552
|
+
expect(entryRow).toContain('└')
|
|
553
|
+
expect(entryRow).not.toContain('┘')
|
|
554
|
+
const arrowCol = entryRow.indexOf('▼')
|
|
555
|
+
expect(arrowCol).toBeGreaterThanOrEqual(0)
|
|
556
|
+
expect(entryRow.indexOf('└')).toBeLessThan(arrowCol)
|
|
557
|
+
})
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
describe('ASCII class diagram — a fanned-out group routed upward (target above source)', () => {
|
|
561
|
+
// The "target is above source" routing branch (a reciprocal-style edge
|
|
562
|
+
// whose "to" class was already leveled shallower than its "from" class)
|
|
563
|
+
// exists and is already covered elsewhere in the suite, but never
|
|
564
|
+
// together with a *fanned* group needing its own jog on that branch —
|
|
565
|
+
// `drawJog`'s call sites there (fromJogs/toJogs) were always false.
|
|
566
|
+
// A -> B -> C -> D is a plain chain (so B/C/D each get a genuine,
|
|
567
|
+
// strictly-deeper level), and D -> B (four relationships, forcing a fan)
|
|
568
|
+
// is a back-edge the cycle-guard rejects for relevelling B — so B stays
|
|
569
|
+
// above D, and D's fanned relationships into it draw upward.
|
|
570
|
+
const SOURCE = `classDiagram
|
|
571
|
+
class A
|
|
572
|
+
class B
|
|
573
|
+
class C
|
|
574
|
+
class D
|
|
575
|
+
A --> B : down1
|
|
576
|
+
B --> C : down2
|
|
577
|
+
C --> D : down3
|
|
578
|
+
D --> B : one
|
|
579
|
+
D --> B : two
|
|
580
|
+
D --> B : three
|
|
581
|
+
D --> B : four`
|
|
582
|
+
|
|
583
|
+
it('renders all four upward relationships with distinct arrowheads into B, labels intact', () => {
|
|
584
|
+
const ascii = renderMermaidASCII(SOURCE, { colorMode: 'none' })
|
|
585
|
+
expect(ascii).not.toContain('…')
|
|
586
|
+
for (const label of [
|
|
587
|
+
'down1',
|
|
588
|
+
'down2',
|
|
589
|
+
'down3',
|
|
590
|
+
'one',
|
|
591
|
+
'two',
|
|
592
|
+
'three',
|
|
593
|
+
'four',
|
|
594
|
+
]) {
|
|
595
|
+
expect(ascii).toContain(label)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// B sits above D in the render (the back-edge did not push B deeper).
|
|
599
|
+
const lines = ascii.split('\n')
|
|
600
|
+
const bRow = lines.findIndex((l) => l.includes('│ B │'))
|
|
601
|
+
const dRow = lines.findIndex((l) => l.includes('│ D │'))
|
|
602
|
+
expect(bRow).toBeGreaterThanOrEqual(0)
|
|
603
|
+
expect(dRow).toBeGreaterThan(bRow)
|
|
604
|
+
|
|
605
|
+
// Four upward arrowheads land on the row just below B's bottom border
|
|
606
|
+
// (B's content row + 2: content, bottom border, then the fanned jog/
|
|
607
|
+
// arrow row), one per relationship. This row's outer trunk corners
|
|
608
|
+
// ('┬'/'┴' merged with the box-border tee) come from `drawJog`'s
|
|
609
|
+
// `toJogs` side (line 1008/1005's loop) — B is the *target* of these
|
|
610
|
+
// upward relationships.
|
|
611
|
+
const arrowRow = bRow + 2
|
|
612
|
+
const arrowCols = [...lines[arrowRow]!].flatMap((ch, i) =>
|
|
613
|
+
ch === '▲' ? [i] : [],
|
|
614
|
+
)
|
|
615
|
+
expect(arrowCols.length).toBeGreaterThanOrEqual(4)
|
|
616
|
+
// The outer ends of this row merge the fan's jog into B's own border
|
|
617
|
+
// as clean corners ('┌' left, '┐' right) because `toJogs` pulls the
|
|
618
|
+
// trunk's vertical run back by one row first — without that pullback
|
|
619
|
+
// the merge instead produces a T-junction ('├'/'┤') against the box
|
|
620
|
+
// border one row too early.
|
|
621
|
+
expect(lines[arrowRow]).toContain('┌')
|
|
622
|
+
expect(lines[arrowRow]).toContain('┐')
|
|
623
|
+
expect(lines[arrowRow]).not.toMatch(/[├┤]/)
|
|
624
|
+
|
|
625
|
+
// D is the *source* of these upward relationships — the row directly
|
|
626
|
+
// above D's top border carries the mirrored `fromJogs` trunk (line
|
|
627
|
+
// 989's loop): its outer edges are square corners ('└' left, '┘'
|
|
628
|
+
// right) merged with the fan's horizontal run, not bare '│'s (which is
|
|
629
|
+
// what an unadjusted/un-jogged trunk row would show instead).
|
|
630
|
+
const dTopRow = lines.findIndex(
|
|
631
|
+
(l, i) =>
|
|
632
|
+
l.includes('┌───┐') && (lines[i + 1]?.includes('│ D │') ?? false),
|
|
633
|
+
)
|
|
634
|
+
expect(dTopRow).toBeGreaterThan(0)
|
|
635
|
+
const dJogRow = lines[dTopRow - 1]!
|
|
636
|
+
expect(dJogRow).toContain('└')
|
|
637
|
+
expect(dJogRow).toContain('┘')
|
|
638
|
+
})
|
|
639
|
+
})
|