@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,40 @@
1
+ // ============================================================================
2
+ // ASCII ER diagram crow's-foot cardinality marker tests
3
+ // ============================================================================
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
7
+
8
+ describe('ASCII ER crow-foot cardinality markers', () => {
9
+ it('mirrors the zero-one marker orientation on the left side (regression)', () => {
10
+ // Left-side markers sit at the left entity's right edge, pointing away
11
+ // from it (line nearest the entity, circle furthest): "|o". Before the
12
+ // fix, getCrowsFootChars() always returned "o|" regardless of side,
13
+ // which is only correct on the right side.
14
+ const ascii = renderMermaidASCII(
15
+ `erDiagram
16
+ WIDGET |o--|| GADGET : rel`,
17
+ { useAscii: true },
18
+ )
19
+ expect(ascii).toContain('|o')
20
+
21
+ const unicode = renderMermaidASCII(`erDiagram
22
+ WIDGET |o--|| GADGET : rel`)
23
+ expect(unicode).toContain('│○')
24
+ })
25
+
26
+ it('keeps the zero-one marker orientation on the right side', () => {
27
+ // Right-side markers sit at the right entity's left edge, pointing away
28
+ // from it: circle nearest the entity, line furthest: "o|".
29
+ const ascii = renderMermaidASCII(
30
+ `erDiagram
31
+ WIDGET ||--o| GADGET : rel`,
32
+ { useAscii: true },
33
+ )
34
+ expect(ascii).toContain('o|')
35
+
36
+ const unicode = renderMermaidASCII(`erDiagram
37
+ WIDGET ||--o| GADGET : rel`)
38
+ expect(unicode).toContain('○│')
39
+ })
40
+ })
@@ -0,0 +1,249 @@
1
+ // ============================================================================
2
+ // ASCII ER diagram: routed relationship lines get a corner glyph at each
3
+ // jog/detour turn (issue #414)
4
+ //
5
+ // renderErAscii drew every relationship line, jog, and detour purely out of
6
+ // plain '─'/'│' segments meeting at unmarked right angles, even at a point
7
+ // where the routed path actually changes direction. In a dense diagram this
8
+ // reads ambiguously: a viewer can't tell "this line turns here" from "this
9
+ // line ends here and an unrelated line begins right next to it".
10
+ //
11
+ // The fix draws a box-drawing corner glyph ('┌'/'┐'/'└'/'┘', or '+' in ASCII
12
+ // mode) at each of the three routing paths that can change direction:
13
+ // - the same-row detour beneath an obstructing entity
14
+ // (`obstructionBottom`/`detourY`)
15
+ // - a vertical relationship's horizontal jog (`needsJog`/`pickBandY`/`midY`)
16
+ // - a vertical relationship's multi-row-obstruction bypass
17
+ // (`multiRowObstruction`/`routingX`)
18
+ // through the same setCGuarded occupancy guard as every other relationship
19
+ // write, so a corner can never overwrite an entity box border or another
20
+ // relationship's label (the same corruption shape #391 fixed).
21
+ //
22
+ // Each assertion below pins an exact substring of the rendered output that
23
+ // was verified (via a before/after diff against the pre-fix renderer) to
24
+ // contain the new corner glyph at the exact cell the path turns, and to be
25
+ // completely absent from the pre-fix render. Reverting the fix in
26
+ // src/ascii/er-diagram.ts makes every one of these assertions fail — the
27
+ // dashes/lines render as before, without the corner character.
28
+ // ============================================================================
29
+
30
+ import { describe, it, expect } from 'vitest'
31
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
32
+
33
+ describe('ASCII ER relationship routing draws a corner glyph at each turn (issue #414)', () => {
34
+ it('draws corner glyphs at a same-row detour around an obstructing entity', () => {
35
+ // FIRST_ENTITY and THIRD_ENTITY are same-row, non-adjacent (MIDDLE_ENTITY
36
+ // sits between them), so "relates_to" must detour beneath
37
+ // MIDDLE_ENTITY's box. FOURTH_ENTITY/FIFTH_ENTITY exist only to keep this
38
+ // component's row width at 3 (sqrt-based row limit), same trick REPRO_*
39
+ // in ascii-er-relationship-label-corruption-350.test.ts uses.
40
+ const ascii = renderMermaidASCII(
41
+ `erDiagram
42
+ FIRST_ENTITY ||--|{ MIDDLE_ENTITY : contains
43
+ MIDDLE_ENTITY ||--o{ THIRD_ENTITY : includes
44
+ FIRST_ENTITY ||--o| THIRD_ENTITY : relates_to
45
+ FIRST_ENTITY ||--o{ FOURTH_ENTITY : owns
46
+ MIDDLE_ENTITY ||--o{ FIFTH_ENTITY : tracks`,
47
+ { colorMode: 'none' },
48
+ )
49
+ // The detour's horizontal run opens with '└' (turning up out of the
50
+ // left vertical drop) and closes with '┘' (turning up into the right
51
+ // vertical drop) instead of plain '─' at both ends.
52
+ //
53
+ // MIDDLE_ENTITY's own vertical "one" marker for its "tracks"
54
+ // relationship no longer crosses this detour row directly — #390's
55
+ // flush-marker change (merged after this test was first written) puts
56
+ // the marker flush against MIDDLE_ENTITY's own border, one row above
57
+ // the detour line, rather than inset into the detour's own row the
58
+ // way the old inset positioning did. Checked separately below instead
59
+ // of assuming they share a row.
60
+ expect(ascii).toContain('└─────────────── tracks ────────────┘')
61
+ const lines = ascii.split('\n')
62
+ const detourLineIndex = lines.findIndex((l) =>
63
+ l.includes('└─────────────── tracks ────────────┘'),
64
+ )
65
+ expect(detourLineIndex).toBeGreaterThan(0)
66
+ // MIDDLE_ENTITY's "tracks" marker ('┼', flush against its own border)
67
+ // sits on the row immediately above the detour line.
68
+ expect(lines[detourLineIndex - 1]).toMatch(/┼/)
69
+ expect(ascii).toContain('relates_to')
70
+ expect(ascii).toContain('tracks')
71
+ expect(ascii).not.toMatch(/[a-z]+[─│┊╌][a-z]+/)
72
+ })
73
+
74
+ it("draws corner glyphs at a vertical relationship's horizontal jog", () => {
75
+ // USER and SESSION don't share a center column, so "opens" needs a
76
+ // horizontal jog partway down — the exact repro from
77
+ // ascii-er-jog-stray-line.test.ts's stray-duplicate-line fix.
78
+ const ascii = renderMermaidASCII(
79
+ `erDiagram
80
+ USER ||--o{ LOG_ENTRY : generates
81
+ USER ||..o{ SESSION : opens`,
82
+ { colorMode: 'none' },
83
+ )
84
+ // The jog's dashed line ('╌', a non-identifying relationship) opens
85
+ // with a solid '┐' corner glyph at the turn instead of another dash.
86
+ // USER's own vertical "one" marker for this relationship ('┼', a
87
+ // crossing tick rather than '│', which would blend into the plain
88
+ // line, or a bare '─', which would sever it — see getCrowsFootChars'
89
+ // `vertical` param) sits flush against USER's own border, one row
90
+ // above the jog — not fused onto the same row as the jog's corner
91
+ // (#390's flush-marker change, merged after this test was first
92
+ // written, shifted the marker there).
93
+ const lines = ascii.split('\n')
94
+ const jogLineIndex = lines.findIndex((l) => l.includes('└┐ opens'))
95
+ expect(jogLineIndex).toBeGreaterThan(0)
96
+ expect(lines[jogLineIndex - 1]).toMatch(/┼/)
97
+ })
98
+
99
+ it('draws the ASCII-mode corner glyph ("+") at the same jog, in useAscii mode', () => {
100
+ // Same repro and turn as the Unicode jog test above — getCornerChar's
101
+ // useAscii branch (a plain '+', matching getCrowsFootChars' own
102
+ // Unicode/ASCII split) has no Unicode-mode equivalent path, so it needs
103
+ // its own coverage rather than assuming the Unicode assertion implies it.
104
+ // Both adjacent corners use the same '+' glyph regardless of direction
105
+ // in ASCII mode, so the two-corner pair reads as '++' rather than
106
+ // Unicode's direction-distinct '└┐' — see the Unicode test above for
107
+ // why there are two adjacent corners here at all.
108
+ const ascii = renderMermaidASCII(
109
+ `erDiagram
110
+ USER ||--o{ LOG_ENTRY : generates
111
+ USER ||..o{ SESSION : opens`,
112
+ { colorMode: 'none', useAscii: true },
113
+ )
114
+ // Both '+' characters are the jog's own two adjacent corner glyphs
115
+ // (ASCII mode uses a plain '+' for every corner direction — see the
116
+ // Unicode test above, which shows the same shape as '└┐'). USER's own
117
+ // vertical "one" marker sits on its own row above this line, flush
118
+ // against USER's border (#390's flush-marker change) — not fused onto
119
+ // this line the way an earlier version of this comment assumed.
120
+ expect(ascii).toContain('++ opens')
121
+ })
122
+
123
+ it("draws corner glyphs at a vertical relationship's multi-row-obstruction bypass", () => {
124
+ // Same 7-entity, 3-row shape as the multi-row-obstruction regression in
125
+ // ascii-er-relationship-label-corruption-350.test.ts: A-G shares D's
126
+ // column two rows down, so "ag" must bypass through a free column with
127
+ // two short jogs (one leaving A's row, one entering G's row).
128
+ const ascii = renderMermaidASCII(
129
+ `erDiagram
130
+ A ||--o{ B : ab
131
+ B ||--o{ C : bc
132
+ C ||--o{ D : cd
133
+ D ||--o{ E : de
134
+ E ||--o{ F : ef
135
+ F ||--o{ G : fg
136
+ A ||--o{ G : ag
137
+ A {
138
+ string id
139
+ string name
140
+ }
141
+ D {
142
+ string id
143
+ string title
144
+ }
145
+ G {
146
+ string id
147
+ string label
148
+ }`,
149
+ { colorMode: 'none' },
150
+ )
151
+ // Corner glyphs at both jogs of the bypass route. G is also the shared
152
+ // target of "fg" (from F, same row as G) and "ag" (this bypass) — since
153
+ // issue #453, each gets its own staggered attachment column instead of
154
+ // both landing on G's exact center, which shifts these exact strings
155
+ // from a single shared-center bypass to two side-by-side ones; the
156
+ // corner glyphs at the bypass's own turns are still there, just at the
157
+ // updated column.
158
+ expect(ascii).toContain('┌─│──────────────────┘')
159
+ expect(ascii).toContain('○╟─────────────────┘')
160
+ expect(ascii).toContain('ag')
161
+ })
162
+
163
+ it('handles a bypass whose free column search finds a match to the left, not just to the right', () => {
164
+ // Same shape as the bypass test above, but D's box is widened enough
165
+ // that the free-column search (which tries lineX+offset before
166
+ // lineX-offset at every step — see columnClearOfBoxes' caller) exhausts
167
+ // rightward space and finds its match on the left side instead. This
168
+ // exercises the *other* branch of the horizDirAtLine/horizDirAtLower
169
+ // ternaries in the bypass block — every other test in this file only
170
+ // ever exercises the rightward case, since a plain sqrt-based row
171
+ // layout with same-sized entities always searches rightward-first
172
+ // successfully.
173
+ //
174
+ // In this specific diagram, the matched free column (routingX) lands
175
+ // off-canvas (negative — nothing to A's own left in this narrow
176
+ // layout), and the two corners at valid coordinates (A's own column,
177
+ // and G's own column) both land exactly on cells the crow's-foot
178
+ // markers already occupy — the marker legitimately wins there (same
179
+ // precedence as the rightward case: markers are drawn after corners,
180
+ // see drawCorner's own doc comment), so there's no *visible* new
181
+ // glyph to assert on here. What this test actually pins down is that
182
+ // reaching that branch doesn't corrupt anything or throw — content
183
+ // integrity, not a specific rendered character.
184
+ const ascii = renderMermaidASCII(
185
+ `erDiagram
186
+ A ||--o{ B : ab
187
+ B ||--o{ C : bc
188
+ C ||--o{ D : cd
189
+ D ||--o{ E : de
190
+ E ||--o{ F : ef
191
+ F ||--o{ G : fg
192
+ A ||--o{ G : ag
193
+ D {
194
+ string this_is_a_very_long_attribute_name_to_widen_the_box
195
+ }
196
+ G {
197
+ string id
198
+ }`,
199
+ { colorMode: 'none' },
200
+ )
201
+ expect(ascii).toContain('ag')
202
+ expect(ascii).toContain('string id')
203
+ expect(ascii).toContain(
204
+ 'string this_is_a_very_long_attribute_name_to_widen_the_box',
205
+ )
206
+ expect(ascii).not.toMatch(/[a-z]+[─│┊╌][a-z]+/)
207
+ })
208
+
209
+ it('does not corrupt content when the bypass free-column search matches lineX itself (content-integrity coverage only)', () => {
210
+ // The obstruction that triggers multiRowObstruction is detected within
211
+ // the narrow row-gap band (bandTop..bandBottom — see the comment above
212
+ // that check), but the free-column search below scans the *full*
213
+ // startY..endY span. When the obstruction is confined to the band and
214
+ // lineX's own column is otherwise clear across the fuller span, offset
215
+ // 0 of that search succeeds immediately at lineX itself — so
216
+ // `routingX === lineX`, skipping the "if (lineX !== routingX)" corner
217
+ // block entirely (every other bypass test in this file has
218
+ // routingX !== lineX, since a same-sized-entity layout's obstruction
219
+ // isn't usually confined that way). Found the same way as the
220
+ // leftward-bypass case above: varying A's own attribute width while
221
+ // instrumenting routingX locally, not by construction alone.
222
+ //
223
+ // When that block is skipped, nothing observably distinguishes this
224
+ // render from an equivalent diagram that never triggered
225
+ // multiRowObstruction at all — skipping the block draws no corner, so
226
+ // there is no glyph unique to this path to assert on (raised in review
227
+ // on this PR: the assertions below prove the branch doesn't corrupt
228
+ // content or throw when reached, not that it visibly changed anything —
229
+ // hence this test's name and scope are deliberately about integrity,
230
+ // not a specific rendered character).
231
+ const ascii = renderMermaidASCII(
232
+ `erDiagram
233
+ A ||--o{ B : ab
234
+ B ||--o{ C : bc
235
+ C ||--o{ D : cd
236
+ D ||--o{ E : de
237
+ E ||--o{ F : ef
238
+ F ||--o{ G : fg
239
+ A ||--o{ G : ag
240
+ A {
241
+ string x
242
+ }`,
243
+ { colorMode: 'none' },
244
+ )
245
+ expect(ascii).toContain('ag')
246
+ expect(ascii).toContain('string x')
247
+ expect(ascii).not.toMatch(/[a-z]+[─│┊╌][a-z]+/)
248
+ })
249
+ })
@@ -0,0 +1,111 @@
1
+ // ============================================================================
2
+ // ASCII ER diagram: a vertical relationship's jog must not leave a stray
3
+ // duplicate line at its original column
4
+ //
5
+ // renderErAscii's vertical-connection branch always drew the initial
6
+ // straight vertical line for the *entire* startY..endY span at the upper
7
+ // entity's own center column (lineX), even when a horizontal jog then
8
+ // redirects the path to the lower entity's center column (lowerCX). Below
9
+ // the jog row, that left two parallel vertical segments where only one
10
+ // path exists: the "real" one at lowerCX (leading into the lower entity),
11
+ // and a leftover one still running the rest of the way down at the
12
+ // *original* lineX — connected to nothing, since the path already turned.
13
+ // Spotted as an unexplained doubled "││" / "┊┊" glyph pair in a real
14
+ // catalog sample's rendered output.
15
+ //
16
+ // A bare "││"/"┊┊" substring is not on its own proof of this bug: a
17
+ // same-row (horizontal) connection's flush-against-the-border crow's-foot
18
+ // marker for "one" cardinality is also a lone "│", so a "one"-cardinality
19
+ // relationship starting right at an entity's own right border legitimately
20
+ // prints "│<border>│<marker>" on the *same row* as the rest of that
21
+ // entity's box — see issue #351's marker-inset removal. The actual bug
22
+ // this test guards against instead leaves a *connector-only* row: a row
23
+ // whose sole non-blank content is the doubled glyph pair, with no entity
24
+ // box or text sharing that row, since the leftover stem runs down through
25
+ // empty space between two entity rows.
26
+ // ============================================================================
27
+
28
+ import { describe, it, expect } from 'vitest'
29
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
30
+
31
+ /**
32
+ * True when some row's only non-whitespace content is a doubled line/dash
33
+ * glyph ("││" or "┊┊") — the signature of a leftover parallel vertical
34
+ * stem left behind by an unstopped jog. A legitimate box-border-flush-
35
+ * against-a-marker "││" always shares its row with other box/text
36
+ * content (the entity's own border and label), so it's excluded by
37
+ * requiring the rest of the row to be blank.
38
+ */
39
+ function hasStrayDuplicateColumn(ascii: string): boolean {
40
+ return ascii.split('\n').some((line) => {
41
+ for (const pattern of ['││', '┊┊']) {
42
+ const col = line.indexOf(pattern)
43
+ if (col === -1) continue
44
+ const rest = line.slice(0, col) + line.slice(col + pattern.length)
45
+ if (rest.trim() === '') return true
46
+ }
47
+ return false
48
+ })
49
+ }
50
+
51
+ describe('ASCII ER vertical relationship jog does not leave a stray duplicate line', () => {
52
+ it('does not draw a leftover line at the original column once a relationship has jogged over', () => {
53
+ // Real catalog sample ("ER: Non-Identifying (Dashed) Relationship"):
54
+ // USER-SESSION needs a horizontal jog (their centers don't align), so
55
+ // its path runs straight down from USER, jogs sideways, then straight
56
+ // down into SESSION. Before this fix, the row directly below the jog
57
+ // showed two dotted-line glyphs side by side ("┊┊") instead of one —
58
+ // the real jogged path, plus a stray remnant of the pre-jog column
59
+ // that was never told to stop.
60
+ const ascii = renderMermaidASCII(
61
+ `erDiagram
62
+ USER ||--o{ LOG_ENTRY : generates
63
+ USER ||..o{ SESSION : opens`,
64
+ { colorMode: 'none' },
65
+ )
66
+
67
+ expect(hasStrayDuplicateColumn(ascii)).toBe(false)
68
+ })
69
+
70
+ it('does not draw a leftover line for either of two independently-jogging relationships', () => {
71
+ // Real catalog sample ("ER: School Management Schema"): both
72
+ // STUDENT-COURSE ("enrolled") and TEACHER-ENROLLMENT ("teaches") jog
73
+ // over to reach their lower entity. Before this fix, the row right
74
+ // below each jog showed a doubled "│╟"/"││" pair on both sides at
75
+ // once — confirming the leftover-column bug wasn't specific to one
76
+ // relationship's geometry.
77
+ const ascii = renderMermaidASCII(
78
+ `erDiagram
79
+ STUDENT {
80
+ int id PK
81
+ string name
82
+ date dob
83
+ string grade
84
+ }
85
+ TEACHER {
86
+ int id PK
87
+ string name
88
+ string department
89
+ }
90
+ COURSE {
91
+ int id PK
92
+ string title
93
+ int teacher_id FK
94
+ int credits
95
+ }
96
+ ENROLLMENT {
97
+ int id PK
98
+ int student_id FK
99
+ int course_id FK
100
+ string semester
101
+ float grade
102
+ }
103
+ STUDENT ||--o{ COURSE : enrolled
104
+ TEACHER ||--o{ ENROLLMENT : teaches
105
+ COURSE ||--o{ ENROLLMENT : has`,
106
+ { colorMode: 'none' },
107
+ )
108
+
109
+ expect(hasStrayDuplicateColumn(ascii)).toBe(false)
110
+ })
111
+ })
@@ -0,0 +1,118 @@
1
+ // ============================================================================
2
+ // ASCII ER diagram relationship label truncation/padding regression tests
3
+ // (GitHub issue #67)
4
+ //
5
+ // Relationship labels were clamped to a fixed 6-char inter-entity gap,
6
+ // silently truncating anything longer (e.g. "ordered in" → "ordere") and,
7
+ // even when a label did fit, leaving it flush against both entity boxes
8
+ // with zero padding. The cardinality glyph cluster (e.g. "││───○╟") was
9
+ // also crammed directly against the entity box border.
10
+ // ============================================================================
11
+
12
+ import { describe, it, expect } from 'vitest'
13
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
14
+
15
+ const REPRO = `erDiagram
16
+ CUSTOMER ||--o{ ORDER : places
17
+ PRODUCT ||--o{ LINE_ITEM : "ordered in"`
18
+
19
+ describe('ASCII ER relationship label truncation and padding (issue #67)', () => {
20
+ it('renders a long quoted label in full instead of truncating it', () => {
21
+ const ascii = renderMermaidASCII(REPRO, { colorMode: 'none' })
22
+
23
+ expect(ascii).toContain('ordered in')
24
+ // The old bug truncated to "ordere" — make sure that's gone.
25
+ expect(ascii).not.toMatch(/ordere(?!d)/)
26
+ })
27
+
28
+ it('renders a long label in full in Unicode mode too', () => {
29
+ const ascii = renderMermaidASCII(REPRO, {
30
+ colorMode: 'none',
31
+ useAscii: false,
32
+ })
33
+ expect(ascii).toContain('ordered in')
34
+ })
35
+
36
+ it('renders a long label in full in pure-ASCII mode too', () => {
37
+ const ascii = renderMermaidASCII(REPRO, {
38
+ colorMode: 'none',
39
+ useAscii: true,
40
+ })
41
+ expect(ascii).toContain('ordered in')
42
+ })
43
+
44
+ it('keeps at least 1 char of padding between the label and each entity box', () => {
45
+ const ascii = renderMermaidASCII(REPRO, { colorMode: 'none' })
46
+ const lines = ascii.split('\n')
47
+
48
+ // "places" sits on the row directly below the CUSTOMER/ORDER connector line,
49
+ // between the two boxes' bottom borders. Find that row and confirm the
50
+ // label isn't flush against either "└" border character.
51
+ const placesLine = lines.find((l) => l.includes('places'))
52
+ expect(placesLine).toBeDefined()
53
+ const idx = placesLine!.indexOf('places')
54
+ // Character immediately before/after the label must not be a box border.
55
+ expect(placesLine![idx - 1]).not.toBe('┘')
56
+ expect(placesLine![idx - 1]).not.toBe('└')
57
+ expect(placesLine![idx + 'places'.length]).not.toBe('┌')
58
+ expect(placesLine![idx + 'places'.length]).not.toBe('└')
59
+
60
+ const orderedInLine = lines.find((l) => l.includes('ordered in'))
61
+ expect(orderedInLine).toBeDefined()
62
+ const idx2 = orderedInLine!.indexOf('ordered in')
63
+ expect(orderedInLine![idx2 - 1]).not.toBe('┘')
64
+ expect(orderedInLine![idx2 - 1]).not.toBe('└')
65
+ expect(orderedInLine![idx2 + 'ordered in'.length]).not.toBe('┌')
66
+ expect(orderedInLine![idx2 + 'ordered in'.length]).not.toBe('└')
67
+ })
68
+
69
+ it('keeps at least 1 char of padding between the cardinality glyph cluster and each entity box', () => {
70
+ const ascii = renderMermaidASCII(
71
+ `erDiagram
72
+ CUSTOMER ||--o{ ORDER : places`,
73
+ { colorMode: 'none' },
74
+ )
75
+ const lines = ascii.split('\n')
76
+ // The connector line row contains the crow's foot markers, e.g.
77
+ // "│ CUSTOMER │─│───○╟─│ ORDER │" — the marker must not sit directly
78
+ // against the "│" that closes the CUSTOMER box or opens the ORDER box.
79
+ const connectorLine = lines.find(
80
+ (l) => l.includes('CUSTOMER') && (l.includes('│') || l.includes('|')),
81
+ )
82
+ expect(connectorLine).toBeDefined()
83
+
84
+ // Locate the right border of the CUSTOMER box and left border of the ORDER box.
85
+ const customerEnd = connectorLine!.indexOf('CUSTOMER') + 'CUSTOMER'.length
86
+ const closingBorderIdx = connectorLine!.indexOf('│', customerEnd)
87
+ expect(closingBorderIdx).toBeGreaterThan(-1)
88
+ // The very next character must not itself be a cardinality marker glyph
89
+ // touching the border — there must be at least 1 separating cell.
90
+ const afterBorder = connectorLine![closingBorderIdx + 1]
91
+ expect(afterBorder).not.toBe('○')
92
+ expect(afterBorder).not.toBe('╟')
93
+ })
94
+
95
+ it('does not widen the gap for short labels that already fit (stays compact)', () => {
96
+ const compact = renderMermaidASCII(
97
+ `erDiagram
98
+ A ||--o{ B : x`,
99
+ { colorMode: 'none' },
100
+ )
101
+ // Sanity: still renders correctly and compactly (no huge gap for a 1-char label).
102
+ const lines = compact.split('\n')
103
+ const line = lines.find((l) => l.includes('A') && l.includes('│'))
104
+ expect(line).toBeDefined()
105
+ expect(line!.length).toBeLessThan(30)
106
+ expect(compact).toContain('x')
107
+ })
108
+
109
+ it('full example diagram from the issue renders every entity and full labels', () => {
110
+ const ascii = renderMermaidASCII(REPRO, { colorMode: 'none' })
111
+ expect(ascii).toContain('CUSTOMER')
112
+ expect(ascii).toContain('ORDER')
113
+ expect(ascii).toContain('PRODUCT')
114
+ expect(ascii).toContain('LINE_ITEM')
115
+ expect(ascii).toContain('places')
116
+ expect(ascii).toContain('ordered in')
117
+ })
118
+ })
@@ -0,0 +1,114 @@
1
+ // ============================================================================
2
+ // ASCII ER diagram padding tests (issue #343)
3
+ //
4
+ // `-x`/`-y`/`-p` (AsciiRenderOptions.paddingX/paddingY/boxBorderPadding)
5
+ // previously only affected the flowchart/state grid layout — ER diagrams
6
+ // ignored them entirely (hGap/vGap/componentGap were fixed constants, and
7
+ // drawMultiBox was always called with its implicit default padding). This
8
+ // file verifies the fix, and that a render with no explicit padding option
9
+ // still renders byte-for-byte identical to before the fix (see the
10
+ // DEFAULT_PADDING_* comment in src/ascii/types.ts for why that matters).
11
+ // ============================================================================
12
+
13
+ import { describe, it, expect } from 'vitest'
14
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
15
+
16
+ // A single relationship with a short label — exercises the horizontal gap
17
+ // between entities without the label itself becoming the binding constraint
18
+ // (a long label widens the gap independently of hGap — see the
19
+ // `pairLabelWidth` widening in er-diagram.ts — which would make the
20
+ // hGap-driven shift non-linear across padding values).
21
+ const RELATED = `erDiagram
22
+ CUSTOMER ||--o{ ORDER : x`
23
+
24
+ // Two disconnected entities — exercises componentGap (vertical stacking).
25
+ const DISCONNECTED = `erDiagram
26
+ CUSTOMER {
27
+ string name
28
+ }
29
+ ORDER {
30
+ string id
31
+ }`
32
+
33
+ function colOf(ascii: string, lineIndex: number, needle: string): number {
34
+ const line = ascii.split('\n')[lineIndex]
35
+ if (line === undefined) throw new Error(`no line ${lineIndex}`)
36
+ const idx = line.indexOf(needle)
37
+ if (idx === -1) throw new Error(`"${needle}" not found on line ${lineIndex}`)
38
+ return idx
39
+ }
40
+
41
+ function lineCount(ascii: string): number {
42
+ return ascii.split('\n').filter((l) => l.length > 0).length
43
+ }
44
+
45
+ function borderWidth(ascii: string, lineIndex: number): number {
46
+ const line = ascii.split('\n')[lineIndex]
47
+ const match = line ? /\+-+\+/.exec(line) : null
48
+ if (!match) throw new Error(`no border run on line ${lineIndex}: ${line}`)
49
+ return match[0].length
50
+ }
51
+
52
+ describe('ASCII ER diagrams — padding (issue #343)', () => {
53
+ it('renders identically with no padding options and with explicit defaults', () => {
54
+ const withoutOptions = renderMermaidASCII(RELATED, { useAscii: true })
55
+ const withDefaults = renderMermaidASCII(RELATED, {
56
+ useAscii: true,
57
+ paddingX: 5,
58
+ paddingY: 5,
59
+ boxBorderPadding: 1,
60
+ })
61
+ expect(withDefaults).toBe(withoutOptions)
62
+ })
63
+
64
+ it('paddingX widens the gap between related entities', () => {
65
+ const base = renderMermaidASCII(RELATED, { useAscii: true })
66
+ const wide = renderMermaidASCII(RELATED, { useAscii: true, paddingX: 25 })
67
+ const baseCol = colOf(base, 1, '| ORDER |')
68
+ const wideCol = colOf(wide, 1, '| ORDER |')
69
+ expect(wideCol).toBeGreaterThan(baseCol)
70
+ const mid = renderMermaidASCII(RELATED, { useAscii: true, paddingX: 15 })
71
+ const midCol = colOf(mid, 1, '| ORDER |')
72
+ expect(midCol - baseCol).toBe(wideCol - midCol)
73
+ })
74
+
75
+ it('a very small paddingX does not shrink the gap below the safe minimum', () => {
76
+ // Unlike sequence/class diagrams, ER's horizontal gap has a floor equal
77
+ // to its own default (6) rather than a smaller generic minimum — a
78
+ // smaller gap lets the crow's-foot markers on each end overlap (see the
79
+ // hGap comment in er-diagram.ts and ascii-padding-edge-cases.test.ts for
80
+ // the regression test), so a very negative paddingX clamps to the same
81
+ // gap as the default rather than tightening further.
82
+ const base = renderMermaidASCII(RELATED, { useAscii: true })
83
+ const tight = renderMermaidASCII(RELATED, {
84
+ useAscii: true,
85
+ paddingX: -10,
86
+ })
87
+ expect(tight).toBe(base)
88
+ })
89
+
90
+ it('paddingY widens the gap between disconnected components', () => {
91
+ const base = renderMermaidASCII(DISCONNECTED, { useAscii: true })
92
+ const tall = renderMermaidASCII(DISCONNECTED, {
93
+ useAscii: true,
94
+ paddingY: 25,
95
+ })
96
+ expect(lineCount(tall)).toBeGreaterThan(lineCount(base))
97
+ const mid = renderMermaidASCII(DISCONNECTED, {
98
+ useAscii: true,
99
+ paddingY: 15,
100
+ })
101
+ expect(lineCount(mid) - lineCount(base)).toBe(
102
+ lineCount(tall) - lineCount(mid),
103
+ )
104
+ })
105
+
106
+ it('boxBorderPadding widens entity boxes by exactly 2 columns per unit', () => {
107
+ const base = renderMermaidASCII(RELATED, { useAscii: true })
108
+ const padded = renderMermaidASCII(RELATED, {
109
+ useAscii: true,
110
+ boxBorderPadding: 4,
111
+ })
112
+ expect(borderWidth(padded, 0) - borderWidth(base, 0)).toBe(6)
113
+ })
114
+ })