@zombie-mermaid/ascii-renderer 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +5392 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/__tests__/ascii-arrowhead-direction-1083.test.ts +51 -0
- package/src/__tests__/ascii-canvas-first-claim-wins-1093.test.ts +89 -0
- package/src/__tests__/ascii-canvas-size-offset-1093.test.ts +95 -0
- package/src/__tests__/ascii-canvas-write.test.ts +132 -0
- package/src/__tests__/ascii-chain-edge-overlap-1067.test.ts +100 -0
- package/src/__tests__/ascii-charset-border-junctions.test.ts +63 -0
- package/src/__tests__/ascii-cjk-width.test.ts +150 -0
- package/src/__tests__/ascii-class-box-occupancy.test.ts +462 -0
- package/src/__tests__/ascii-class-column-width-488-489.test.ts +639 -0
- package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +187 -0
- package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +114 -0
- package/src/__tests__/ascii-class-diagram-compartments.test.ts +70 -0
- package/src/__tests__/ascii-class-label-box-collision.test.ts +60 -0
- package/src/__tests__/ascii-class-label-row-collision-531.test.ts +135 -0
- package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +57 -0
- package/src/__tests__/ascii-class-padding.test.ts +104 -0
- package/src/__tests__/ascii-class-parent-alignment-971.test.ts +139 -0
- package/src/__tests__/ascii-class-parent-alignment-972.test.ts +208 -0
- package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +169 -0
- package/src/__tests__/ascii-combining-mark-width.test.ts +70 -0
- package/src/__tests__/ascii-coords-overlay.test.ts +64 -0
- package/src/__tests__/ascii-decision-lr-box-start.test.ts +106 -0
- package/src/__tests__/ascii-display-width-unit.test.ts +154 -0
- package/src/__tests__/ascii-draw-arrows-coverage.test.ts +224 -0
- package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +105 -0
- package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +72 -0
- package/src/__tests__/ascii-edge-ending-glyphs.test.ts +206 -0
- package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +95 -0
- package/src/__tests__/ascii-edge-routing-fixes.test.ts +178 -0
- package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +103 -0
- package/src/__tests__/ascii-edge-style-consistency-1067.test.ts +89 -0
- package/src/__tests__/ascii-edge-styles.test.ts +149 -0
- package/src/__tests__/ascii-emoji-cluster-width.test.ts +101 -0
- package/src/__tests__/ascii-er-box-occupancy.test.ts +251 -0
- package/src/__tests__/ascii-er-cardinality.test.ts +40 -0
- package/src/__tests__/ascii-er-corner-glyphs.test.ts +249 -0
- package/src/__tests__/ascii-er-jog-stray-line.test.ts +111 -0
- package/src/__tests__/ascii-er-label-padding.test.ts +118 -0
- package/src/__tests__/ascii-er-padding.test.ts +114 -0
- package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +458 -0
- package/src/__tests__/ascii-er-relationship-overwrite.test.ts +325 -0
- package/src/__tests__/ascii-er-stray-connectors.test.ts +344 -0
- package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +98 -0
- package/src/__tests__/ascii-er-vertical-one-marker.test.ts +110 -0
- package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +168 -0
- package/src/__tests__/ascii-lane-search.test.ts +207 -0
- package/src/__tests__/ascii-multibox-cjk-width.test.ts +184 -0
- package/src/__tests__/ascii-multiline.test.ts +288 -0
- package/src/__tests__/ascii-padding-edge-cases.test.ts +154 -0
- package/src/__tests__/ascii-pathfinder-route-edge.test.ts +184 -0
- package/src/__tests__/ascii-sequence-alt-else-label.test.ts +236 -0
- package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +217 -0
- package/src/__tests__/ascii-sequence-box-group.test.ts +159 -0
- package/src/__tests__/ascii-sequence-cjk-width.test.ts +235 -0
- package/src/__tests__/ascii-sequence-create-destroy.test.ts +114 -0
- package/src/__tests__/ascii-sequence-form-invariants.test.ts +432 -0
- package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +219 -0
- package/src/__tests__/ascii-sequence-notes.test.ts +61 -0
- package/src/__tests__/ascii-sequence-padding.test.ts +119 -0
- package/src/__tests__/ascii-sequence-self-arrow.test.ts +206 -0
- package/src/__tests__/ascii-shape-diamond.test.ts +38 -0
- package/src/__tests__/ascii-shape-rectangle.test.ts +258 -0
- package/src/__tests__/ascii-shape-rounded.test.ts +36 -0
- package/src/__tests__/ascii-shapes-circle.test.ts +41 -0
- package/src/__tests__/ascii-shapes-hexagon.test.ts +42 -0
- package/src/__tests__/ascii-shapes-special.test.ts +344 -0
- package/src/__tests__/ascii-shapes-stadium.test.ts +217 -0
- package/src/__tests__/ascii-shapes-state.test.ts +224 -0
- package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +130 -0
- package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +90 -0
- package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +152 -0
- package/src/__tests__/ascii-subgraph-title-padding.test.ts +77 -0
- package/src/__tests__/ascii-territory-unit.test.ts +219 -0
- package/src/__tests__/ascii-validate.test.ts +220 -0
- package/src/__tests__/ascii.test.ts +325 -0
- package/src/__tests__/class-arrow-directions.test.ts +505 -0
- package/src/__tests__/draw-lines.test.ts +93 -0
- package/src/__tests__/edge-cell-styles.test.ts +278 -0
- package/src/__tests__/grid-occupancy.test.ts +240 -0
- package/src/__tests__/helpers/ascii-form.ts +142 -0
- package/src/__tests__/helpers/terminal-display-width.ts +74 -0
- package/src/__tests__/pathfinder.test.ts +239 -0
- package/src/__tests__/testdata/ascii/ampersand_lhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_lhs_and_rhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_rhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_td_fanin.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_td_fanout.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_without_edge.txt +18 -0
- package/src/__tests__/testdata/ascii/back_reference_from_child.txt +10 -0
- package/src/__tests__/testdata/ascii/backlink_from_bottom.txt +22 -0
- package/src/__tests__/testdata/ascii/backlink_from_top.txt +22 -0
- package/src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt +20 -0
- package/src/__tests__/testdata/ascii/cls_all_relationships.txt +19 -0
- package/src/__tests__/testdata/ascii/cls_annotation.txt +29 -0
- package/src/__tests__/testdata/ascii/cls_association.txt +14 -0
- package/src/__tests__/testdata/ascii/cls_basic.txt +15 -0
- package/src/__tests__/testdata/ascii/cls_dependency.txt +14 -0
- package/src/__tests__/testdata/ascii/cls_inheritance.txt +20 -0
- package/src/__tests__/testdata/ascii/cls_methods.txt +21 -0
- package/src/__tests__/testdata/ascii/comments.txt +23 -0
- package/src/__tests__/testdata/ascii/custom_padding.txt +10 -0
- package/src/__tests__/testdata/ascii/duplicate_labels.txt +19 -0
- package/src/__tests__/testdata/ascii/er_attributes.txt +21 -0
- package/src/__tests__/testdata/ascii/er_basic.txt +8 -0
- package/src/__tests__/testdata/ascii/er_identifying.txt +18 -0
- package/src/__tests__/testdata/ascii/flowchart_tb_simple.txt +29 -0
- package/src/__tests__/testdata/ascii/graph_bt_direction.txt +28 -0
- package/src/__tests__/testdata/ascii/graph_tb_direction.txt +26 -0
- package/src/__tests__/testdata/ascii/nested_subgraphs_with_labels.txt +36 -0
- package/src/__tests__/testdata/ascii/preserve_order_of_definition.txt +23 -0
- package/src/__tests__/testdata/ascii/self_reference.txt +10 -0
- package/src/__tests__/testdata/ascii/self_reference_with_edge.txt +10 -0
- package/src/__tests__/testdata/ascii/seq_basic.txt +17 -0
- package/src/__tests__/testdata/ascii/seq_multiple_messages.txt +25 -0
- package/src/__tests__/testdata/ascii/seq_self_message.txt +18 -0
- package/src/__tests__/testdata/ascii/single_node.txt +8 -0
- package/src/__tests__/testdata/ascii/single_node_longer_name.txt +8 -0
- package/src/__tests__/testdata/ascii/subgraph_complex_mixed.txt +38 -0
- package/src/__tests__/testdata/ascii/subgraph_complex_nested.txt +49 -0
- package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +47 -0
- package/src/__tests__/testdata/ascii/subgraph_empty.txt +10 -0
- package/src/__tests__/testdata/ascii/subgraph_mixed_nodes.txt +20 -0
- package/src/__tests__/testdata/ascii/subgraph_mixed_nodes_td.txt +48 -0
- package/src/__tests__/testdata/ascii/subgraph_multiple_edges.txt +32 -0
- package/src/__tests__/testdata/ascii/subgraph_multiple_nodes.txt +16 -0
- package/src/__tests__/testdata/ascii/subgraph_nested.txt +24 -0
- package/src/__tests__/testdata/ascii/subgraph_nested_with_external.txt +30 -0
- package/src/__tests__/testdata/ascii/subgraph_node_outside_lr.txt +17 -0
- package/src/__tests__/testdata/ascii/subgraph_single_node.txt +16 -0
- package/src/__tests__/testdata/ascii/subgraph_td_direction.txt +26 -0
- package/src/__tests__/testdata/ascii/subgraph_td_multiple.txt +44 -0
- package/src/__tests__/testdata/ascii/subgraph_td_multiple_paddingy.txt +42 -0
- package/src/__tests__/testdata/ascii/subgraph_three_levels_nested.txt +32 -0
- package/src/__tests__/testdata/ascii/subgraph_three_separate.txt +24 -0
- package/src/__tests__/testdata/ascii/subgraph_two_separate.txt +20 -0
- package/src/__tests__/testdata/ascii/subgraph_with_labels.txt +20 -0
- package/src/__tests__/testdata/ascii/three_nodes.txt +9 -0
- package/src/__tests__/testdata/ascii/three_nodes_single_line.txt +8 -0
- package/src/__tests__/testdata/ascii/two_layer_single_graph.txt +19 -0
- package/src/__tests__/testdata/ascii/two_layer_single_graph_longer_names.txt +19 -0
- package/src/__tests__/testdata/ascii/two_nodes_linked.txt +8 -0
- package/src/__tests__/testdata/ascii/two_nodes_longer_names.txt +8 -0
- package/src/__tests__/testdata/ascii/two_root_nodes.txt +19 -0
- package/src/__tests__/testdata/ascii/two_root_nodes_longer_names.txt +19 -0
- package/src/__tests__/testdata/ascii/two_single_root_nodes.txt +19 -0
- package/src/__tests__/testdata/unicode/ampersand_lhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_lhs_and_rhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_rhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_without_edge.txt +18 -0
- package/src/__tests__/testdata/unicode/back_reference_from_child.txt +10 -0
- package/src/__tests__/testdata/unicode/backlink_from_bottom.txt +22 -0
- package/src/__tests__/testdata/unicode/backlink_from_top.txt +22 -0
- package/src/__tests__/testdata/unicode/cls_all_relationships.txt +19 -0
- package/src/__tests__/testdata/unicode/cls_annotation.txt +29 -0
- package/src/__tests__/testdata/unicode/cls_association.txt +14 -0
- package/src/__tests__/testdata/unicode/cls_basic.txt +15 -0
- package/src/__tests__/testdata/unicode/cls_dependency.txt +14 -0
- package/src/__tests__/testdata/unicode/cls_inheritance.txt +20 -0
- package/src/__tests__/testdata/unicode/cls_methods.txt +21 -0
- package/src/__tests__/testdata/unicode/comments.txt +23 -0
- package/src/__tests__/testdata/unicode/duplicate_labels.txt +19 -0
- package/src/__tests__/testdata/unicode/er_attributes.txt +21 -0
- package/src/__tests__/testdata/unicode/er_basic.txt +8 -0
- package/src/__tests__/testdata/unicode/er_identifying.txt +18 -0
- package/src/__tests__/testdata/unicode/graph_bt_direction.txt +28 -0
- package/src/__tests__/testdata/unicode/preserve_order_of_definition.txt +23 -0
- package/src/__tests__/testdata/unicode/self_reference.txt +10 -0
- package/src/__tests__/testdata/unicode/self_reference_with_edge.txt +10 -0
- package/src/__tests__/testdata/unicode/seq_basic.txt +17 -0
- package/src/__tests__/testdata/unicode/seq_multiple_messages.txt +25 -0
- package/src/__tests__/testdata/unicode/seq_self_message.txt +18 -0
- package/src/__tests__/testdata/unicode/single_node.txt +8 -0
- package/src/__tests__/testdata/unicode/single_node_longer_name.txt +8 -0
- package/src/__tests__/testdata/unicode/three_nodes.txt +9 -0
- package/src/__tests__/testdata/unicode/three_nodes_single_line.txt +8 -0
- package/src/__tests__/testdata/unicode/two_layer_single_graph.txt +19 -0
- package/src/__tests__/testdata/unicode/two_layer_single_graph_longer_names.txt +19 -0
- package/src/__tests__/testdata/unicode/two_nodes_linked.txt +8 -0
- package/src/__tests__/testdata/unicode/two_nodes_longer_names.txt +8 -0
- package/src/__tests__/testdata/unicode/two_root_nodes.txt +19 -0
- package/src/__tests__/testdata/unicode/two_root_nodes_longer_names.txt +19 -0
- package/src/__tests__/testdata/unicode/two_single_root_nodes.txt +19 -0
- package/src/__tests__/xychart-ascii.test.ts +376 -0
- package/src/ansi.ts +490 -0
- package/src/canvas.ts +757 -0
- package/src/class-diagram.ts +2001 -0
- package/src/converter.ts +446 -0
- package/src/coords.ts +58 -0
- package/src/display-width.ts +151 -0
- package/src/draw-arrows.ts +593 -0
- package/src/draw-boxes.ts +267 -0
- package/src/draw-bundles.ts +611 -0
- package/src/draw-lines.ts +174 -0
- package/src/draw-subgraphs.ts +108 -0
- package/src/draw.ts +350 -0
- package/src/edge-bundling.ts +435 -0
- package/src/edge-cell-styles.ts +209 -0
- package/src/edge-routing.ts +1070 -0
- package/src/er-diagram.ts +1488 -0
- package/src/flowchart.ts +94 -0
- package/src/grid-occupancy.ts +234 -0
- package/src/grid.ts +1309 -0
- package/src/hyperlinks.ts +248 -0
- package/src/index.ts +163 -0
- package/src/lane-search.ts +68 -0
- package/src/multiline-utils.ts +82 -0
- package/src/pathfinder.ts +448 -0
- package/src/registry.ts +88 -0
- package/src/sequence.ts +1318 -0
- package/src/shapes/circle.ts +31 -0
- package/src/shapes/corners.ts +273 -0
- package/src/shapes/diamond.ts +31 -0
- package/src/shapes/hexagon.ts +35 -0
- package/src/shapes/index.ts +123 -0
- package/src/shapes/rectangle.ts +199 -0
- package/src/shapes/rounded.ts +31 -0
- package/src/shapes/special.ts +360 -0
- package/src/shapes/stadium.ts +122 -0
- package/src/shapes/state.ts +204 -0
- package/src/shapes/types.ts +78 -0
- package/src/territory.ts +136 -0
- package/src/types.ts +454 -0
- package/src/validate.ts +189 -0
- package/src/xychart.ts +1085 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for issue #66 — ASCII renderer box borders misaligned by
|
|
3
|
+
* CJK/fullwidth-form characters.
|
|
4
|
+
*
|
|
5
|
+
* The ASCII grid is column-major with one grid cell per JS code point, but
|
|
6
|
+
* CJK/kana/hangul/fullwidth-form/emoji characters occupy one grid cell while
|
|
7
|
+
* rendering as TWO columns in a real monospace terminal. Left unhandled,
|
|
8
|
+
* this makes box borders too narrow for wide-character labels.
|
|
9
|
+
*
|
|
10
|
+
* `terminalDisplayWidth` re-implements the "how many terminal columns does
|
|
11
|
+
* this string occupy" calculation independently of
|
|
12
|
+
* `src/ascii/display-width.ts`, so these tests verify the *rendered output*
|
|
13
|
+
* against an authority separate from the implementation under test — not
|
|
14
|
+
* just that the fix's own helper agrees with itself.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect } from 'vitest'
|
|
17
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
18
|
+
import {
|
|
19
|
+
terminalDisplayWidth,
|
|
20
|
+
assertUniformDisplayWidth,
|
|
21
|
+
} from './helpers/terminal-display-width.ts'
|
|
22
|
+
|
|
23
|
+
describe('ASCII renderer — CJK/fullwidth character box alignment (issue #66)', () => {
|
|
24
|
+
const mermaid = 'flowchart TD\n A[日本語テスト] --> B[終了]'
|
|
25
|
+
|
|
26
|
+
for (const useAscii of [false, true]) {
|
|
27
|
+
describe(useAscii ? 'ASCII charset' : 'Unicode charset', () => {
|
|
28
|
+
it('renders the issue repro with all rows sharing one terminal display width', () => {
|
|
29
|
+
const ascii = renderMermaidASCII(mermaid, {
|
|
30
|
+
useAscii,
|
|
31
|
+
colorMode: 'none',
|
|
32
|
+
})
|
|
33
|
+
assertUniformDisplayWidth(ascii)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('sizes the label box wide enough for the CJK label content', () => {
|
|
37
|
+
const ascii = renderMermaidASCII(mermaid, {
|
|
38
|
+
useAscii,
|
|
39
|
+
colorMode: 'none',
|
|
40
|
+
})
|
|
41
|
+
const lines = ascii.split('\n')
|
|
42
|
+
|
|
43
|
+
const labelLineIndex = lines.findIndex((l) =>
|
|
44
|
+
l.includes('日本語テスト'),
|
|
45
|
+
)
|
|
46
|
+
expect(labelLineIndex).toBeGreaterThanOrEqual(0)
|
|
47
|
+
const labelLine = lines[labelLineIndex]!
|
|
48
|
+
|
|
49
|
+
// The border directly above the label line delimits the box width.
|
|
50
|
+
const borderLine = lines[labelLineIndex - 1]!
|
|
51
|
+
expect(borderLine.length).toBeGreaterThan(0)
|
|
52
|
+
|
|
53
|
+
// The label row and its border must occupy the same terminal width —
|
|
54
|
+
// that is the concrete manifestation of "borders line up".
|
|
55
|
+
expect(terminalDisplayWidth(labelLine)).toBe(
|
|
56
|
+
terminalDisplayWidth(borderLine),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
// The label's own display width (6 fullwidth glyphs = 12 columns)
|
|
60
|
+
// must fit inside the box's interior (border width minus the two
|
|
61
|
+
// vertical border characters).
|
|
62
|
+
const boxDisplayWidth = terminalDisplayWidth(borderLine)
|
|
63
|
+
const labelDisplayWidth = terminalDisplayWidth('日本語テスト')
|
|
64
|
+
expect(labelDisplayWidth).toBe(12)
|
|
65
|
+
expect(boxDisplayWidth).toBeGreaterThanOrEqual(labelDisplayWidth + 2)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('keeps the box vertical borders in the same column on every row', () => {
|
|
69
|
+
const ascii = renderMermaidASCII(mermaid, {
|
|
70
|
+
useAscii,
|
|
71
|
+
colorMode: 'none',
|
|
72
|
+
})
|
|
73
|
+
const lines = ascii.split('\n')
|
|
74
|
+
const vChar = useAscii ? '|' : '│'
|
|
75
|
+
|
|
76
|
+
const labelLineIndex = lines.findIndex((l) =>
|
|
77
|
+
l.includes('日本語テスト'),
|
|
78
|
+
)
|
|
79
|
+
const labelLine = lines[labelLineIndex]!
|
|
80
|
+
const blankLineAbove = lines[labelLineIndex - 2]! // top padding row
|
|
81
|
+
|
|
82
|
+
// The right border character's *string* index differs between the
|
|
83
|
+
// CJK row and an all-ASCII row in the same box (because the CJK row
|
|
84
|
+
// has fewer code units for the same display width) — but the
|
|
85
|
+
// terminal-column index of the right border must match.
|
|
86
|
+
const cjkRightBorderCol = terminalDisplayWidth(
|
|
87
|
+
labelLine.slice(0, labelLine.lastIndexOf(vChar)),
|
|
88
|
+
)
|
|
89
|
+
const blankRightBorderCol = terminalDisplayWidth(
|
|
90
|
+
blankLineAbove.slice(0, blankLineAbove.lastIndexOf(vChar)),
|
|
91
|
+
)
|
|
92
|
+
expect(cjkRightBorderCol).toBe(blankRightBorderCol)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('does not introduce diagonal or otherwise-broken box characters', () => {
|
|
96
|
+
const ascii = renderMermaidASCII(mermaid, {
|
|
97
|
+
useAscii,
|
|
98
|
+
colorMode: 'none',
|
|
99
|
+
})
|
|
100
|
+
expect(ascii).not.toContain('/')
|
|
101
|
+
expect(ascii).not.toContain('\\')
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
it('handles mixed ASCII + CJK content in the same label', () => {
|
|
107
|
+
const ascii = renderMermaidASCII(
|
|
108
|
+
'flowchart TD\n A[Hello 世界] --> B[OK]',
|
|
109
|
+
{ colorMode: 'none' },
|
|
110
|
+
)
|
|
111
|
+
assertUniformDisplayWidth(ascii)
|
|
112
|
+
expect(ascii).toContain('Hello 世界')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('handles emoji labels', () => {
|
|
116
|
+
const ascii = renderMermaidASCII(
|
|
117
|
+
'flowchart TD\n A[🎉 Party] --> B[Done]',
|
|
118
|
+
{
|
|
119
|
+
colorMode: 'none',
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
assertUniformDisplayWidth(ascii)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('handles CJK edge labels without breaking column alignment', () => {
|
|
126
|
+
const ascii = renderMermaidASCII(
|
|
127
|
+
'flowchart TD\n A[Start] -->|日本語ラベル| B[End]',
|
|
128
|
+
{ colorMode: 'none' },
|
|
129
|
+
)
|
|
130
|
+
assertUniformDisplayWidth(ascii)
|
|
131
|
+
expect(ascii).toContain('日本語ラベル')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('handles CJK labels on non-rectangle shapes (diamond, stadium)', () => {
|
|
135
|
+
const ascii = renderMermaidASCII(
|
|
136
|
+
'flowchart TD\n A{決定} --> B(("結果"))\n A --> C([スタジアム])',
|
|
137
|
+
{ colorMode: 'none' },
|
|
138
|
+
)
|
|
139
|
+
assertUniformDisplayWidth(ascii)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('handles a CJK subgraph title', () => {
|
|
143
|
+
const ascii = renderMermaidASCII(
|
|
144
|
+
'flowchart TD\n subgraph サブグラフ\n A[Node] --> B[Node2]\n end',
|
|
145
|
+
{ colorMode: 'none' },
|
|
146
|
+
)
|
|
147
|
+
assertUniformDisplayWidth(ascii)
|
|
148
|
+
expect(ascii).toContain('サブグラフ')
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII class diagram box-occupancy invariants
|
|
3
|
+
//
|
|
4
|
+
// packages/ascii-renderer/src/er-diagram.ts has an occupancy guard —
|
|
5
|
+
// `setCGuarded`/`boxCells`/`regionClear` — fixing issue #350.
|
|
6
|
+
// packages/ascii-renderer/src/class-diagram.ts now generalizes that same
|
|
7
|
+
// guard (see its own `boxCells`/`setCGuarded`, plus an obstruction-aware
|
|
8
|
+
// same-level detour row) to close issue #953's class-diagram cases. This
|
|
9
|
+
// suite sweeps the same "does a relationship line/label ever land on top of
|
|
10
|
+
// an unrelated class's box" question issue #350 asked for ER diagrams,
|
|
11
|
+
// generalized to class diagrams. Two cases below (the same-level detour
|
|
12
|
+
// crossing a taller, in-between class's box) used to be confirmed-broken and
|
|
13
|
+
// marked `it.fails`; they're now plain `it()` cases, verifying the fix.
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest'
|
|
17
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
18
|
+
import { findBoxRect, rectsOverlap } from './helpers/ascii-form.ts'
|
|
19
|
+
|
|
20
|
+
function expectNoBoxOverlap(ascii: string, names: string[]): void {
|
|
21
|
+
const rects = names.map((n) => ({ n, r: findBoxRect(ascii, n) }))
|
|
22
|
+
for (let i = 0; i < rects.length; i++) {
|
|
23
|
+
for (let j = i + 1; j < rects.length; j++) {
|
|
24
|
+
expect(
|
|
25
|
+
rectsOverlap(rects[i]!.r, rects[j]!.r),
|
|
26
|
+
`${rects[i]!.n} and ${rects[j]!.n} boxes overlap`,
|
|
27
|
+
).toBe(false)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function manyAttrs(prefix: string, count: number): string {
|
|
33
|
+
return Array.from(
|
|
34
|
+
{ length: count },
|
|
35
|
+
(_, i) => ` +String ${prefix}${i}`,
|
|
36
|
+
).join('\n')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Assert every one of `needles` appears verbatim, exactly once, in `text`. */
|
|
40
|
+
function expectAllPresentOnce(text: string, needles: string[]): void {
|
|
41
|
+
for (const needle of needles) {
|
|
42
|
+
const count = text.split(needle).length - 1
|
|
43
|
+
expect(count, `expected "${needle}" to appear exactly once`).toBe(1)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('ASCII class diagrams — class boxes never overlap', () => {
|
|
48
|
+
it('2 classes, inheritance, target below (baseline)', () => {
|
|
49
|
+
const src = `classDiagram
|
|
50
|
+
class A
|
|
51
|
+
class B
|
|
52
|
+
A <|-- B`
|
|
53
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
54
|
+
expectNoBoxOverlap(ascii, ['A', 'B'])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('2 classes, association, long label', () => {
|
|
58
|
+
const src = `classDiagram
|
|
59
|
+
class A
|
|
60
|
+
class B
|
|
61
|
+
A --> B : a very long descriptive association label`
|
|
62
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
63
|
+
expectNoBoxOverlap(ascii, ['A', 'B'])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('root + 3 same-level siblings, one inheritance edge', () => {
|
|
67
|
+
const src = `classDiagram
|
|
68
|
+
class Root
|
|
69
|
+
class Sib1
|
|
70
|
+
class Sib2
|
|
71
|
+
class Sib3
|
|
72
|
+
Root <|-- Sib1
|
|
73
|
+
Root <|-- Sib2
|
|
74
|
+
Root <|-- Sib3`
|
|
75
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
76
|
+
expectNoBoxOverlap(ascii, ['Root', 'Sib1', 'Sib2', 'Sib3'])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------
|
|
80
|
+
// "Same level" routing (src/ascii/class-diagram.ts's final `else` branch,
|
|
81
|
+
// the class-diagram analog of issue #350's ER bug) turns out to be hard
|
|
82
|
+
// to reach on purpose: empirically (verified by rendering and inspecting
|
|
83
|
+
// raw output, not just reading the code), a direct relationship edge
|
|
84
|
+
// between two classes pushes the target to a deeper level than the
|
|
85
|
+
// source even when both share a parent — so two siblings connected by
|
|
86
|
+
// their own edge route through the "below" branch, not "same level".
|
|
87
|
+
// The same-level branch only fires when the layout can't linearly level
|
|
88
|
+
// the graph at all, i.e. a relationship cycle forces its `levelCap`
|
|
89
|
+
// fallback. A 3-node cycle (A->B->C->A) reliably triggers it.
|
|
90
|
+
// ---------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
it('3-node cycle (same-level fallback), no obstruction (baseline)', () => {
|
|
93
|
+
const src = `classDiagram
|
|
94
|
+
class Alpha
|
|
95
|
+
class Mid
|
|
96
|
+
class Charlie
|
|
97
|
+
Alpha --> Mid
|
|
98
|
+
Mid --> Charlie
|
|
99
|
+
Charlie --> Alpha`
|
|
100
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
101
|
+
expectNoBoxOverlap(ascii, ['Alpha', 'Mid', 'Charlie'])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// The same-level branch's detour row used to be computed only from the
|
|
105
|
+
// relationship's own two endpoints (Alpha, Charlie), never checking
|
|
106
|
+
// whether Mid — sitting between them in the same row — was taller and
|
|
107
|
+
// extended below that row. class-diagram.ts now searches for such a
|
|
108
|
+
// same-row obstruction and routes the detour below its bottom edge too
|
|
109
|
+
// (generalizing er-diagram.ts's obstructionBottom guard, issue #350),
|
|
110
|
+
// so the detour's horizontal segment no longer cuts through Mid's box.
|
|
111
|
+
it('3-node cycle, Mid (middle) has 6 attributes: detour line corrupts an attribute row', () => {
|
|
112
|
+
const src = `classDiagram
|
|
113
|
+
class Alpha
|
|
114
|
+
class Mid {
|
|
115
|
+
${manyAttrs('b', 6)}
|
|
116
|
+
}
|
|
117
|
+
class Charlie
|
|
118
|
+
Alpha --> Mid
|
|
119
|
+
Mid --> Charlie
|
|
120
|
+
Charlie --> Alpha`
|
|
121
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
122
|
+
expectAllPresentOnce(ascii, [
|
|
123
|
+
'+ b0: String',
|
|
124
|
+
'+ b1: String',
|
|
125
|
+
'+ b2: String',
|
|
126
|
+
'+ b3: String',
|
|
127
|
+
'+ b4: String',
|
|
128
|
+
'+ b5: String',
|
|
129
|
+
])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// The same-row obstruction search (see the test above) must skip a class
|
|
133
|
+
// on a *different* level/row, even when that class's box geometrically
|
|
134
|
+
// overlaps the detour's horizontal span — not just when it happens to sit
|
|
135
|
+
// off to the side where the search would ignore it anyway regardless of
|
|
136
|
+
// the `other.y !== fromP.y` filter. `Root`/`Kid` are a real root/child
|
|
137
|
+
// pair via the level-BFS (Root has no parent, Kid is its level-1 child),
|
|
138
|
+
// placed on a different row from the Alpha/Mid/Charlie cycle.
|
|
139
|
+
//
|
|
140
|
+
// Two variants of the same Root/Kid pair are compared: a narrow `Kid`
|
|
141
|
+
// never reaches the cycle's column span (baseline); a `Kid` wide enough
|
|
142
|
+
// to need real space, with `Root` declared right after `Alpha`, overlaps
|
|
143
|
+
// Alpha's (and Mid's) column range instead (confirmed below via
|
|
144
|
+
// `findBoxRect`). If the different-row filter worked, both variants
|
|
145
|
+
// render the cycle identically — Root/Kid's actual row is irrelevant to
|
|
146
|
+
// it either way. A test that only used the non-overlapping variant would
|
|
147
|
+
// pass even with the filter deleted (verified: an earlier version of this
|
|
148
|
+
// test did exactly that, and CodeRabbit correctly flagged it as not
|
|
149
|
+
// actually exercising the filter — see the PR discussion).
|
|
150
|
+
//
|
|
151
|
+
// Declaration order alone no longer controls Kid's column (issue #971:
|
|
152
|
+
// `Kid`, as the sole occupant of its level with exactly one qualifying
|
|
153
|
+
// parent, now aligns under Root's own — already-placed — column instead
|
|
154
|
+
// of always landing at the leftmost slot). This test used to force the
|
|
155
|
+
// overlapping/non-overlapping split by moving `Root`/`Kid`'s declaration
|
|
156
|
+
// point around the cycle alone; that no longer produces reliably
|
|
157
|
+
// different geometry, since Kid's position now tracks wherever Root
|
|
158
|
+
// ends up rather than its own declaration slot. Widening `Kid` (so its
|
|
159
|
+
// box needs more room than the gap next to Root provides) is what
|
|
160
|
+
// reliably produces a real overlap now, confirmed empirically against
|
|
161
|
+
// the current renderer rather than assumed.
|
|
162
|
+
//
|
|
163
|
+
// Sabotage-verified: deleting the filter leaves both variants' `+ bN:
|
|
164
|
+
// String` lines intact but makes the overlapping variant one row taller
|
|
165
|
+
// than the baseline (19 vs. 18), which the equality assertion below
|
|
166
|
+
// catches.
|
|
167
|
+
it('3-node cycle, unrelated Root->Kid pair on a different level: obstruction search ignores it regardless of whether it overlaps the detour span', () => {
|
|
168
|
+
// Baseline: Root/Kid declared after the whole cycle, Kid narrow enough
|
|
169
|
+
// to never need more room than the gap next to Root provides.
|
|
170
|
+
const nonOverlapping = renderMermaidASCII(
|
|
171
|
+
`classDiagram
|
|
172
|
+
class Alpha
|
|
173
|
+
class Mid {
|
|
174
|
+
${manyAttrs('b', 6)}
|
|
175
|
+
}
|
|
176
|
+
class Charlie
|
|
177
|
+
class Root
|
|
178
|
+
class Kid
|
|
179
|
+
Alpha --> Mid
|
|
180
|
+
Mid --> Charlie
|
|
181
|
+
Charlie --> Alpha
|
|
182
|
+
Root --> Kid`,
|
|
183
|
+
{ useAscii: true },
|
|
184
|
+
)
|
|
185
|
+
// Overlapping: Root declared immediately after Alpha, Kid wide enough
|
|
186
|
+
// that centering it under Root's (narrow) column reaches into Alpha's.
|
|
187
|
+
const overlapping = renderMermaidASCII(
|
|
188
|
+
`classDiagram
|
|
189
|
+
class Alpha
|
|
190
|
+
class Root
|
|
191
|
+
class VeryLongKidName
|
|
192
|
+
class Mid {
|
|
193
|
+
${manyAttrs('b', 6)}
|
|
194
|
+
}
|
|
195
|
+
class Charlie
|
|
196
|
+
Alpha --> Mid
|
|
197
|
+
Mid --> Charlie
|
|
198
|
+
Charlie --> Alpha
|
|
199
|
+
Root --> VeryLongKidName`,
|
|
200
|
+
{ useAscii: true },
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
// Confirms the geometry this test relies on: in the "overlapping"
|
|
204
|
+
// variant, Kid really does sit on a different row from the cycle, and
|
|
205
|
+
// really does overlap Alpha's own column range (i.e. the obstruction
|
|
206
|
+
// search has something to incorrectly notice if the different-row
|
|
207
|
+
// filter is missing); in the baseline, it doesn't.
|
|
208
|
+
const overlappingAlphaRect = findBoxRect(overlapping, 'Alpha')
|
|
209
|
+
const overlappingKidRect = findBoxRect(overlapping, 'VeryLongKidName')
|
|
210
|
+
expect(overlappingKidRect.y0).toBeGreaterThan(overlappingAlphaRect.y1)
|
|
211
|
+
expect(overlappingKidRect.x0).toBeLessThanOrEqual(overlappingAlphaRect.x1)
|
|
212
|
+
const nonOverlappingAlphaRect = findBoxRect(nonOverlapping, 'Alpha')
|
|
213
|
+
const nonOverlappingKidRect = findBoxRect(nonOverlapping, 'Kid')
|
|
214
|
+
expect(
|
|
215
|
+
nonOverlappingKidRect.x1 < nonOverlappingAlphaRect.x0 ||
|
|
216
|
+
nonOverlappingKidRect.x0 > nonOverlappingAlphaRect.x1,
|
|
217
|
+
).toBe(true)
|
|
218
|
+
|
|
219
|
+
expectAllPresentOnce(nonOverlapping, [
|
|
220
|
+
'+ b0: String',
|
|
221
|
+
'+ b1: String',
|
|
222
|
+
'+ b2: String',
|
|
223
|
+
'+ b3: String',
|
|
224
|
+
'+ b4: String',
|
|
225
|
+
'+ b5: String',
|
|
226
|
+
])
|
|
227
|
+
expectNoBoxOverlap(nonOverlapping, [
|
|
228
|
+
'Alpha',
|
|
229
|
+
'Mid',
|
|
230
|
+
'Charlie',
|
|
231
|
+
'Root',
|
|
232
|
+
'Kid',
|
|
233
|
+
])
|
|
234
|
+
expectAllPresentOnce(overlapping, [
|
|
235
|
+
'+ b0: String',
|
|
236
|
+
'+ b1: String',
|
|
237
|
+
'+ b2: String',
|
|
238
|
+
'+ b3: String',
|
|
239
|
+
'+ b4: String',
|
|
240
|
+
'+ b5: String',
|
|
241
|
+
])
|
|
242
|
+
expectNoBoxOverlap(overlapping, ['Alpha', 'Mid', 'Charlie', 'Root'])
|
|
243
|
+
|
|
244
|
+
// The real check: Kid overlapping the detour span must not change the
|
|
245
|
+
// cycle's own rendered height compared to Kid sitting clear of it.
|
|
246
|
+
expect(overlapping.split('\n').length).toBe(
|
|
247
|
+
nonOverlapping.split('\n').length,
|
|
248
|
+
)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// `computeLabelAnchor`'s same-level branch used to recompute the label's
|
|
252
|
+
// row from scratch (`Math.max(fromBY, toBY) + 2`) instead of reusing the
|
|
253
|
+
// connector's own obstruction-aware `detourY` (`sameLevelDetourY` above)
|
|
254
|
+
// — so a *labeled* same-level relationship still landed its label inside
|
|
255
|
+
// Mid's box even after the unlabeled connector-line fix above, the same
|
|
256
|
+
// corruption in a sibling code path (caught in review on PR #957).
|
|
257
|
+
it('3-node cycle, Mid (middle) has 6 attributes, labeled wrap-around relationship: label no longer corrupts an attribute row', () => {
|
|
258
|
+
const src = `classDiagram
|
|
259
|
+
class Alpha
|
|
260
|
+
class Mid {
|
|
261
|
+
${manyAttrs('b', 6)}
|
|
262
|
+
}
|
|
263
|
+
class Charlie
|
|
264
|
+
Alpha --> Mid
|
|
265
|
+
Mid --> Charlie
|
|
266
|
+
Charlie --> Alpha : a rather long descriptive label`
|
|
267
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
268
|
+
expectAllPresentOnce(ascii, [
|
|
269
|
+
'+ b0: String',
|
|
270
|
+
'+ b1: String',
|
|
271
|
+
'+ b2: String',
|
|
272
|
+
'+ b3: String',
|
|
273
|
+
'+ b4: String',
|
|
274
|
+
'+ b5: String',
|
|
275
|
+
])
|
|
276
|
+
expect(ascii).toContain('a rather long descriptive label')
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('3-node cycle, long relationship label, no tall obstruction', () => {
|
|
280
|
+
const src = `classDiagram
|
|
281
|
+
class Alpha
|
|
282
|
+
class Mid
|
|
283
|
+
class Charlie
|
|
284
|
+
Alpha --> Mid : a rather long descriptive label
|
|
285
|
+
Mid --> Charlie
|
|
286
|
+
Charlie --> Alpha`
|
|
287
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
288
|
+
expectNoBoxOverlap(ascii, ['Alpha', 'Mid', 'Charlie'])
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('3-node cycle, long class names, no tall obstruction', () => {
|
|
292
|
+
const src = `classDiagram
|
|
293
|
+
class AlphaService
|
|
294
|
+
class MidRepository
|
|
295
|
+
class CharlieController
|
|
296
|
+
AlphaService --> MidRepository
|
|
297
|
+
MidRepository --> CharlieController
|
|
298
|
+
CharlieController --> AlphaService`
|
|
299
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
300
|
+
expectNoBoxOverlap(ascii, [
|
|
301
|
+
'AlphaService',
|
|
302
|
+
'MidRepository',
|
|
303
|
+
'CharlieController',
|
|
304
|
+
])
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('2 levels (A,B / C,D), C tall, A->D association (cross-level jog)', () => {
|
|
308
|
+
const src = `classDiagram
|
|
309
|
+
class A
|
|
310
|
+
class B
|
|
311
|
+
class C {
|
|
312
|
+
${manyAttrs('c', 6)}
|
|
313
|
+
}
|
|
314
|
+
class D
|
|
315
|
+
A <|-- C
|
|
316
|
+
B <|-- D
|
|
317
|
+
A --> D`
|
|
318
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
319
|
+
expectNoBoxOverlap(ascii, ['A', 'B', 'C', 'D'])
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// A 4-node cycle (A->B->C->D->A) plus a skip edge (A->C) also fell into
|
|
323
|
+
// the same unguarded same-level detour as the 3-node case above — a
|
|
324
|
+
// second, independently-verified topology hitting the same code gap,
|
|
325
|
+
// this time skipping over B's box. Fixed by the same obstruction-aware
|
|
326
|
+
// detour routing (see the 3-node cycle test above).
|
|
327
|
+
it('4-node cycle with a skip edge, tall middle class: detour corrupts an attribute row', () => {
|
|
328
|
+
const src = `classDiagram
|
|
329
|
+
class A
|
|
330
|
+
class B {
|
|
331
|
+
${manyAttrs('b', 6)}
|
|
332
|
+
}
|
|
333
|
+
class C
|
|
334
|
+
class D
|
|
335
|
+
A --> B
|
|
336
|
+
B --> C
|
|
337
|
+
C --> D
|
|
338
|
+
D --> A
|
|
339
|
+
A --> C`
|
|
340
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
341
|
+
expectAllPresentOnce(ascii, [
|
|
342
|
+
'+ b0: String',
|
|
343
|
+
'+ b1: String',
|
|
344
|
+
'+ b2: String',
|
|
345
|
+
'+ b3: String',
|
|
346
|
+
'+ b4: String',
|
|
347
|
+
'+ b5: String',
|
|
348
|
+
])
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('3 siblings + 2 children, crossing inheritance edges', () => {
|
|
352
|
+
const src = `classDiagram
|
|
353
|
+
class Sib1
|
|
354
|
+
class Sib2
|
|
355
|
+
class Sib3
|
|
356
|
+
class Child1
|
|
357
|
+
class Child2
|
|
358
|
+
Sib1 <|-- Child1
|
|
359
|
+
Sib3 <|-- Child2
|
|
360
|
+
Sib2 <|-- Child1
|
|
361
|
+
Sib2 <|-- Child2`
|
|
362
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
363
|
+
expectNoBoxOverlap(ascii, ['Sib1', 'Sib2', 'Sib3', 'Child1', 'Child2'])
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('strict 3-level vertical chain (negative control, no detour logic)', () => {
|
|
367
|
+
const src = `classDiagram
|
|
368
|
+
class A
|
|
369
|
+
class B
|
|
370
|
+
class C
|
|
371
|
+
A <|-- B
|
|
372
|
+
B <|-- C`
|
|
373
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
374
|
+
expectNoBoxOverlap(ascii, ['A', 'B', 'C'])
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('4 siblings under a root, plus a cross-relationship A->D', () => {
|
|
378
|
+
const src = `classDiagram
|
|
379
|
+
class Root
|
|
380
|
+
class A
|
|
381
|
+
class B
|
|
382
|
+
class C
|
|
383
|
+
class D
|
|
384
|
+
Root <|-- A
|
|
385
|
+
Root <|-- B
|
|
386
|
+
Root <|-- C
|
|
387
|
+
Root <|-- D
|
|
388
|
+
A --> D`
|
|
389
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
390
|
+
expectNoBoxOverlap(ascii, ['Root', 'A', 'B', 'C', 'D'])
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('2 classes, realization, target above source', () => {
|
|
394
|
+
const src = `classDiagram
|
|
395
|
+
class Impl
|
|
396
|
+
class Iface
|
|
397
|
+
Impl ..|> Iface`
|
|
398
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
399
|
+
expectNoBoxOverlap(ascii, ['Impl', 'Iface'])
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('3 siblings under a root, chained by two dependency edges (A->B, B->C)', () => {
|
|
403
|
+
const src = `classDiagram
|
|
404
|
+
class Root
|
|
405
|
+
class A
|
|
406
|
+
class B
|
|
407
|
+
class C
|
|
408
|
+
Root <|-- A
|
|
409
|
+
Root <|-- B
|
|
410
|
+
Root <|-- C
|
|
411
|
+
A ..> B
|
|
412
|
+
B ..> C`
|
|
413
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
414
|
+
expectNoBoxOverlap(ascii, ['Root', 'A', 'B', 'C'])
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('2 classes, association, one has interface annotation + long attrs/methods', () => {
|
|
418
|
+
const src = `classDiagram
|
|
419
|
+
class Serializable {
|
|
420
|
+
<<interface>>
|
|
421
|
+
+serialize() String
|
|
422
|
+
+deserialize(data) void
|
|
423
|
+
}
|
|
424
|
+
class Document
|
|
425
|
+
Document ..|> Serializable`
|
|
426
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
427
|
+
expectNoBoxOverlap(ascii, ['Serializable', 'Document'])
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('3 siblings under a root plus a relationship, unrelated D one level below', () => {
|
|
431
|
+
const src = `classDiagram
|
|
432
|
+
class Alpha
|
|
433
|
+
class Bravo
|
|
434
|
+
class Charlie
|
|
435
|
+
class Delta
|
|
436
|
+
Alpha <|-- Bravo
|
|
437
|
+
Alpha <|-- Charlie
|
|
438
|
+
Bravo *-- Charlie
|
|
439
|
+
Bravo <|-- Delta`
|
|
440
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
441
|
+
expectNoBoxOverlap(ascii, ['Alpha', 'Bravo', 'Charlie', 'Delta'])
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('cyclic dependency edges (cycle-breaking still non-overlapping)', () => {
|
|
445
|
+
const src = `classDiagram
|
|
446
|
+
class View
|
|
447
|
+
class Model
|
|
448
|
+
View --> Model
|
|
449
|
+
Model ..> View`
|
|
450
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
451
|
+
expectNoBoxOverlap(ascii, ['View', 'Model'])
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
it('2 classes, near-duplicate names (substring-collision guard)', () => {
|
|
455
|
+
const src = `classDiagram
|
|
456
|
+
class ServiceA
|
|
457
|
+
class ServiceA2
|
|
458
|
+
ServiceA <|-- ServiceA2`
|
|
459
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
460
|
+
expectNoBoxOverlap(ascii, ['ServiceA', 'ServiceA2'])
|
|
461
|
+
})
|
|
462
|
+
})
|