@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,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
+ })