@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.
Files changed (232) hide show
  1. package/LICENSE +22 -0
  2. package/dist/index.cjs +8 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +146 -0
  5. package/dist/index.d.ts +146 -0
  6. package/dist/index.js +5392 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +36 -0
  9. package/src/__tests__/ascii-arrowhead-direction-1083.test.ts +51 -0
  10. package/src/__tests__/ascii-canvas-first-claim-wins-1093.test.ts +89 -0
  11. package/src/__tests__/ascii-canvas-size-offset-1093.test.ts +95 -0
  12. package/src/__tests__/ascii-canvas-write.test.ts +132 -0
  13. package/src/__tests__/ascii-chain-edge-overlap-1067.test.ts +100 -0
  14. package/src/__tests__/ascii-charset-border-junctions.test.ts +63 -0
  15. package/src/__tests__/ascii-cjk-width.test.ts +150 -0
  16. package/src/__tests__/ascii-class-box-occupancy.test.ts +462 -0
  17. package/src/__tests__/ascii-class-column-width-488-489.test.ts +639 -0
  18. package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +187 -0
  19. package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +114 -0
  20. package/src/__tests__/ascii-class-diagram-compartments.test.ts +70 -0
  21. package/src/__tests__/ascii-class-label-box-collision.test.ts +60 -0
  22. package/src/__tests__/ascii-class-label-row-collision-531.test.ts +135 -0
  23. package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +57 -0
  24. package/src/__tests__/ascii-class-padding.test.ts +104 -0
  25. package/src/__tests__/ascii-class-parent-alignment-971.test.ts +139 -0
  26. package/src/__tests__/ascii-class-parent-alignment-972.test.ts +208 -0
  27. package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +169 -0
  28. package/src/__tests__/ascii-combining-mark-width.test.ts +70 -0
  29. package/src/__tests__/ascii-coords-overlay.test.ts +64 -0
  30. package/src/__tests__/ascii-decision-lr-box-start.test.ts +106 -0
  31. package/src/__tests__/ascii-display-width-unit.test.ts +154 -0
  32. package/src/__tests__/ascii-draw-arrows-coverage.test.ts +224 -0
  33. package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +105 -0
  34. package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +72 -0
  35. package/src/__tests__/ascii-edge-ending-glyphs.test.ts +206 -0
  36. package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +95 -0
  37. package/src/__tests__/ascii-edge-routing-fixes.test.ts +178 -0
  38. package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +103 -0
  39. package/src/__tests__/ascii-edge-style-consistency-1067.test.ts +89 -0
  40. package/src/__tests__/ascii-edge-styles.test.ts +149 -0
  41. package/src/__tests__/ascii-emoji-cluster-width.test.ts +101 -0
  42. package/src/__tests__/ascii-er-box-occupancy.test.ts +251 -0
  43. package/src/__tests__/ascii-er-cardinality.test.ts +40 -0
  44. package/src/__tests__/ascii-er-corner-glyphs.test.ts +249 -0
  45. package/src/__tests__/ascii-er-jog-stray-line.test.ts +111 -0
  46. package/src/__tests__/ascii-er-label-padding.test.ts +118 -0
  47. package/src/__tests__/ascii-er-padding.test.ts +114 -0
  48. package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +458 -0
  49. package/src/__tests__/ascii-er-relationship-overwrite.test.ts +325 -0
  50. package/src/__tests__/ascii-er-stray-connectors.test.ts +344 -0
  51. package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +98 -0
  52. package/src/__tests__/ascii-er-vertical-one-marker.test.ts +110 -0
  53. package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +168 -0
  54. package/src/__tests__/ascii-lane-search.test.ts +207 -0
  55. package/src/__tests__/ascii-multibox-cjk-width.test.ts +184 -0
  56. package/src/__tests__/ascii-multiline.test.ts +288 -0
  57. package/src/__tests__/ascii-padding-edge-cases.test.ts +154 -0
  58. package/src/__tests__/ascii-pathfinder-route-edge.test.ts +184 -0
  59. package/src/__tests__/ascii-sequence-alt-else-label.test.ts +236 -0
  60. package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +217 -0
  61. package/src/__tests__/ascii-sequence-box-group.test.ts +159 -0
  62. package/src/__tests__/ascii-sequence-cjk-width.test.ts +235 -0
  63. package/src/__tests__/ascii-sequence-create-destroy.test.ts +114 -0
  64. package/src/__tests__/ascii-sequence-form-invariants.test.ts +432 -0
  65. package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +219 -0
  66. package/src/__tests__/ascii-sequence-notes.test.ts +61 -0
  67. package/src/__tests__/ascii-sequence-padding.test.ts +119 -0
  68. package/src/__tests__/ascii-sequence-self-arrow.test.ts +206 -0
  69. package/src/__tests__/ascii-shape-diamond.test.ts +38 -0
  70. package/src/__tests__/ascii-shape-rectangle.test.ts +258 -0
  71. package/src/__tests__/ascii-shape-rounded.test.ts +36 -0
  72. package/src/__tests__/ascii-shapes-circle.test.ts +41 -0
  73. package/src/__tests__/ascii-shapes-hexagon.test.ts +42 -0
  74. package/src/__tests__/ascii-shapes-special.test.ts +344 -0
  75. package/src/__tests__/ascii-shapes-stadium.test.ts +217 -0
  76. package/src/__tests__/ascii-shapes-state.test.ts +224 -0
  77. package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +130 -0
  78. package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +90 -0
  79. package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +152 -0
  80. package/src/__tests__/ascii-subgraph-title-padding.test.ts +77 -0
  81. package/src/__tests__/ascii-territory-unit.test.ts +219 -0
  82. package/src/__tests__/ascii-validate.test.ts +220 -0
  83. package/src/__tests__/ascii.test.ts +325 -0
  84. package/src/__tests__/class-arrow-directions.test.ts +505 -0
  85. package/src/__tests__/draw-lines.test.ts +93 -0
  86. package/src/__tests__/edge-cell-styles.test.ts +278 -0
  87. package/src/__tests__/grid-occupancy.test.ts +240 -0
  88. package/src/__tests__/helpers/ascii-form.ts +142 -0
  89. package/src/__tests__/helpers/terminal-display-width.ts +74 -0
  90. package/src/__tests__/pathfinder.test.ts +239 -0
  91. package/src/__tests__/testdata/ascii/ampersand_lhs.txt +18 -0
  92. package/src/__tests__/testdata/ascii/ampersand_lhs_and_rhs.txt +18 -0
  93. package/src/__tests__/testdata/ascii/ampersand_rhs.txt +18 -0
  94. package/src/__tests__/testdata/ascii/ampersand_td_fanin.txt +18 -0
  95. package/src/__tests__/testdata/ascii/ampersand_td_fanout.txt +18 -0
  96. package/src/__tests__/testdata/ascii/ampersand_without_edge.txt +18 -0
  97. package/src/__tests__/testdata/ascii/back_reference_from_child.txt +10 -0
  98. package/src/__tests__/testdata/ascii/backlink_from_bottom.txt +22 -0
  99. package/src/__tests__/testdata/ascii/backlink_from_top.txt +22 -0
  100. package/src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt +20 -0
  101. package/src/__tests__/testdata/ascii/cls_all_relationships.txt +19 -0
  102. package/src/__tests__/testdata/ascii/cls_annotation.txt +29 -0
  103. package/src/__tests__/testdata/ascii/cls_association.txt +14 -0
  104. package/src/__tests__/testdata/ascii/cls_basic.txt +15 -0
  105. package/src/__tests__/testdata/ascii/cls_dependency.txt +14 -0
  106. package/src/__tests__/testdata/ascii/cls_inheritance.txt +20 -0
  107. package/src/__tests__/testdata/ascii/cls_methods.txt +21 -0
  108. package/src/__tests__/testdata/ascii/comments.txt +23 -0
  109. package/src/__tests__/testdata/ascii/custom_padding.txt +10 -0
  110. package/src/__tests__/testdata/ascii/duplicate_labels.txt +19 -0
  111. package/src/__tests__/testdata/ascii/er_attributes.txt +21 -0
  112. package/src/__tests__/testdata/ascii/er_basic.txt +8 -0
  113. package/src/__tests__/testdata/ascii/er_identifying.txt +18 -0
  114. package/src/__tests__/testdata/ascii/flowchart_tb_simple.txt +29 -0
  115. package/src/__tests__/testdata/ascii/graph_bt_direction.txt +28 -0
  116. package/src/__tests__/testdata/ascii/graph_tb_direction.txt +26 -0
  117. package/src/__tests__/testdata/ascii/nested_subgraphs_with_labels.txt +36 -0
  118. package/src/__tests__/testdata/ascii/preserve_order_of_definition.txt +23 -0
  119. package/src/__tests__/testdata/ascii/self_reference.txt +10 -0
  120. package/src/__tests__/testdata/ascii/self_reference_with_edge.txt +10 -0
  121. package/src/__tests__/testdata/ascii/seq_basic.txt +17 -0
  122. package/src/__tests__/testdata/ascii/seq_multiple_messages.txt +25 -0
  123. package/src/__tests__/testdata/ascii/seq_self_message.txt +18 -0
  124. package/src/__tests__/testdata/ascii/single_node.txt +8 -0
  125. package/src/__tests__/testdata/ascii/single_node_longer_name.txt +8 -0
  126. package/src/__tests__/testdata/ascii/subgraph_complex_mixed.txt +38 -0
  127. package/src/__tests__/testdata/ascii/subgraph_complex_nested.txt +49 -0
  128. package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +47 -0
  129. package/src/__tests__/testdata/ascii/subgraph_empty.txt +10 -0
  130. package/src/__tests__/testdata/ascii/subgraph_mixed_nodes.txt +20 -0
  131. package/src/__tests__/testdata/ascii/subgraph_mixed_nodes_td.txt +48 -0
  132. package/src/__tests__/testdata/ascii/subgraph_multiple_edges.txt +32 -0
  133. package/src/__tests__/testdata/ascii/subgraph_multiple_nodes.txt +16 -0
  134. package/src/__tests__/testdata/ascii/subgraph_nested.txt +24 -0
  135. package/src/__tests__/testdata/ascii/subgraph_nested_with_external.txt +30 -0
  136. package/src/__tests__/testdata/ascii/subgraph_node_outside_lr.txt +17 -0
  137. package/src/__tests__/testdata/ascii/subgraph_single_node.txt +16 -0
  138. package/src/__tests__/testdata/ascii/subgraph_td_direction.txt +26 -0
  139. package/src/__tests__/testdata/ascii/subgraph_td_multiple.txt +44 -0
  140. package/src/__tests__/testdata/ascii/subgraph_td_multiple_paddingy.txt +42 -0
  141. package/src/__tests__/testdata/ascii/subgraph_three_levels_nested.txt +32 -0
  142. package/src/__tests__/testdata/ascii/subgraph_three_separate.txt +24 -0
  143. package/src/__tests__/testdata/ascii/subgraph_two_separate.txt +20 -0
  144. package/src/__tests__/testdata/ascii/subgraph_with_labels.txt +20 -0
  145. package/src/__tests__/testdata/ascii/three_nodes.txt +9 -0
  146. package/src/__tests__/testdata/ascii/three_nodes_single_line.txt +8 -0
  147. package/src/__tests__/testdata/ascii/two_layer_single_graph.txt +19 -0
  148. package/src/__tests__/testdata/ascii/two_layer_single_graph_longer_names.txt +19 -0
  149. package/src/__tests__/testdata/ascii/two_nodes_linked.txt +8 -0
  150. package/src/__tests__/testdata/ascii/two_nodes_longer_names.txt +8 -0
  151. package/src/__tests__/testdata/ascii/two_root_nodes.txt +19 -0
  152. package/src/__tests__/testdata/ascii/two_root_nodes_longer_names.txt +19 -0
  153. package/src/__tests__/testdata/ascii/two_single_root_nodes.txt +19 -0
  154. package/src/__tests__/testdata/unicode/ampersand_lhs.txt +18 -0
  155. package/src/__tests__/testdata/unicode/ampersand_lhs_and_rhs.txt +18 -0
  156. package/src/__tests__/testdata/unicode/ampersand_rhs.txt +18 -0
  157. package/src/__tests__/testdata/unicode/ampersand_without_edge.txt +18 -0
  158. package/src/__tests__/testdata/unicode/back_reference_from_child.txt +10 -0
  159. package/src/__tests__/testdata/unicode/backlink_from_bottom.txt +22 -0
  160. package/src/__tests__/testdata/unicode/backlink_from_top.txt +22 -0
  161. package/src/__tests__/testdata/unicode/cls_all_relationships.txt +19 -0
  162. package/src/__tests__/testdata/unicode/cls_annotation.txt +29 -0
  163. package/src/__tests__/testdata/unicode/cls_association.txt +14 -0
  164. package/src/__tests__/testdata/unicode/cls_basic.txt +15 -0
  165. package/src/__tests__/testdata/unicode/cls_dependency.txt +14 -0
  166. package/src/__tests__/testdata/unicode/cls_inheritance.txt +20 -0
  167. package/src/__tests__/testdata/unicode/cls_methods.txt +21 -0
  168. package/src/__tests__/testdata/unicode/comments.txt +23 -0
  169. package/src/__tests__/testdata/unicode/duplicate_labels.txt +19 -0
  170. package/src/__tests__/testdata/unicode/er_attributes.txt +21 -0
  171. package/src/__tests__/testdata/unicode/er_basic.txt +8 -0
  172. package/src/__tests__/testdata/unicode/er_identifying.txt +18 -0
  173. package/src/__tests__/testdata/unicode/graph_bt_direction.txt +28 -0
  174. package/src/__tests__/testdata/unicode/preserve_order_of_definition.txt +23 -0
  175. package/src/__tests__/testdata/unicode/self_reference.txt +10 -0
  176. package/src/__tests__/testdata/unicode/self_reference_with_edge.txt +10 -0
  177. package/src/__tests__/testdata/unicode/seq_basic.txt +17 -0
  178. package/src/__tests__/testdata/unicode/seq_multiple_messages.txt +25 -0
  179. package/src/__tests__/testdata/unicode/seq_self_message.txt +18 -0
  180. package/src/__tests__/testdata/unicode/single_node.txt +8 -0
  181. package/src/__tests__/testdata/unicode/single_node_longer_name.txt +8 -0
  182. package/src/__tests__/testdata/unicode/three_nodes.txt +9 -0
  183. package/src/__tests__/testdata/unicode/three_nodes_single_line.txt +8 -0
  184. package/src/__tests__/testdata/unicode/two_layer_single_graph.txt +19 -0
  185. package/src/__tests__/testdata/unicode/two_layer_single_graph_longer_names.txt +19 -0
  186. package/src/__tests__/testdata/unicode/two_nodes_linked.txt +8 -0
  187. package/src/__tests__/testdata/unicode/two_nodes_longer_names.txt +8 -0
  188. package/src/__tests__/testdata/unicode/two_root_nodes.txt +19 -0
  189. package/src/__tests__/testdata/unicode/two_root_nodes_longer_names.txt +19 -0
  190. package/src/__tests__/testdata/unicode/two_single_root_nodes.txt +19 -0
  191. package/src/__tests__/xychart-ascii.test.ts +376 -0
  192. package/src/ansi.ts +490 -0
  193. package/src/canvas.ts +757 -0
  194. package/src/class-diagram.ts +2001 -0
  195. package/src/converter.ts +446 -0
  196. package/src/coords.ts +58 -0
  197. package/src/display-width.ts +151 -0
  198. package/src/draw-arrows.ts +593 -0
  199. package/src/draw-boxes.ts +267 -0
  200. package/src/draw-bundles.ts +611 -0
  201. package/src/draw-lines.ts +174 -0
  202. package/src/draw-subgraphs.ts +108 -0
  203. package/src/draw.ts +350 -0
  204. package/src/edge-bundling.ts +435 -0
  205. package/src/edge-cell-styles.ts +209 -0
  206. package/src/edge-routing.ts +1070 -0
  207. package/src/er-diagram.ts +1488 -0
  208. package/src/flowchart.ts +94 -0
  209. package/src/grid-occupancy.ts +234 -0
  210. package/src/grid.ts +1309 -0
  211. package/src/hyperlinks.ts +248 -0
  212. package/src/index.ts +163 -0
  213. package/src/lane-search.ts +68 -0
  214. package/src/multiline-utils.ts +82 -0
  215. package/src/pathfinder.ts +448 -0
  216. package/src/registry.ts +88 -0
  217. package/src/sequence.ts +1318 -0
  218. package/src/shapes/circle.ts +31 -0
  219. package/src/shapes/corners.ts +273 -0
  220. package/src/shapes/diamond.ts +31 -0
  221. package/src/shapes/hexagon.ts +35 -0
  222. package/src/shapes/index.ts +123 -0
  223. package/src/shapes/rectangle.ts +199 -0
  224. package/src/shapes/rounded.ts +31 -0
  225. package/src/shapes/special.ts +360 -0
  226. package/src/shapes/stadium.ts +122 -0
  227. package/src/shapes/state.ts +204 -0
  228. package/src/shapes/types.ts +78 -0
  229. package/src/territory.ts +136 -0
  230. package/src/types.ts +454 -0
  231. package/src/validate.ts +189 -0
  232. package/src/xychart.ts +1085 -0
