@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,61 @@
1
+ // ============================================================================
2
+ // ASCII sequence diagram note tests
3
+ // ============================================================================
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
7
+
8
+ describe('ASCII sequence diagrams – pre-message notes', () => {
9
+ it('renders a note placed before the first message (regression)', () => {
10
+ const result = renderMermaidASCII(`sequenceDiagram
11
+ participant A as Alice
12
+ participant B as Bob
13
+ Note over A: note 1
14
+ A->>B: Hello`)
15
+ expect(result).toContain('note 1')
16
+ expect(result).toContain('Hello')
17
+ })
18
+
19
+ it('renders a notes-only diagram (0 messages) without crashing', () => {
20
+ const result = renderMermaidASCII(`sequenceDiagram
21
+ participant A
22
+ Note over A: lonely note`)
23
+ expect(result).toContain('lonely note')
24
+ })
25
+ })
26
+
27
+ describe('ASCII sequence diagrams – note box vs. lifeline collision (regression)', () => {
28
+ // When a "Note over A,B" box is computed wide enough that its own right
29
+ // padding column lands exactly on B's lifeline x-position, the lifeline
30
+ // (drawn in an earlier pass) must not leak through as a second border
31
+ // character next to the note's real right border. 8-9 'x' characters is
32
+ // the exact width, given these actors' default spacing, that reproduces
33
+ // the collision — shorter or longer text misses the colliding column.
34
+ for (const n of [8, 9]) {
35
+ it(`does not double up border characters for a ${n}-char note spanning two actors`, () => {
36
+ const noteText = 'x'.repeat(n)
37
+ const result = renderMermaidASCII(
38
+ `sequenceDiagram
39
+ participant A
40
+ participant B
41
+ A->>B: hi
42
+ Note over A,B: ${noteText}`,
43
+ { colorMode: 'none' },
44
+ )
45
+ const lines = result.split('\n')
46
+ const contentLine = lines.find((l) => l.includes(noteText))
47
+ expect(contentLine).toBeDefined()
48
+
49
+ // No two border/line-drawing characters should sit directly adjacent
50
+ // with no space between them anywhere in the note's rows — that
51
+ // shape (e.g. "││") only occurs when a stale lifeline character
52
+ // leaks through an unblanked padding column next to a real border.
53
+ const noteLineIndex = lines.indexOf(contentLine!)
54
+ const topBorderLine = lines[noteLineIndex - 1]!
55
+ const bottomBorderLine = lines[noteLineIndex + 1]!
56
+ for (const line of [topBorderLine, contentLine!, bottomBorderLine]) {
57
+ expect(line).not.toMatch(/[│|┌┐└┘][│|┌┐└┘]/)
58
+ }
59
+ })
60
+ }
61
+ })
@@ -0,0 +1,119 @@
1
+ // ============================================================================
2
+ // ASCII sequence diagram padding tests (issue #343)
3
+ //
4
+ // `-x`/`-y`/`-p` (AsciiRenderOptions.paddingX/paddingY/boxBorderPadding)
5
+ // previously only affected the flowchart/state grid layout — sequence
6
+ // diagrams ignored them entirely and always used fixed internal spacing
7
+ // constants. This file verifies the fix: padding now visibly widens/
8
+ // tightens lifeline spacing, actor-box size, and vertical row spacing,
9
+ // while a render with no explicit padding option still renders byte-for-
10
+ // byte identical to before the fix (see the DEFAULT_PADDING_* comment in
11
+ // src/ascii/types.ts for why that matters).
12
+ // ============================================================================
13
+
14
+ import { describe, it, expect } from 'vitest'
15
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
16
+
17
+ const SEQ = `sequenceDiagram
18
+ participant A
19
+ participant B
20
+ A->>B: Hi`
21
+
22
+ /** Column index of the first occurrence of `needle` on a given line. */
23
+ function colOf(ascii: string, lineIndex: number, needle: string): number {
24
+ const line = ascii.split('\n')[lineIndex]
25
+ if (line === undefined) throw new Error(`no line ${lineIndex}`)
26
+ const idx = line.indexOf(needle)
27
+ if (idx === -1) throw new Error(`"${needle}" not found on line ${lineIndex}`)
28
+ return idx
29
+ }
30
+
31
+ /** Width (in chars) of the first `+---+`-style border run on a line. */
32
+ function borderWidth(ascii: string, lineIndex: number): number {
33
+ const line = ascii.split('\n')[lineIndex]
34
+ if (line === undefined) throw new Error(`no line ${lineIndex}`)
35
+ const match = /\+-+\+/.exec(line)
36
+ if (!match) throw new Error(`no border run on line ${lineIndex}: ${line}`)
37
+ return match[0].length
38
+ }
39
+
40
+ function lineCount(ascii: string): number {
41
+ return ascii.split('\n').filter((l) => l.length > 0).length
42
+ }
43
+
44
+ describe('ASCII sequence diagrams — padding (issue #343)', () => {
45
+ it('renders identically with no padding options and with explicit defaults', () => {
46
+ const withoutOptions = renderMermaidASCII(SEQ, { useAscii: true })
47
+ const withDefaults = renderMermaidASCII(SEQ, {
48
+ useAscii: true,
49
+ paddingX: 5,
50
+ paddingY: 5,
51
+ boxBorderPadding: 1,
52
+ })
53
+ expect(withDefaults).toBe(withoutOptions)
54
+ })
55
+
56
+ it('paddingX widens the gap between lifelines (actor B shifts right)', () => {
57
+ const base = renderMermaidASCII(SEQ, { useAscii: true })
58
+ const wide = renderMermaidASCII(SEQ, { useAscii: true, paddingX: 25 })
59
+ const baseCol = colOf(base, 1, '| B |')
60
+ const wideCol = colOf(wide, 1, '| B |')
61
+ expect(wideCol).toBeGreaterThan(baseCol)
62
+ // Two independent step sizes from two different starting points produce
63
+ // the same shift — demonstrates paddingX scales lifeline spacing
64
+ // linearly rather than just "doing something" once.
65
+ const mid = renderMermaidASCII(SEQ, { useAscii: true, paddingX: 15 })
66
+ const midCol = colOf(mid, 1, '| B |')
67
+ expect(midCol - baseCol).toBe(wideCol - midCol)
68
+ })
69
+
70
+ it('a very small paddingX tightens lifeline spacing below the default', () => {
71
+ const base = renderMermaidASCII(SEQ, { useAscii: true })
72
+ const tight = renderMermaidASCII(SEQ, { useAscii: true, paddingX: -10 })
73
+ expect(colOf(tight, 1, '| B |')).toBeLessThan(colOf(base, 1, '| B |'))
74
+ })
75
+
76
+ it('paddingY adds extra blank rows (taller diagram)', () => {
77
+ const base = renderMermaidASCII(SEQ, { useAscii: true })
78
+ const tall = renderMermaidASCII(SEQ, { useAscii: true, paddingY: 25 })
79
+ expect(lineCount(tall)).toBeGreaterThan(lineCount(base))
80
+ // Same linearity check as paddingX above.
81
+ const mid = renderMermaidASCII(SEQ, { useAscii: true, paddingY: 15 })
82
+ expect(lineCount(mid) - lineCount(base)).toBe(
83
+ lineCount(tall) - lineCount(mid),
84
+ )
85
+ })
86
+
87
+ it('boxBorderPadding widens actor boxes by exactly 2 columns per unit', () => {
88
+ const base = renderMermaidASCII(SEQ, { useAscii: true })
89
+ const padded = renderMermaidASCII(SEQ, {
90
+ useAscii: true,
91
+ boxBorderPadding: 4,
92
+ })
93
+ // boxWidth = labelWidth + 2*boxBorderPadding + 2 (border chars) — going
94
+ // from the default (1) to 4 adds 2*(4-1) = 6 columns to each actor box.
95
+ expect(borderWidth(padded, 0) - borderWidth(base, 0)).toBe(6)
96
+ })
97
+
98
+ it('boxBorderPadding also widens note boxes and shifts note content', () => {
99
+ const withNote = `sequenceDiagram
100
+ participant A
101
+ Note over A: hello`
102
+ const base = renderMermaidASCII(withNote, { useAscii: true })
103
+ const padded = renderMermaidASCII(withNote, {
104
+ useAscii: true,
105
+ boxBorderPadding: 4,
106
+ })
107
+ // The note's own top border is the second '+---+' run rendered (the
108
+ // first belongs to the actor header box), so isolate it before
109
+ // comparing width.
110
+ const noteBorder = (ascii: string) => {
111
+ const line = ascii.split('\n')[4] // note top border row
112
+ const match = line ? /\+-+\+/.exec(line) : null
113
+ if (!match) throw new Error(`no note border found: ${line}`)
114
+ return match[0].length
115
+ }
116
+ expect(noteBorder(padded) - noteBorder(base)).toBe(6)
117
+ expect(colOf(padded, 5, 'hello')).toBeGreaterThan(colOf(base, 5, 'hello'))
118
+ })
119
+ })
@@ -0,0 +1,206 @@
1
+ // ============================================================================
2
+ // ASCII sequence diagram — self-arrow regression tests (issue #68)
3
+ //
4
+ // Two bugs in the ASCII sequence-diagram renderer's handling of self-arrows
5
+ // (A->>A: ...):
6
+ // 1. <br/> in a self-arrow label wrote the raw string (with an embedded
7
+ // newline) onto a single canvas row instead of splitting it onto
8
+ // multiple rows, corrupting every column to the right for the rest of
9
+ // the diagram.
10
+ // 2. A self-arrow inside an alt/loop/opt block could be drawn outside the
11
+ // block's wall because the wall's width was computed from lifeline
12
+ // positions only, ignoring the self-arrow's loop glyphs and label.
13
+ // ============================================================================
14
+
15
+ import { describe, it, expect } from 'vitest'
16
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
17
+
18
+ describe('ASCII sequence diagrams – self-arrow labels (issue #68)', () => {
19
+ it('splits <br/> in a self-arrow label across rows without corrupting the canvas', () => {
20
+ const result = renderMermaidASCII(
21
+ `
22
+ sequenceDiagram
23
+ participant A
24
+ participant B
25
+ A->>A: line one<br/>line two
26
+ `,
27
+ { useAscii: false },
28
+ )
29
+
30
+ const lines = result.split('\n')
31
+ const line1Idx = lines.findIndex((l) => l.includes('line one'))
32
+ const line2Idx = lines.findIndex((l) => l.includes('line two'))
33
+
34
+ expect(line1Idx).toBeGreaterThan(-1)
35
+ expect(line2Idx).toBeGreaterThan(-1)
36
+ // Each line of the label gets its own row, immediately below the last.
37
+ expect(line2Idx).toBe(line1Idx + 1)
38
+
39
+ // A corrupted canvas dumps the raw "line one\nline two" string onto one
40
+ // cell: the terminal newline forces "line two" to print flush against
41
+ // column 0 (no box-drawing indentation) instead of as a properly
42
+ // positioned canvas row.
43
+ expect(lines[line2Idx]!.startsWith(' ')).toBe(true)
44
+
45
+ // B's lifeline must still be intact on both label rows — a corrupted
46
+ // canvas shifts every column right of the embedded newline down one
47
+ // row for the rest of the diagram, destroying the B column.
48
+ expect(lines[line1Idx]).toContain('│')
49
+ expect(lines[line2Idx]).toContain('│')
50
+
51
+ // The footer actor boxes must render correctly and stay aligned with
52
+ // the header boxes (proves later rows weren't shifted by the newline).
53
+ const bottomBorders = (result.match(/└───┘/g) || []).length
54
+ expect(bottomBorders).toBe(2) // one footer box per actor (A, B)
55
+ })
56
+
57
+ it('handles 3+ line self-arrow labels', () => {
58
+ const result = renderMermaidASCII(
59
+ `
60
+ sequenceDiagram
61
+ participant A
62
+ A->>A: one<br/>two<br/>three
63
+ `,
64
+ { useAscii: false },
65
+ )
66
+ const lines = result.split('\n')
67
+ const idxOne = lines.findIndex((l) => l.includes('one'))
68
+ const idxTwo = lines.findIndex((l) => l.includes('two'))
69
+ const idxThree = lines.findIndex((l) => l.includes('three'))
70
+ expect(idxTwo).toBe(idxOne + 1)
71
+ expect(idxThree).toBe(idxTwo + 1)
72
+ })
73
+
74
+ it("extends an alt block's wall past a self-arrow's loop and label", () => {
75
+ const result = renderMermaidASCII(
76
+ `
77
+ sequenceDiagram
78
+ participant A
79
+ participant B
80
+ alt some condition
81
+ A->>A: a fairly long self-arrow label here
82
+ end
83
+ `,
84
+ { useAscii: false },
85
+ )
86
+
87
+ // The block header label must not be clipped by an undersized wall.
88
+ expect(result).toContain('alt [some condition]')
89
+
90
+ // The self-arrow's loop corners must stay intact — a wall drawn too
91
+ // far left overwrites them with the wall's vertical-bar character.
92
+ expect(result).toContain('├───┐')
93
+ expect(result).toContain('◀───┘')
94
+
95
+ // The label and the block's right wall must coexist on the same row,
96
+ // with the wall to the right of (not through) the label.
97
+ const labelLine = result
98
+ .split('\n')
99
+ .find((l) => l.includes('a fairly long self-arrow label here'))
100
+ expect(labelLine).toBeDefined()
101
+ expect(labelLine!.trimEnd().endsWith('│')).toBe(true)
102
+ })
103
+
104
+ it("extends a loop block's wall past a self-arrow's loop and label", () => {
105
+ const result = renderMermaidASCII(
106
+ `
107
+ sequenceDiagram
108
+ participant A
109
+ loop every request
110
+ A->>A: a fairly long self-arrow label for the loop block
111
+ end
112
+ `,
113
+ { useAscii: false },
114
+ )
115
+
116
+ expect(result).toContain('loop [every request]')
117
+ expect(result).toContain('├───┐')
118
+ expect(result).toContain('◀───┘')
119
+
120
+ const labelLine = result
121
+ .split('\n')
122
+ .find((l) =>
123
+ l.includes('a fairly long self-arrow label for the loop block'),
124
+ )
125
+ expect(labelLine).toBeDefined()
126
+ expect(labelLine!.trimEnd().endsWith('│')).toBe(true)
127
+ })
128
+ })
129
+
130
+ // ============================================================================
131
+ // Self-message autonumber badge (issue #342)
132
+ //
133
+ // `autonumber` consumed a sequence number for a self-message but never drew
134
+ // the badge glyph on the loop — the counter still advanced correctly, but a
135
+ // numbered self-message and an unnumbered one looked identical. Fixed by
136
+ // widening the loop (see `selfLoopWidth` in sequence.ts) to make room for the
137
+ // badge digits at the start of the top arm, without disturbing the corner
138
+ // glyphs (├/┐ on top, ◀/┘ on bottom).
139
+ // ============================================================================
140
+
141
+ describe('ASCII sequence diagrams – self-message autonumber badge (issue #342)', () => {
142
+ it('draws the autonumber badge on a self-message loop', () => {
143
+ const result = renderMermaidASCII(
144
+ `sequenceDiagram
145
+ autonumber
146
+ A->>A: Reflect`,
147
+ { useAscii: false },
148
+ )
149
+ const lines = result.split('\n')
150
+ // The badge digit "1" sits on the loop's top arm, between the left
151
+ // junction and the top-right corner. (Actor boxes also contain "┐", so
152
+ // match on the junction character "├" to find the loop row specifically.)
153
+ const topArmLine = lines.find((l) => l.includes('├'))
154
+ expect(topArmLine).toBeDefined()
155
+ expect(topArmLine).toMatch(/├1─+┐/)
156
+ // The loop's corner glyphs must stay intact — a corrupted badge
157
+ // placement would overwrite the corner instead of just the dashes.
158
+ expect(result).toContain('┐')
159
+ expect(result).toContain('┘')
160
+ expect(result).toContain('Reflect')
161
+ })
162
+
163
+ it('keeps numbering in sequence across self-messages mixed with normal messages', () => {
164
+ const result = renderMermaidASCII(
165
+ `sequenceDiagram
166
+ participant A
167
+ participant B
168
+ autonumber
169
+ A->>B: First
170
+ A->>A: Self
171
+ A->>B: Third`,
172
+ { useAscii: false },
173
+ )
174
+ const lines = result.split('\n')
175
+
176
+ // First normal message numbered 1.
177
+ const firstArrowLine = lines.find((l) => l.includes('▶') && l.includes('1'))
178
+ expect(firstArrowLine).toBeDefined()
179
+
180
+ // Self-message numbered 2, badge drawn on its loop's top arm.
181
+ const selfTopArmLine = lines.find((l) => l.includes('├'))
182
+ expect(selfTopArmLine).toBeDefined()
183
+ expect(selfTopArmLine).toMatch(/├2─+┐/)
184
+
185
+ // Third message (also A->>B) numbered 3, appearing after the self
186
+ // message's rows.
187
+ const selfIdx = lines.indexOf(selfTopArmLine ?? '')
188
+ const thirdArrowLine = lines
189
+ .slice(selfIdx + 1)
190
+ .find((l) => l.includes('▶') && l.includes('3'))
191
+ expect(thirdArrowLine).toBeDefined()
192
+ })
193
+
194
+ it('renders a self-message without autonumber unchanged (no stray digits, no regression)', () => {
195
+ const result = renderMermaidASCII(
196
+ `sequenceDiagram
197
+ A->>A: Reflect`,
198
+ { useAscii: false },
199
+ )
200
+ // No autonumber active — the loop must render exactly as before: plain
201
+ // corners with no digit inserted into the top arm.
202
+ expect(result).toContain('├───┐')
203
+ expect(result).toContain('◀───┘')
204
+ expect(result).toContain('Reflect')
205
+ })
206
+ })
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { diamondRenderer } from '../shapes/diamond.ts'
3
+ import { canvasToString } from '../canvas.ts'
4
+ import type { ShapeRenderOptions } from '../shapes/types.ts'
5
+
6
+ describe('diamondRenderer.render', () => {
7
+ it('draws unicode diamond corner markers around a short label', () => {
8
+ const options: ShapeRenderOptions = { useAscii: false, padding: 1 }
9
+ const dimensions = diamondRenderer.getDimensions('Yes', options)
10
+ const canvas = diamondRenderer.render('Yes', dimensions, options)
11
+
12
+ const text = canvasToString(canvas)
13
+ const lines = text.split('\n')
14
+
15
+ expect(lines[0]!.startsWith('◇')).toBe(true)
16
+ expect(lines[0]!.endsWith('◇')).toBe(true)
17
+ expect(lines[lines.length - 1]!.startsWith('◇')).toBe(true)
18
+ expect(lines[lines.length - 1]!.endsWith('◇')).toBe(true)
19
+ expect(text).toContain('Yes')
20
+ })
21
+
22
+ it('draws ascii diamond corner markers around a multi-line label', () => {
23
+ const options: ShapeRenderOptions = { useAscii: true, padding: 1 }
24
+ const label = 'Retry?\nCheck'
25
+ const dimensions = diamondRenderer.getDimensions(label, options)
26
+ const canvas = diamondRenderer.render(label, dimensions, options)
27
+
28
+ const text = canvasToString(canvas)
29
+ const lines = text.split('\n')
30
+
31
+ expect(lines[0]!.startsWith('<')).toBe(true)
32
+ expect(lines[0]!.endsWith('>')).toBe(true)
33
+ expect(lines[lines.length - 1]!.startsWith('<')).toBe(true)
34
+ expect(lines[lines.length - 1]!.endsWith('>')).toBe(true)
35
+ expect(text).toContain('Retry?')
36
+ expect(text).toContain('Check')
37
+ })
38
+ })
@@ -0,0 +1,258 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ getBoxDimensions,
4
+ renderBox,
5
+ getBoxAttachmentPoint,
6
+ rectangleRenderer,
7
+ } from '../shapes/rectangle.ts'
8
+ import { canvasToString } from '../canvas.ts'
9
+ import { getCorners } from '../shapes/corners.ts'
10
+ import {
11
+ Up,
12
+ Down,
13
+ Left,
14
+ Right,
15
+ UpperLeft,
16
+ UpperRight,
17
+ LowerLeft,
18
+ LowerRight,
19
+ Middle,
20
+ } from '../types.ts'
21
+ import type { ShapeRenderOptions } from '../shapes/types.ts'
22
+
23
+ const optsP0: ShapeRenderOptions = { useAscii: false, padding: 0 }
24
+ const optsP1: ShapeRenderOptions = { useAscii: false, padding: 1 }
25
+ const optsAsciiP0: ShapeRenderOptions = { useAscii: true, padding: 0 }
26
+
27
+ describe('rectangle shape: getBoxDimensions', () => {
28
+ it('sizes a single-line label with zero padding', () => {
29
+ const dims = getBoxDimensions('Hi', optsP0)
30
+ expect(dims.width).toBe(4)
31
+ expect(dims.height).toBe(3)
32
+ expect(dims.labelArea).toEqual({ x: 1, y: 1, width: 2, height: 1 })
33
+ expect(dims.gridColumns).toEqual([1, 2, 1])
34
+ expect(dims.gridRows).toEqual([1, 1, 1])
35
+ })
36
+
37
+ it('grows width and height with padding', () => {
38
+ const dims = getBoxDimensions('Hi', optsP1)
39
+ expect(dims.width).toBe(6)
40
+ // rawInnerHeight = 1 + 2*1 = 3 (odd) -> innerHeight stays 3
41
+ expect(dims.height).toBe(5)
42
+ expect(dims.gridColumns).toEqual([1, 4, 1])
43
+ expect(dims.gridRows).toEqual([1, 3, 1])
44
+ })
45
+
46
+ it('bumps innerHeight to odd when line count makes it even', () => {
47
+ const dims = getBoxDimensions('One\nTwo', optsP0)
48
+ // lineCount=2, rawInnerHeight=2 (even) -> innerHeight=3
49
+ expect(dims.gridRows).toEqual([1, 3, 1])
50
+ expect(dims.height).toBe(5)
51
+ expect(dims.labelArea.height).toBe(2)
52
+ })
53
+
54
+ it('keeps innerHeight odd when line count already makes it odd', () => {
55
+ const dims = getBoxDimensions('One\nTwo\nThree', optsP0)
56
+ // lineCount=3, rawInnerHeight=3 (odd) -> innerHeight stays 3
57
+ expect(dims.gridRows).toEqual([1, 3, 1])
58
+ expect(dims.height).toBe(5)
59
+ })
60
+
61
+ it('uses the widest line to size width for multi-line labels', () => {
62
+ const dims = getBoxDimensions('Short\nA much longer line', optsP0)
63
+ const expectedWidth = 'A much longer line'.length + 2
64
+ expect(dims.width).toBe(expectedWidth)
65
+ expect(dims.labelArea.width).toBe('A much longer line'.length)
66
+ })
67
+
68
+ it('handles an empty label', () => {
69
+ const dims = getBoxDimensions('', optsP0)
70
+ expect(dims.labelArea.width).toBe(0)
71
+ expect(dims.labelArea.height).toBe(1)
72
+ expect(dims.width).toBe(2)
73
+ })
74
+ })
75
+
76
+ describe('rectangle shape: renderBox', () => {
77
+ it('draws unicode borders and corners for a single-line label', () => {
78
+ const dims = getBoxDimensions('Hi', optsP0)
79
+ const corners = getCorners('rectangle', false)
80
+ const canvas = renderBox('Hi', dims, corners, false)
81
+ const text = canvasToString(canvas)
82
+ expect(text).toContain('┌')
83
+ expect(text).toContain('┐')
84
+ expect(text).toContain('└')
85
+ expect(text).toContain('┘')
86
+ expect(text).toContain('─')
87
+ expect(text).toContain('│')
88
+ expect(text).toContain('Hi')
89
+ })
90
+
91
+ it('draws ascii borders and corners for a single-line label', () => {
92
+ const dims = getBoxDimensions('Hi', optsAsciiP0)
93
+ const corners = getCorners('rectangle', true)
94
+ const canvas = renderBox('Hi', dims, corners, true)
95
+ const text = canvasToString(canvas)
96
+ expect(text).toContain('+')
97
+ expect(text).toContain('-')
98
+ expect(text).toContain('|')
99
+ expect(text).not.toContain('┌')
100
+ expect(text).toContain('Hi')
101
+ })
102
+
103
+ it('centers a single-line label horizontally and vertically', () => {
104
+ const dims = getBoxDimensions('Hi', optsP1)
105
+ const corners = getCorners('rectangle', false)
106
+ const canvas = renderBox('Hi', dims, corners, false)
107
+ const lines = canvasToString(canvas).split('\n')
108
+ const labelRow = lines.findIndex((l) => l.includes('Hi'))
109
+ expect(labelRow).toBeGreaterThan(0)
110
+ expect(labelRow).toBeLessThan(lines.length - 1)
111
+ const col = lines[labelRow]!.indexOf('Hi')
112
+ expect(col).toBeGreaterThan(0)
113
+ })
114
+
115
+ it('stacks a multi-line label across separate rows in order', () => {
116
+ const dims = getBoxDimensions('One\nTwo\nThree', optsP0)
117
+ const corners = getCorners('rectangle', false)
118
+ const canvas = renderBox('One\nTwo\nThree', dims, corners, false)
119
+ const lines = canvasToString(canvas).split('\n')
120
+ const oneRow = lines.findIndex((l) => l.includes('One'))
121
+ const twoRow = lines.findIndex((l) => l.includes('Two'))
122
+ const threeRow = lines.findIndex((l) => l.includes('Three'))
123
+ expect(oneRow).toBeGreaterThan(-1)
124
+ expect(twoRow).toBeGreaterThan(oneRow)
125
+ expect(threeRow).toBeGreaterThan(twoRow)
126
+ })
127
+
128
+ it('centers an even number of label lines around the middle row', () => {
129
+ const dims = getBoxDimensions('One\nTwo', optsP0)
130
+ const corners = getCorners('rectangle', false)
131
+ const canvas = renderBox('One\nTwo', dims, corners, false)
132
+ const lines = canvasToString(canvas).split('\n')
133
+ const oneRow = lines.findIndex((l) => l.includes('One'))
134
+ const twoRow = lines.findIndex((l) => l.includes('Two'))
135
+ expect(twoRow).toBe(oneRow + 1)
136
+ })
137
+
138
+ it('clips label characters that fall outside the canvas bounds', () => {
139
+ const tinyDims = getBoxDimensions('', optsP0)
140
+ const corners = getCorners('rectangle', false)
141
+ const canvas = renderBox('Hello World', tinyDims, corners, false)
142
+ const text = canvasToString(canvas)
143
+ expect(text).not.toContain('Hello World')
144
+ })
145
+
146
+ it('uses custom corner characters when provided', () => {
147
+ const dims = getBoxDimensions('Hi', optsP0)
148
+ const canvas = renderBox(
149
+ 'Hi',
150
+ dims,
151
+ { tl: '<', tr: '>', bl: '<', br: '>' },
152
+ false,
153
+ )
154
+ const text = canvasToString(canvas)
155
+ expect(text).toContain('<')
156
+ expect(text).toContain('>')
157
+ })
158
+ })
159
+
160
+ describe('rectangle shape: getBoxAttachmentPoint', () => {
161
+ const dims = getBoxDimensions('Hi', optsP1)
162
+ const base = { x: 10, y: 20 }
163
+
164
+ it('returns the top-center point for Up', () => {
165
+ expect(getBoxAttachmentPoint(Up, dims, base)).toEqual({
166
+ x: base.x + Math.floor(dims.width / 2),
167
+ y: base.y,
168
+ })
169
+ })
170
+
171
+ it('returns the bottom-center point for Down', () => {
172
+ expect(getBoxAttachmentPoint(Down, dims, base)).toEqual({
173
+ x: base.x + Math.floor(dims.width / 2),
174
+ y: base.y + dims.height - 1,
175
+ })
176
+ })
177
+
178
+ it('returns the left-middle point for Left', () => {
179
+ expect(getBoxAttachmentPoint(Left, dims, base)).toEqual({
180
+ x: base.x,
181
+ y: base.y + Math.floor(dims.height / 2),
182
+ })
183
+ })
184
+
185
+ it('returns the right-middle point for Right', () => {
186
+ expect(getBoxAttachmentPoint(Right, dims, base)).toEqual({
187
+ x: base.x + dims.width - 1,
188
+ y: base.y + Math.floor(dims.height / 2),
189
+ })
190
+ })
191
+
192
+ it('returns the top-left corner for UpperLeft', () => {
193
+ expect(getBoxAttachmentPoint(UpperLeft, dims, base)).toEqual({
194
+ x: base.x,
195
+ y: base.y,
196
+ })
197
+ })
198
+
199
+ it('returns the top-right corner for UpperRight', () => {
200
+ expect(getBoxAttachmentPoint(UpperRight, dims, base)).toEqual({
201
+ x: base.x + dims.width - 1,
202
+ y: base.y,
203
+ })
204
+ })
205
+
206
+ it('returns the bottom-left corner for LowerLeft', () => {
207
+ expect(getBoxAttachmentPoint(LowerLeft, dims, base)).toEqual({
208
+ x: base.x,
209
+ y: base.y + dims.height - 1,
210
+ })
211
+ })
212
+
213
+ it('returns the bottom-right corner for LowerRight', () => {
214
+ expect(getBoxAttachmentPoint(LowerRight, dims, base)).toEqual({
215
+ x: base.x + dims.width - 1,
216
+ y: base.y + dims.height - 1,
217
+ })
218
+ })
219
+
220
+ it('falls back to the center point for Middle', () => {
221
+ expect(getBoxAttachmentPoint(Middle, dims, base)).toEqual({
222
+ x: base.x + Math.floor(dims.width / 2),
223
+ y: base.y + Math.floor(dims.height / 2),
224
+ })
225
+ })
226
+ })
227
+
228
+ describe('rectangleRenderer', () => {
229
+ it('delegates getDimensions to getBoxDimensions', () => {
230
+ expect(rectangleRenderer.getDimensions('Hi', optsP0)).toEqual(
231
+ getBoxDimensions('Hi', optsP0),
232
+ )
233
+ })
234
+
235
+ it('renders unicode rectangle corners by default', () => {
236
+ const dims = rectangleRenderer.getDimensions('Hi', optsP0)
237
+ const canvas = rectangleRenderer.render('Hi', dims, optsP0)
238
+ const text = canvasToString(canvas)
239
+ expect(text).toContain('┌')
240
+ expect(text).toContain('┘')
241
+ })
242
+
243
+ it('renders ascii rectangle corners when useAscii is set', () => {
244
+ const dims = rectangleRenderer.getDimensions('Hi', optsAsciiP0)
245
+ const canvas = rectangleRenderer.render('Hi', dims, optsAsciiP0)
246
+ const text = canvasToString(canvas)
247
+ expect(text).toContain('+')
248
+ expect(text).not.toContain('┌')
249
+ })
250
+
251
+ it('delegates getAttachmentPoint to getBoxAttachmentPoint', () => {
252
+ const dims = rectangleRenderer.getDimensions('Hi', optsP0)
253
+ const base = { x: 3, y: 4 }
254
+ expect(rectangleRenderer.getAttachmentPoint(Down, dims, base)).toEqual(
255
+ getBoxAttachmentPoint(Down, dims, base),
256
+ )
257
+ })
258
+ })