@@ -0,0 +1,505 @@
1
+ /**
2
+ * Comprehensive tests for class diagram arrow directions.
3
+ *
4
+ * Ensures all relationship types have correctly oriented arrows:
5
+ * - Inheritance/Realization: hollow triangles point toward parent/interface
6
+ * - Association/Dependency: filled arrows point from source to target
7
+ * - Composition/Aggregation: diamonds are omnidirectional
8
+ */
9
+
10
+ import { describe, test, expect } from 'vitest'
11
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
12
+
13
+ describe('Class Diagram Arrow Directions', () => {
14
+ // ============================================================================
15
+ // INHERITANCE (<|--)
16
+ // ============================================================================
17
+
18
+ describe('Inheritance (<|--)', () => {
19
+ test('parent above child - triangle points UP toward parent', () => {
20
+ const diagram = `classDiagram
21
+ Animal <|-- Dog`
22
+ const result = renderMermaidASCII(diagram)
23
+
24
+ // Should contain upward triangle
25
+ expect(result).toContain('△')
26
+ expect(result).not.toContain('▽')
27
+
28
+ // Parent should be above child
29
+ const lines = result.split('\n')
30
+ const animalLine = lines.findIndex((l) => l.includes('Animal'))
31
+ const dogLine = lines.findIndex((l) => l.includes('Dog'))
32
+ expect(animalLine).toBeLessThan(dogLine)
33
+ })
34
+
35
+ test('multiple inheritance creates separate arrows', () => {
36
+ const diagram = `classDiagram
37
+ Animal <|-- Dog
38
+ Animal <|-- Cat
39
+ Dog <|-- Puppy`
40
+ const result = renderMermaidASCII(diagram)
41
+
42
+ // Animal should be at top, then Dog/Cat, then Puppy
43
+ const lines = result.split('\n')
44
+ const animalLine = lines.findIndex((l) => l.includes('Animal'))
45
+ const dogLine = lines.findIndex((l) => l.includes('Dog'))
46
+ const catLine = lines.findIndex((l) => l.includes('Cat'))
47
+ const puppyLine = lines.findIndex((l) => l.includes('Puppy'))
48
+
49
+ expect(animalLine).toBeLessThan(dogLine)
50
+ expect(animalLine).toBeLessThan(catLine)
51
+ expect(dogLine).toBeLessThan(puppyLine)
52
+ })
53
+
54
+ test('multi-level inheritance - all triangles point UP', () => {
55
+ const diagram = `classDiagram
56
+ Animal <|-- Mammal
57
+ Mammal <|-- Dog`
58
+ const result = renderMermaidASCII(diagram)
59
+
60
+ // Verify ordering: Animal > Mammal > Dog (top to bottom)
61
+ const lines = result.split('\n')
62
+ const animalLine = lines.findIndex((l) => l.includes('Animal'))
63
+ const mammalLine = lines.findIndex((l) => l.includes('Mammal'))
64
+ const dogLine = lines.findIndex((l) => l.includes('Dog'))
65
+
66
+ expect(animalLine).toBeLessThan(mammalLine)
67
+ expect(mammalLine).toBeLessThan(dogLine)
68
+
69
+ // All triangles should point up
70
+ expect(result.match(/△/g)?.length).toBe(2)
71
+ })
72
+
73
+ test('multiple inheritance from same parent', () => {
74
+ const diagram = `classDiagram
75
+ Animal <|-- Dog
76
+ Animal <|-- Cat`
77
+ const result = renderMermaidASCII(diagram)
78
+
79
+ // Animal should be above both children
80
+ const lines = result.split('\n')
81
+ const animalLine = lines.findIndex((l) => l.includes('Animal'))
82
+ const dogLine = lines.findIndex((l) => l.includes('Dog'))
83
+ const catLine = lines.findIndex((l) => l.includes('Cat'))
84
+
85
+ expect(animalLine).toBeLessThan(dogLine)
86
+ expect(animalLine).toBeLessThan(catLine)
87
+
88
+ // Should have at least one triangle pointing up (may merge visually)
89
+ expect(result).toContain('△')
90
+ })
91
+
92
+ test('ASCII mode uses ^ for upward triangle', () => {
93
+ const diagram = `classDiagram
94
+ Animal <|-- Dog`
95
+ const result = renderMermaidASCII(diagram, { useAscii: true })
96
+
97
+ expect(result).toContain('^')
98
+ expect(result).not.toContain('v')
99
+ })
100
+ })
101
+
102
+ // ============================================================================
103
+ // ASSOCIATION (-->)
104
+ // ============================================================================
105
+
106
+ describe('Association (-->)', () => {
107
+ test('source above target - arrow points DOWN', () => {
108
+ const diagram = `classDiagram
109
+ Person --> Address`
110
+ const result = renderMermaidASCII(diagram)
111
+
112
+ // Should contain downward arrow
113
+ expect(result).toContain('▼')
114
+ expect(result).not.toContain('▲')
115
+
116
+ // Person should be above Address
117
+ const lines = result.split('\n')
118
+ const personLine = lines.findIndex((l) => l.includes('Person'))
119
+ const addressLine = lines.findIndex((l) => l.includes('Address'))
120
+ expect(personLine).toBeLessThan(addressLine)
121
+ })
122
+
123
+ test('multiple associations from same source', () => {
124
+ const diagram = `classDiagram
125
+ Person --> Address
126
+ Person --> Phone`
127
+ const result = renderMermaidASCII(diagram)
128
+
129
+ // Person should be above both targets
130
+ const lines = result.split('\n')
131
+ const personLine = lines.findIndex((l) => l.includes('Person'))
132
+ const addressLine = lines.findIndex((l) => l.includes('Address'))
133
+ const phoneLine = lines.findIndex((l) => l.includes('Phone'))
134
+
135
+ expect(personLine).toBeLessThan(addressLine)
136
+ expect(personLine).toBeLessThan(phoneLine)
137
+ })
138
+
139
+ test('chain of associations', () => {
140
+ const diagram = `classDiagram
141
+ A --> B
142
+ B --> C`
143
+ const result = renderMermaidASCII(diagram)
144
+
145
+ // A > B > C ordering
146
+ const lines = result.split('\n')
147
+ const aLine = lines.findIndex((l) => l.includes('│ A │'))
148
+ const bLine = lines.findIndex((l) => l.includes('│ B │'))
149
+ const cLine = lines.findIndex((l) => l.includes('│ C │'))
150
+
151
+ expect(aLine).toBeLessThan(bLine)
152
+ expect(bLine).toBeLessThan(cLine)
153
+
154
+ // Both arrows point down
155
+ expect(result.match(/▼/g)?.length).toBe(2)
156
+ })
157
+
158
+ test('ASCII mode uses v for downward arrow', () => {
159
+ const diagram = `classDiagram
160
+ Person --> Address`
161
+ const result = renderMermaidASCII(diagram, { useAscii: true })
162
+
163
+ expect(result).toContain('v')
164
+ expect(result).not.toContain('^')
165
+ })
166
+
167
+ test('association routed around an obstruction still points down (non-hierarchical detour case)', () => {
168
+ // Same detour/collision-avoidance routing as the realization test
169
+ // below, but with a plain association so the target-below-source
170
+ // detour branch's `isHierarchical` ternary is exercised on its
171
+ // non-hierarchical ('down') side too, not just the hierarchical
172
+ // ('up') side.
173
+ const diagram = `classDiagram
174
+ class Flyable {
175
+ <<interface>>
176
+ +fly() void
177
+ }
178
+ class Helper {
179
+ +assist() void
180
+ }
181
+ class Bird {
182
+ +fly() void
183
+ }
184
+ Bird --> Helper
185
+ Helper --> Flyable
186
+ Bird --> Flyable`
187
+ const result = renderMermaidASCII(diagram)
188
+
189
+ expect(result).toContain('▼')
190
+ expect(result).not.toContain('▲')
191
+ })
192
+ })
193
+
194
+ // ============================================================================
195
+ // DEPENDENCY (..>)
196
+ // ============================================================================
197
+
198
+ describe('Dependency (..>)', () => {
199
+ test('source above target - arrow points DOWN', () => {
200
+ const diagram = `classDiagram
201
+ Client ..> Server`
202
+ const result = renderMermaidASCII(diagram)
203
+
204
+ expect(result).toContain('▼')
205
+ expect(result).not.toContain('▲')
206
+
207
+ const lines = result.split('\n')
208
+ const clientLine = lines.findIndex((l) => l.includes('Client'))
209
+ const serverLine = lines.findIndex((l) => l.includes('Server'))
210
+ expect(clientLine).toBeLessThan(serverLine)
211
+ })
212
+
213
+ test('multiple dependencies', () => {
214
+ const diagram = `classDiagram
215
+ Client ..> Server
216
+ Client ..> Database`
217
+ const result = renderMermaidASCII(diagram)
218
+
219
+ const lines = result.split('\n')
220
+ const clientLine = lines.findIndex((l) => l.includes('Client'))
221
+ const serverLine = lines.findIndex((l) => l.includes('Server'))
222
+ const dbLine = lines.findIndex((l) => l.includes('Database'))
223
+
224
+ expect(clientLine).toBeLessThan(serverLine)
225
+ expect(clientLine).toBeLessThan(dbLine)
226
+ })
227
+
228
+ test('ASCII mode uses v for downward arrow', () => {
229
+ const diagram = `classDiagram
230
+ Client ..> Server`
231
+ const result = renderMermaidASCII(diagram, { useAscii: true })
232
+
233
+ expect(result).toContain('v')
234
+ })
235
+ })
236
+
237
+ // ============================================================================
238
+ // REALIZATION (..|>)
239
+ // ============================================================================
240
+
241
+ describe('Realization (..|>)', () => {
242
+ test('implementation above interface - triangle points down into interface', () => {
243
+ // Circle ..|> Shape means "Circle implements Shape". Layout places
244
+ // "from" above "to" for every relationship type (matching real
245
+ // mermaid.js — see issue #446), so Circle (implementation/from) is
246
+ // placed ABOVE Shape (interface/to), with the hollow triangle sitting
247
+ // just above Shape, its TIP touching (pointing down into) the
248
+ // interface box — a hollow triangle points toward whichever box it's
249
+ // adjacent to, not away from it.
250
+ const diagram = `classDiagram
251
+ Circle ..|> Shape`
252
+ const result = renderMermaidASCII(diagram)
253
+
254
+ // Circle (implementation) should be above Shape (interface)
255
+ const lines = result.split('\n')
256
+ const shapeLine = lines.findIndex((l) => l.includes('Shape'))
257
+ const circleLine = lines.findIndex((l) => l.includes('Circle'))
258
+ expect(circleLine).toBeLessThan(shapeLine)
259
+ expect(result).toContain('▽')
260
+ })
261
+
262
+ test('realization with <|.. syntax (marker at from end)', () => {
263
+ // Shape <|.. Circle means "Circle implements Shape" (same as Circle ..|> Shape)
264
+ const diagram = `classDiagram
265
+ Shape <|.. Circle`
266
+ const result = renderMermaidASCII(diagram)
267
+
268
+ // Shape (interface) should be above Circle (implementation)
269
+ const lines = result.split('\n')
270
+ const shapeLine = lines.findIndex((l) => l.includes('Shape'))
271
+ const circleLine = lines.findIndex((l) => l.includes('Circle'))
272
+ expect(shapeLine).toBeLessThan(circleLine)
273
+ expect(result).toContain('△')
274
+ })
275
+
276
+ test('multiple implementations', () => {
277
+ // Circle and Square both implement Shape
278
+ const diagram = `classDiagram
279
+ Circle ..|> Shape
280
+ Square ..|> Shape`
281
+ const result = renderMermaidASCII(diagram)
282
+
283
+ // Both implementations above the shared interface
284
+ const lines = result.split('\n')
285
+ const shapeLine = lines.findIndex((l) => l.includes('Shape'))
286
+ const circleLine = lines.findIndex((l) => l.includes('Circle'))
287
+ const squareLine = lines.findIndex((l) => l.includes('Square'))
288
+
289
+ expect(circleLine).toBeLessThan(shapeLine)
290
+ expect(squareLine).toBeLessThan(shapeLine)
291
+ // At least one triangle (may merge visually if same connection point).
292
+ // markerAt='to' here (Shape), so the triangle points down into it: ▽.
293
+ expect(result).toContain('▽')
294
+ })
295
+
296
+ test('realization edge routed around an obstruction still points down into the interface', () => {
297
+ // Bird realizes Flyable two levels down (Helper sits in between,
298
+ // forced there by Bird-->Helper and Helper-->Flyable), so Bird's
299
+ // realization edge can't run straight down — it must detour around
300
+ // Helper's box via the collision-avoidance routing path. This is a
301
+ // different code path from the direct/no-collision case covered by
302
+ // the tests above, and had its own separate rotation bug that was
303
+ // missed in the original fix (only caught in review): the detour
304
+ // branch's marker-drawing code needed the same `isHierarchical`
305
+ // rotation compensation as the direct-routing branches.
306
+ const diagram = `classDiagram
307
+ class Flyable {
308
+ <<interface>>
309
+ +fly() void
310
+ }
311
+ class Helper {
312
+ +assist() void
313
+ }
314
+ class Bird {
315
+ +fly() void
316
+ }
317
+ Bird --> Helper
318
+ Helper --> Flyable
319
+ Bird ..|> Flyable`
320
+ const result = renderMermaidASCII(diagram)
321
+
322
+ // The realization edge is declared last, so it draws last and its
323
+ // marker wins where it lands on the same cell as Helper-->Flyable's
324
+ // own (unrelated, association-style) arrowhead — a separate,
325
+ // pre-existing limitation of this renderer's last-write-wins cell
326
+ // model, not something this test is about.
327
+ expect(result).toContain('▽')
328
+ })
329
+ })
330
+
331
+ // ============================================================================
332
+ // COMPOSITION & AGGREGATION (omnidirectional diamonds)
333
+ // ============================================================================
334
+
335
+ describe('Composition (*--) and Aggregation (o--)', () => {
336
+ test('composition - diamond is omnidirectional', () => {
337
+ const diagram = `classDiagram
338
+ Car *-- Engine`
339
+ const result = renderMermaidASCII(diagram)
340
+
341
+ // Should contain filled diamond
342
+ expect(result).toContain('◆')
343
+ })
344
+
345
+ test('aggregation - hollow diamond is omnidirectional', () => {
346
+ const diagram = `classDiagram
347
+ Team o-- Player`
348
+ const result = renderMermaidASCII(diagram)
349
+
350
+ // Should contain hollow diamond
351
+ expect(result).toContain('◇')
352
+ })
353
+ })
354
+
355
+ // ============================================================================
356
+ // MIXED SCENARIOS
357
+ // ============================================================================
358
+
359
+ describe('Mixed Relationship Scenarios', () => {
360
+ test('all 6 relationship types together', () => {
361
+ const diagram = `classDiagram
362
+ A <|-- B : inheritance
363
+ C *-- D : composition
364
+ E o-- F : aggregation
365
+ G --> H : association
366
+ I ..> J : dependency
367
+ K ..|> L : realization`
368
+ const result = renderMermaidASCII(diagram)
369
+
370
+ // A above B (inheritance, marker at 'from'/A) — marker sits just below
371
+ // A pointing up into it: △. K above L (realization, marker at
372
+ // 'to'/L) — marker sits just above L pointing down into it: ▽. Both
373
+ // point toward their parent/interface; the glyph differs because the
374
+ // marker's position relative to its target differs.
375
+ expect(result.match(/△/g)?.length).toBe(1)
376
+ expect(result.match(/▽/g)?.length).toBe(1)
377
+
378
+ // Downward arrows for association and dependency
379
+ expect(result.match(/▼/g)?.length).toBe(2)
380
+
381
+ // Diamonds for composition and aggregation
382
+ expect(result).toContain('◆')
383
+ expect(result).toContain('◇')
384
+ })
385
+
386
+ test('inheritance with association - different arrow directions', () => {
387
+ const diagram = `classDiagram
388
+ Animal <|-- Dog
389
+ Dog --> Food`
390
+ const result = renderMermaidASCII(diagram)
391
+
392
+ // Should have both up triangle (inheritance) and down arrow (association)
393
+ expect(result).toContain('△')
394
+ expect(result).toContain('▼')
395
+ })
396
+
397
+ test('circular reference creates valid layout', () => {
398
+ const diagram = `classDiagram
399
+ A --> B
400
+ B --> C
401
+ C ..> A`
402
+ const result = renderMermaidASCII(diagram)
403
+
404
+ // Cycles may create mixed arrow directions (up and down) to avoid overlaps
405
+ // Just verify arrows are present and classes are rendered
406
+ const hasUpArrow = result.includes('▲')
407
+ const hasDownArrow = result.includes('▼')
408
+ expect(hasUpArrow || hasDownArrow).toBe(true)
409
+ expect(result).toContain('│ A │')
410
+ expect(result).toContain('│ B │')
411
+ expect(result).toContain('│ C │')
412
+ })
413
+
414
+ test('circular reference with a realization edge points the hollow triangle correctly (same-level routing)', () => {
415
+ // Same cyclic structure as the test above (which forces A and C onto
416
+ // the same level, routing their connecting edge below both boxes and
417
+ // back up — a third, separate marker-drawing branch from the two
418
+ // "target below/above source" cases covered elsewhere in this file),
419
+ // but with the cycle-closing edge as a realization instead of a
420
+ // dependency, so this same-level branch's `isHierarchical` rotation
421
+ // compensation is exercised on its hierarchical side too.
422
+ const diagram = `classDiagram
423
+ A --> B
424
+ B --> C
425
+ C ..|> A`
426
+ const result = renderMermaidASCII(diagram)
427
+
428
+ // The hollow triangle must point up, into A (the realized interface,
429
+ // approached from below after routing under B) — not down, away from
430
+ // it.
431
+ expect(result).toContain('△')
432
+ expect(result).not.toContain('▽')
433
+ })
434
+ })
435
+
436
+ // ============================================================================
437
+ // ASCII vs UNICODE CONSISTENCY
438
+ // ============================================================================
439
+
440
+ describe('ASCII and Unicode Mode Consistency', () => {
441
+ test('same diagram produces consistent layouts in both modes', () => {
442
+ const diagram = `classDiagram
443
+ Animal <|-- Dog
444
+ Person --> Address`
445
+
446
+ const unicode = renderMermaidASCII(diagram)
447
+ const ascii = renderMermaidASCII(diagram, { useAscii: true })
448
+
449
+ // Both should have same node ordering
450
+ const unicodeLines = unicode.split('\n')
451
+ const asciiLines = ascii.split('\n')
452
+
453
+ const uAnimal = unicodeLines.findIndex((l) => l.includes('Animal'))
454
+ const uDog = unicodeLines.findIndex((l) => l.includes('Dog'))
455
+ const aPerson = asciiLines.findIndex((l) => l.includes('Person'))
456
+ const aAddress = asciiLines.findIndex((l) => l.includes('Address'))
457
+
458
+ expect(uAnimal).toBeLessThan(uDog)
459
+ expect(aPerson).toBeLessThan(aAddress)
460
+
461
+ // Unicode has △ and ▼, ASCII has ^ and v
462
+ expect(unicode).toContain('△')
463
+ expect(unicode).toContain('▼')
464
+ expect(ascii).toContain('^')
465
+ expect(ascii).toContain('v')
466
+ })
467
+ })
468
+
469
+ // ============================================================================
470
+ // EDGE CASES
471
+ // ============================================================================
472
+
473
+ describe('Edge Cases', () => {
474
+ test('single inheritance relationship', () => {
475
+ const diagram = `classDiagram
476
+ A <|-- B`
477
+ const result = renderMermaidASCII(diagram)
478
+
479
+ expect(result).toContain('△')
480
+ const lines = result.split('\n')
481
+ const aLine = lines.findIndex((l) => l.includes('│ A │'))
482
+ const bLine = lines.findIndex((l) => l.includes('│ B │'))
483
+ expect(aLine).toBeLessThan(bLine)
484
+ })
485
+
486
+ test('classes with members maintain arrow directions', () => {
487
+ const diagram = `classDiagram
488
+ class Animal {
489
+ +String name
490
+ +eat() void
491
+ }
492
+ class Dog {
493
+ +bark() void
494
+ }
495
+ Animal <|-- Dog`
496
+ const result = renderMermaidASCII(diagram)
497
+
498
+ expect(result).toContain('△')
499
+ const lines = result.split('\n')
500
+ const animalLine = lines.findIndex((l) => l.includes('Animal'))
501
+ const dogLine = lines.findIndex((l) => l.includes('Dog'))
502
+ expect(animalLine).toBeLessThan(dogLine)
503
+ })
504
+ })
505
+ })
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Direct unit tests for drawLine's diagonal routing branches
3
+ * (UpperLeft/UpperRight/LowerLeft/LowerRight in src/ascii/draw-lines.ts).
4
+ *
5
+ * These branches only fire when `from` and `to` differ on both axes.
6
+ * Every real call site (draw-arrows.ts's drawPath, draw-bundles.ts) feeds
7
+ * drawLine coordinates derived from an A* grid path — and the pathfinder
8
+ * only ever moves 4-directionally (see pathfinder.ts's MOVE_DIRS) — so no
9
+ * Mermaid diagram through the renderer can actually produce a diagonal
10
+ * `from`/`to` pair here; the pure-vertical/horizontal branches are already
11
+ * covered that way. Testing this diagonal routing therefore has to call
12
+ * drawLine directly, the same way pathfinder.test.ts unit-tests getPath
13
+ * directly with hand-built inputs rather than through a full render.
14
+ */
15
+ import { describe, it, expect } from 'vitest'
16
+ import { drawLine } from '../draw-lines.ts'
17
+ import { mkCanvas } from '../canvas.ts'
18
+
19
+ describe('drawLine diagonal routing', () => {
20
+ it('routes UpperLeft as a horizontal-then-vertical bend', () => {
21
+ const canvas = mkCanvas(10, 10)
22
+ const drawn = drawLine(canvas, { x: 8, y: 8 }, { x: 2, y: 2 }, 1, -1, false)
23
+ expect(canvas[7]![8]).toBe('─')
24
+ expect(canvas[2]![8]).toBe('─')
25
+ expect(canvas[2]![7]).toBe('│')
26
+ expect(canvas[2]![3]).toBe('│')
27
+ expect(drawn.length).toBe(6 + 5)
28
+ })
29
+
30
+ it('routes UpperRight as a horizontal-then-vertical bend', () => {
31
+ const canvas = mkCanvas(10, 10)
32
+ const drawn = drawLine(canvas, { x: 2, y: 8 }, { x: 8, y: 2 }, 1, -1, false)
33
+ expect(canvas[3]![8]).toBe('─')
34
+ expect(canvas[8]![8]).toBe('─')
35
+ expect(canvas[8]![7]).toBe('│')
36
+ expect(canvas[8]![3]).toBe('│')
37
+ expect(drawn.length).toBe(6 + 5)
38
+ })
39
+
40
+ it('routes LowerLeft as a horizontal-then-vertical bend', () => {
41
+ const canvas = mkCanvas(10, 10)
42
+ const drawn = drawLine(canvas, { x: 8, y: 2 }, { x: 2, y: 8 }, 1, -1, false)
43
+ expect(canvas[7]![2]).toBe('─')
44
+ expect(canvas[2]![2]).toBe('─')
45
+ expect(canvas[2]![3]).toBe('│')
46
+ expect(canvas[2]![7]).toBe('│')
47
+ expect(drawn.length).toBe(6 + 5)
48
+ })
49
+
50
+ describe('LowerRight', () => {
51
+ it('draws a straight vertical line when x delta is 1 (source-aligned)', () => {
52
+ const canvas = mkCanvas(10, 10)
53
+ const drawn = drawLine(
54
+ canvas,
55
+ { x: 5, y: 2 },
56
+ { x: 6, y: 8 },
57
+ 1,
58
+ -1,
59
+ false,
60
+ )
61
+ for (let y = 3; y <= 7; y++) {
62
+ expect(canvas[5]![y]).toBe('│')
63
+ }
64
+ expect(canvas[6]![8]).toBe(' ')
65
+ expect(drawn.every((c) => c.x === 5)).toBe(true)
66
+ expect(drawn.length).toBe(5)
67
+ })
68
+
69
+ it('routes as a horizontal-then-vertical bend when x delta is greater than 1', () => {
70
+ const canvas = mkCanvas(10, 10)
71
+ const drawn = drawLine(
72
+ canvas,
73
+ { x: 2, y: 2 },
74
+ { x: 8, y: 8 },
75
+ 1,
76
+ -1,
77
+ false,
78
+ )
79
+ expect(canvas[3]![2]).toBe('─')
80
+ expect(canvas[8]![2]).toBe('─')
81
+ expect(canvas[8]![3]).toBe('│')
82
+ expect(canvas[8]![7]).toBe('│')
83
+ expect(drawn.length).toBe(6 + 5)
84
+ })
85
+
86
+ it('uses ASCII characters in ASCII mode', () => {
87
+ const canvas = mkCanvas(10, 10)
88
+ drawLine(canvas, { x: 2, y: 2 }, { x: 8, y: 8 }, 1, -1, true)
89
+ expect(canvas[3]![2]).toBe('-')
90
+ expect(canvas[8]![3]).toBe('|')
91
+ })
92
+ })
93
+ })