@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,104 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII class diagram padding tests (issue #343)
|
|
3
|
+
//
|
|
4
|
+
// `-x`/`-y`/`-p` (AsciiRenderOptions.paddingX/paddingY/boxBorderPadding)
|
|
5
|
+
// previously only affected the flowchart/state grid layout — class diagrams
|
|
6
|
+
// ignored them entirely (hGap/vGap were fixed constants, and drawMultiBox
|
|
7
|
+
// was always called with its implicit default padding). This file verifies
|
|
8
|
+
// the fix, and that a render with no explicit padding option still renders
|
|
9
|
+
// byte-for-byte identical to before the fix (see the DEFAULT_PADDING_*
|
|
10
|
+
// 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
|
+
// Two independent, same-level classes — exercises the horizontal gap.
|
|
17
|
+
const SIDE_BY_SIDE = `classDiagram
|
|
18
|
+
class Foo
|
|
19
|
+
class Bar`
|
|
20
|
+
|
|
21
|
+
// A parent/child pair — exercises the vertical gap between levels.
|
|
22
|
+
const STACKED = `classDiagram
|
|
23
|
+
class Animal
|
|
24
|
+
class Dog
|
|
25
|
+
Animal <|-- Dog`
|
|
26
|
+
|
|
27
|
+
function colOf(ascii: string, lineIndex: number, needle: string): number {
|
|
28
|
+
const line = ascii.split('\n')[lineIndex]
|
|
29
|
+
if (line === undefined) throw new Error(`no line ${lineIndex}`)
|
|
30
|
+
const idx = line.indexOf(needle)
|
|
31
|
+
if (idx === -1) throw new Error(`"${needle}" not found on line ${lineIndex}`)
|
|
32
|
+
return idx
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function lineCount(ascii: string): number {
|
|
36
|
+
return ascii.split('\n').filter((l) => l.length > 0).length
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function borderWidth(ascii: string, lineIndex: number): number {
|
|
40
|
+
const line = ascii.split('\n')[lineIndex]
|
|
41
|
+
const match = line ? /\+-+\+/.exec(line) : null
|
|
42
|
+
if (!match) throw new Error(`no border run on line ${lineIndex}: ${line}`)
|
|
43
|
+
return match[0].length
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('ASCII class diagrams — padding (issue #343)', () => {
|
|
47
|
+
it('renders identically with no padding options and with explicit defaults', () => {
|
|
48
|
+
const withoutOptions = renderMermaidASCII(SIDE_BY_SIDE, {
|
|
49
|
+
useAscii: true,
|
|
50
|
+
})
|
|
51
|
+
const withDefaults = renderMermaidASCII(SIDE_BY_SIDE, {
|
|
52
|
+
useAscii: true,
|
|
53
|
+
paddingX: 5,
|
|
54
|
+
paddingY: 5,
|
|
55
|
+
boxBorderPadding: 1,
|
|
56
|
+
})
|
|
57
|
+
expect(withDefaults).toBe(withoutOptions)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('paddingX widens the gap between same-level class boxes', () => {
|
|
61
|
+
const base = renderMermaidASCII(SIDE_BY_SIDE, { useAscii: true })
|
|
62
|
+
const wide = renderMermaidASCII(SIDE_BY_SIDE, {
|
|
63
|
+
useAscii: true,
|
|
64
|
+
paddingX: 25,
|
|
65
|
+
})
|
|
66
|
+
const baseCol = colOf(base, 1, '| Bar |')
|
|
67
|
+
const wideCol = colOf(wide, 1, '| Bar |')
|
|
68
|
+
expect(wideCol).toBeGreaterThan(baseCol)
|
|
69
|
+
const mid = renderMermaidASCII(SIDE_BY_SIDE, {
|
|
70
|
+
useAscii: true,
|
|
71
|
+
paddingX: 15,
|
|
72
|
+
})
|
|
73
|
+
const midCol = colOf(mid, 1, '| Bar |')
|
|
74
|
+
expect(midCol - baseCol).toBe(wideCol - midCol)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('a very small paddingX tightens the gap below the default', () => {
|
|
78
|
+
const base = renderMermaidASCII(SIDE_BY_SIDE, { useAscii: true })
|
|
79
|
+
const tight = renderMermaidASCII(SIDE_BY_SIDE, {
|
|
80
|
+
useAscii: true,
|
|
81
|
+
paddingX: -10,
|
|
82
|
+
})
|
|
83
|
+
expect(colOf(tight, 1, '| Bar |')).toBeLessThan(colOf(base, 1, '| Bar |'))
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('paddingY widens the vertical gap between inheritance levels', () => {
|
|
87
|
+
const base = renderMermaidASCII(STACKED, { useAscii: true })
|
|
88
|
+
const tall = renderMermaidASCII(STACKED, { useAscii: true, paddingY: 25 })
|
|
89
|
+
expect(lineCount(tall)).toBeGreaterThan(lineCount(base))
|
|
90
|
+
const mid = renderMermaidASCII(STACKED, { useAscii: true, paddingY: 15 })
|
|
91
|
+
expect(lineCount(mid) - lineCount(base)).toBe(
|
|
92
|
+
lineCount(tall) - lineCount(mid),
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('boxBorderPadding widens class boxes by exactly 2 columns per unit', () => {
|
|
97
|
+
const base = renderMermaidASCII(SIDE_BY_SIDE, { useAscii: true })
|
|
98
|
+
const padded = renderMermaidASCII(SIDE_BY_SIDE, {
|
|
99
|
+
useAscii: true,
|
|
100
|
+
boxBorderPadding: 4,
|
|
101
|
+
})
|
|
102
|
+
expect(borderWidth(padded, 0) - borderWidth(base, 0)).toBe(6)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for issue #971 — "ASCII class-diagram layout: single-
|
|
3
|
+
* parent x-alignment," the first implementation stage of #964's fix
|
|
4
|
+
* (docs/decisions/ascii-class-diagram-x-coordinate-assignment-970.md).
|
|
5
|
+
*
|
|
6
|
+
* Before this fix, `class-diagram.ts` packed each level's classes strictly
|
|
7
|
+
* left-to-right in declaration order, with no reference to any parent's
|
|
8
|
+
* column — a level with exactly one occupant always landed at column 0,
|
|
9
|
+
* even when its real parent sat far to the right. This fix aligns a
|
|
10
|
+
* "singleton block" (no sibling at its level shares its exact parent-id
|
|
11
|
+
* set) with exactly one qualifying (strictly-shallower) parent under that
|
|
12
|
+
* parent's own box center instead. Every other shape (no parents, an
|
|
13
|
+
* all-same-level/rootless-cycle parent set, or more than one class sharing
|
|
14
|
+
* a parent set) is out of scope here — it keeps its pre-fix left-to-right
|
|
15
|
+
* position, tracked separately by issue #972.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from 'vitest'
|
|
19
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
20
|
+
import { findBoxRect } from './helpers/ascii-form.ts'
|
|
21
|
+
|
|
22
|
+
describe('ASCII class diagram — single-parent x-alignment (issue #971)', () => {
|
|
23
|
+
it("#964's exact repro: a lone child aligns under its real parent's center, not the leftmost column", () => {
|
|
24
|
+
const src = `classDiagram
|
|
25
|
+
class Left
|
|
26
|
+
class TallMid {
|
|
27
|
+
+String b0
|
|
28
|
+
+String b1
|
|
29
|
+
+String b2
|
|
30
|
+
+String b3
|
|
31
|
+
+String b4
|
|
32
|
+
+String b5
|
|
33
|
+
}
|
|
34
|
+
class RightSrc
|
|
35
|
+
class Kid
|
|
36
|
+
RightSrc --> Kid`
|
|
37
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
38
|
+
|
|
39
|
+
const rightSrc = findBoxRect(ascii, 'RightSrc')
|
|
40
|
+
const kid = findBoxRect(ascii, 'Kid')
|
|
41
|
+
const rightSrcCenter =
|
|
42
|
+
rightSrc.x0 + Math.floor((rightSrc.x1 - rightSrc.x0) / 2)
|
|
43
|
+
const kidCenter = kid.x0 + Math.floor((kid.x1 - kid.x0) / 2)
|
|
44
|
+
|
|
45
|
+
// Kid's box center lands on (or within 1 cell of, for odd-width
|
|
46
|
+
// rounding) RightSrc's box center -- not at column 0, where every
|
|
47
|
+
// sole-occupant level used to land regardless of its real parent.
|
|
48
|
+
expect(Math.abs(kidCenter - rightSrcCenter)).toBeLessThanOrEqual(1)
|
|
49
|
+
expect(kid.x0).toBeGreaterThan(0)
|
|
50
|
+
|
|
51
|
+
// Left is the leftmost root and must stay exactly where declaration
|
|
52
|
+
// order has always put it -- this fix only ever moves a class that has
|
|
53
|
+
// a real single parent to align to.
|
|
54
|
+
const left = findBoxRect(ascii, 'Left')
|
|
55
|
+
expect(left.x0).toBe(0)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('a root class with no parent at all is unaffected', () => {
|
|
59
|
+
const src = `classDiagram
|
|
60
|
+
class Lonely`
|
|
61
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
62
|
+
const lonely = findBoxRect(ascii, 'Lonely')
|
|
63
|
+
expect(lonely.x0).toBe(0)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('a class whose only parent edges are same-level (a rootless relationship cycle) is unaffected', () => {
|
|
67
|
+
// A -> B -> C -> A has no external root: the level-BFS puts every
|
|
68
|
+
// member at level 0 together, but the `parents` map still records
|
|
69
|
+
// each one's in-cycle predecessor. That predecessor is same-level, not
|
|
70
|
+
// strictly shallower, so it doesn't qualify -- every member keeps its
|
|
71
|
+
// plain declaration-order position, same as before this fix.
|
|
72
|
+
const src = `classDiagram
|
|
73
|
+
class A
|
|
74
|
+
class B
|
|
75
|
+
class C
|
|
76
|
+
A --> B
|
|
77
|
+
B --> C
|
|
78
|
+
C --> A`
|
|
79
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
80
|
+
const a = findBoxRect(ascii, 'A')
|
|
81
|
+
const b = findBoxRect(ascii, 'B')
|
|
82
|
+
const c = findBoxRect(ascii, 'C')
|
|
83
|
+
expect(a.x0).toBe(0)
|
|
84
|
+
expect(a.x0).toBeLessThan(b.x0)
|
|
85
|
+
expect(b.x0).toBeLessThan(c.x0)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Two children sharing the exact same single parent are a *multi-member*
|
|
89
|
+
// block -- explicitly out of #971's scope (see #972) -- so this sample
|
|
90
|
+
// must render byte-for-byte the way it always has. Chosen because
|
|
91
|
+
// docs/decisions/ascii-class-diagram-x-coordinate-assignment-970.md
|
|
92
|
+
// names this exact samples-data.ts sample ("Class: Inheritance (<|--)")
|
|
93
|
+
// as one it verified stays unchanged.
|
|
94
|
+
it('an existing catalog sample with a multi-child parent (Class: Inheritance) keeps its exact current positions', () => {
|
|
95
|
+
const src = `classDiagram
|
|
96
|
+
class Animal {
|
|
97
|
+
+String name
|
|
98
|
+
+eat() void
|
|
99
|
+
}
|
|
100
|
+
class Dog {
|
|
101
|
+
+String breed
|
|
102
|
+
+bark() void
|
|
103
|
+
}
|
|
104
|
+
class Cat {
|
|
105
|
+
+bool isIndoor
|
|
106
|
+
+meow() void
|
|
107
|
+
}
|
|
108
|
+
Animal <|-- Dog
|
|
109
|
+
Animal <|-- Cat`
|
|
110
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
111
|
+
const animal = findBoxRect(ascii, 'Animal')
|
|
112
|
+
const dog = findBoxRect(ascii, 'Dog')
|
|
113
|
+
const cat = findBoxRect(ascii, 'Cat')
|
|
114
|
+
expect(animal).toEqual({ x0: 0, x1: 17, y0: 0, y1: 6 })
|
|
115
|
+
expect(dog).toEqual({ x0: 0, x1: 18, y0: 10, y1: 16 })
|
|
116
|
+
expect(cat).toEqual({ x0: 23, x1: 42, y0: 10, y1: 16 })
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('two independent single-parent children at the same level both align, and compaction keeps them from overlapping', () => {
|
|
120
|
+
// ParentA and ParentB are both roots; KidA is ParentA's only child,
|
|
121
|
+
// KidB is ParentB's only child. Each is its own singleton block with
|
|
122
|
+
// exactly one qualifying parent, so both get a real desired center --
|
|
123
|
+
// this exercises the compaction pass itself (#971 must implement it
|
|
124
|
+
// for every block shape, not just multi-parent ones): two different
|
|
125
|
+
// singleton blocks can still want colliding desired positions.
|
|
126
|
+
const src = `classDiagram
|
|
127
|
+
class ParentA
|
|
128
|
+
class ParentB
|
|
129
|
+
class KidA
|
|
130
|
+
class KidB
|
|
131
|
+
ParentA --> KidA
|
|
132
|
+
ParentB --> KidB`
|
|
133
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
134
|
+
const kidA = findBoxRect(ascii, 'KidA')
|
|
135
|
+
const kidB = findBoxRect(ascii, 'KidB')
|
|
136
|
+
// Declaration order is preserved -- KidA never lands right of KidB.
|
|
137
|
+
expect(kidA.x1).toBeLessThan(kidB.x0)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for issue #972 — "ASCII class-diagram layout:
|
|
3
|
+
* multi-parent / overlap resolution," the second implementation stage of
|
|
4
|
+
* #964's fix (docs/decisions/ascii-class-diagram-x-coordinate-assignment-970.md),
|
|
5
|
+
* building on #971's single-parent alignment + universal compaction pass.
|
|
6
|
+
*
|
|
7
|
+
* #971 only computed a real desired position for a *singleton* block (no
|
|
8
|
+
* sibling at its level shares its exact parent-id set) with exactly one
|
|
9
|
+
* qualifying (strictly-shallower) parent. This fix adds the remaining
|
|
10
|
+
* block shapes #970's decision named: a block of more than one class
|
|
11
|
+
* sharing one qualifying-parent-set spreads evenly around that shared
|
|
12
|
+
* center; a block with more than one qualifying parent centers on the
|
|
13
|
+
* mean of their centers. Both reuse #971's exact compaction pass
|
|
14
|
+
* unchanged, which already handles "an aligned position collides with a
|
|
15
|
+
* sibling" — #972 does not add new nudging logic, only new
|
|
16
|
+
* desired-position computations for `if (qualifying.length === 0)` this
|
|
17
|
+
* pass now escapes into.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect } from 'vitest'
|
|
21
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
22
|
+
import { findBoxRect, rectsOverlap } from './helpers/ascii-form.ts'
|
|
23
|
+
|
|
24
|
+
function boxCenter(rect: { x0: number; x1: number }): number {
|
|
25
|
+
return rect.x0 + Math.floor((rect.x1 - rect.x0 + 1) / 2)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('ASCII class diagram — multi-parent / overlap resolution (issue #972)', () => {
|
|
29
|
+
it('two children of the same parent spread evenly around its center, not packed from column 0', () => {
|
|
30
|
+
// WidePadding gives Animal real room on both sides -- proves genuine
|
|
31
|
+
// symmetric spreading, not just an edge-of-canvas clamp coincidence.
|
|
32
|
+
const src = `classDiagram
|
|
33
|
+
class WidePadding {
|
|
34
|
+
+String p0
|
|
35
|
+
+String p1
|
|
36
|
+
+String p2
|
|
37
|
+
}
|
|
38
|
+
class Animal
|
|
39
|
+
class Dog
|
|
40
|
+
class Cat
|
|
41
|
+
Animal <|-- Dog
|
|
42
|
+
Animal <|-- Cat`
|
|
43
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
44
|
+
const animal = findBoxRect(ascii, 'Animal')
|
|
45
|
+
const dog = findBoxRect(ascii, 'Dog')
|
|
46
|
+
const cat = findBoxRect(ascii, 'Cat')
|
|
47
|
+
|
|
48
|
+
// Declaration order preserved: Dog left of Cat, neither overlapping.
|
|
49
|
+
expect(dog.x1).toBeLessThan(cat.x0)
|
|
50
|
+
// The pair's combined footprint centers on Animal's own center.
|
|
51
|
+
const blockCenter = dog.x0 + Math.floor((cat.x1 - dog.x0 + 1) / 2)
|
|
52
|
+
expect(Math.abs(blockCenter - boxCenter(animal))).toBeLessThanOrEqual(1)
|
|
53
|
+
// Neither child sits at column 0 (the pre-#972 leftover artifact).
|
|
54
|
+
expect(dog.x0).toBeGreaterThan(0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('a child with two parents at different levels centers on the mean of their centers', () => {
|
|
58
|
+
// Controller (level 0) -> Model (level 1) -> View (level 2); View also
|
|
59
|
+
// has a direct edge from Controller. View's qualifying parents are
|
|
60
|
+
// both Controller and Model (both strictly shallower than View), so
|
|
61
|
+
// its desired center is their mean -- not just Controller's alone.
|
|
62
|
+
const src = `classDiagram
|
|
63
|
+
class Controller {
|
|
64
|
+
+handleInput() void
|
|
65
|
+
}
|
|
66
|
+
class Model {
|
|
67
|
+
+getData() void
|
|
68
|
+
}
|
|
69
|
+
class View {
|
|
70
|
+
+render() void
|
|
71
|
+
}
|
|
72
|
+
Controller --> Model
|
|
73
|
+
Controller --> View
|
|
74
|
+
Model ..> View`
|
|
75
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
76
|
+
const controller = findBoxRect(ascii, 'Controller')
|
|
77
|
+
const model = findBoxRect(ascii, 'Model')
|
|
78
|
+
const view = findBoxRect(ascii, 'View')
|
|
79
|
+
|
|
80
|
+
const expectedCenter = Math.round(
|
|
81
|
+
(boxCenter(controller) + boxCenter(model)) / 2,
|
|
82
|
+
)
|
|
83
|
+
expect(Math.abs(boxCenter(view) - expectedCenter)).toBeLessThanOrEqual(1)
|
|
84
|
+
// Not simply parked back at column 0 the way an unresolved block used to be.
|
|
85
|
+
expect(view.x0).toBeGreaterThan(0)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('a multi-child block whose desired position collides with an earlier sibling is pushed right without overlapping it', () => {
|
|
89
|
+
// Two parents both near the canvas's left edge: EarlyParent's own
|
|
90
|
+
// child (EarlySibling) is placed first; WidePair's two children want
|
|
91
|
+
// to center on WidePair, which sits close enough that their desired
|
|
92
|
+
// block would otherwise land on top of EarlySibling. The universal
|
|
93
|
+
// compaction pass (#971, reused unchanged by #972) must push the
|
|
94
|
+
// whole block right of EarlySibling instead of overlapping it.
|
|
95
|
+
const src = `classDiagram
|
|
96
|
+
class EarlyParent
|
|
97
|
+
class WidePair
|
|
98
|
+
class EarlySibling
|
|
99
|
+
class ChildA
|
|
100
|
+
class ChildB
|
|
101
|
+
EarlyParent --> EarlySibling
|
|
102
|
+
WidePair --> ChildA
|
|
103
|
+
WidePair --> ChildB`
|
|
104
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
105
|
+
const names = [
|
|
106
|
+
'EarlyParent',
|
|
107
|
+
'WidePair',
|
|
108
|
+
'EarlySibling',
|
|
109
|
+
'ChildA',
|
|
110
|
+
'ChildB',
|
|
111
|
+
]
|
|
112
|
+
const rects = names.map((n) => ({ n, r: findBoxRect(ascii, n) }))
|
|
113
|
+
for (let i = 0; i < rects.length; i++) {
|
|
114
|
+
for (let j = i + 1; j < rects.length; j++) {
|
|
115
|
+
expect(
|
|
116
|
+
rectsOverlap(rects[i]!.r, rects[j]!.r),
|
|
117
|
+
`${rects[i]!.n} and ${rects[j]!.n} boxes overlap`,
|
|
118
|
+
).toBe(false)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Declaration order preserved even after compaction pushes the block right.
|
|
122
|
+
const earlySibling = findBoxRect(ascii, 'EarlySibling')
|
|
123
|
+
const childA = findBoxRect(ascii, 'ChildA')
|
|
124
|
+
expect(earlySibling.x1).toBeLessThan(childA.x0)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('a block with no qualifying parents (a rootless cycle with two same-level "parent" edges) keeps its plain baseline position', () => {
|
|
128
|
+
// A and B mutually reference each other with no external root: both
|
|
129
|
+
// land at level 0, and neither's same-level "parent" edge qualifies --
|
|
130
|
+
// #972 must not spread or converge this shape, only #971's untouched
|
|
131
|
+
// no-resolvable-parents fallback applies.
|
|
132
|
+
const src = `classDiagram
|
|
133
|
+
class A
|
|
134
|
+
class B
|
|
135
|
+
A --> B
|
|
136
|
+
B --> A`
|
|
137
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
138
|
+
const a = findBoxRect(ascii, 'A')
|
|
139
|
+
const b = findBoxRect(ascii, 'B')
|
|
140
|
+
expect(a.x0).toBe(0)
|
|
141
|
+
expect(a.x1).toBeLessThan(b.x0)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Regression for a CodeRabbit finding on #972's own PR (thread
|
|
145
|
+
// PRRT_kwDOT5ndxc6hpu-C): two classes with no resolvable parents but the
|
|
146
|
+
// exact same (non-qualifying) parent-id set used to be merged into one
|
|
147
|
+
// joint placement unit the moment the *first* of them was reached in
|
|
148
|
+
// declaration order -- silently pulling an unrelated class declared
|
|
149
|
+
// *between* them ahead of its own position, since it hadn't been placed
|
|
150
|
+
// yet when the merged pair's combined placement consumed the columns it
|
|
151
|
+
// would have occupied.
|
|
152
|
+
it('an unrelated class declared between two same-parent-set siblings with no qualifying parent keeps its own declaration-order position', () => {
|
|
153
|
+
// Q and R share the exact same parent-id set ({P}), but P is at the
|
|
154
|
+
// same level as Q/R (a 3-way mutual-reference cycle), so neither
|
|
155
|
+
// qualifies -- both are "no resolvable parents" blocks on their own.
|
|
156
|
+
// Interloper is a plain, unrelated root declared between Q and R.
|
|
157
|
+
const src = `classDiagram
|
|
158
|
+
class P
|
|
159
|
+
class Q
|
|
160
|
+
class Interloper
|
|
161
|
+
class R
|
|
162
|
+
P --> Q
|
|
163
|
+
P --> R
|
|
164
|
+
Q --> P
|
|
165
|
+
R --> P`
|
|
166
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
167
|
+
const q = findBoxRect(ascii, 'Q')
|
|
168
|
+
const interloper = findBoxRect(ascii, 'Interloper')
|
|
169
|
+
const r = findBoxRect(ascii, 'R')
|
|
170
|
+
// Declared order Q, Interloper, R must survive as rendered left-to-right order.
|
|
171
|
+
expect(q.x1).toBeLessThan(interloper.x0)
|
|
172
|
+
expect(interloper.x1).toBeLessThan(r.x0)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Regression for a CodeRabbit finding on #972's own PR (thread
|
|
176
|
+
// PRRT_kwDOT5ndxc6hpu-M): a multi-member block's desired left edge used
|
|
177
|
+
// to be derived from centering the *padded slot* span (every member's
|
|
178
|
+
// own reach padding, including the outermost edges with no neighbor to
|
|
179
|
+
// separate from) on the shared parent center, rather than the *visible
|
|
180
|
+
// box* span -- so a block whose first member has real reach padding
|
|
181
|
+
// (e.g. its own outgoing relationship carries a long label) and whose
|
|
182
|
+
// last member has none rendered visibly off-center from the parent it
|
|
183
|
+
// was supposed to align under.
|
|
184
|
+
it('a multi-child block with asymmetric reach padding on one child still visually centers on the shared parent', () => {
|
|
185
|
+
const src = `classDiagram
|
|
186
|
+
class Padding {
|
|
187
|
+
+String p0
|
|
188
|
+
+String p1
|
|
189
|
+
+String p2
|
|
190
|
+
+String p3
|
|
191
|
+
}
|
|
192
|
+
class Animal
|
|
193
|
+
class Dog
|
|
194
|
+
class Cat
|
|
195
|
+
class SomewhereElse
|
|
196
|
+
Animal <|-- Dog
|
|
197
|
+
Animal <|-- Cat
|
|
198
|
+
Dog --> SomewhereElse : a moderately long label`
|
|
199
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
200
|
+
const animal = findBoxRect(ascii, 'Animal')
|
|
201
|
+
const dog = findBoxRect(ascii, 'Dog')
|
|
202
|
+
const cat = findBoxRect(ascii, 'Cat')
|
|
203
|
+
const visibleBlockCenter = dog.x0 + Math.floor((cat.x1 - dog.x0 + 1) / 2)
|
|
204
|
+
expect(
|
|
205
|
+
Math.abs(visibleBlockCenter - boxCenter(animal)),
|
|
206
|
+
).toBeLessThanOrEqual(1)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for #448 — "Class: MVC Architecture sample drops the
|
|
3
|
+
* `View --> Model : reads` edge."
|
|
4
|
+
*
|
|
5
|
+
* Root cause: `renderClassAscii` connected every relationship's line/arrow/
|
|
6
|
+
* label to its source and target boxes at a fixed box-center column, with
|
|
7
|
+
* no awareness of other relationships. When two relationships connect the
|
|
8
|
+
* same pair of classes in opposite directions (`View --> Model : reads`
|
|
9
|
+
* alongside `Model ..> View : notifies`), both computed the identical
|
|
10
|
+
* center column, so the later-drawn one silently overwrote the earlier
|
|
11
|
+
* one's line, arrowhead, and label.
|
|
12
|
+
*
|
|
13
|
+
* The fix groups relationships by unordered class-pair and gives each
|
|
14
|
+
* member of a multi-relationship group its own connection column. This
|
|
15
|
+
* test exercises that grouping/column-offset logic directly — no prior
|
|
16
|
+
* test in this repo did (verification for #448 was ad hoc/visual only via
|
|
17
|
+
* a real-terminal capture, never committed as a vitest case), which is
|
|
18
|
+
* exactly why this code path had gone completely uncovered.
|
|
19
|
+
*
|
|
20
|
+
* Uses the same source as the "Class: MVC Architecture" sample
|
|
21
|
+
* (samples-data.ts) that the original issue was reported against — a
|
|
22
|
+
* narrower repro with only short single-word labels and no other class
|
|
23
|
+
* members hits an unrelated, tighter label/border collision (also
|
|
24
|
+
* present, separately, in the pre-#448 code and out of scope here), so
|
|
25
|
+
* this keeps the box widths the real sample has.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { describe, it, expect } from 'vitest'
|
|
29
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
30
|
+
|
|
31
|
+
const MVC_SOURCE = `classDiagram
|
|
32
|
+
class Model {
|
|
33
|
+
-data Map
|
|
34
|
+
+getData() Map
|
|
35
|
+
+setData(key, val) void
|
|
36
|
+
+notify() void
|
|
37
|
+
}
|
|
38
|
+
class View {
|
|
39
|
+
-model Model
|
|
40
|
+
+render() void
|
|
41
|
+
+update() void
|
|
42
|
+
}
|
|
43
|
+
class Controller {
|
|
44
|
+
-model Model
|
|
45
|
+
-view View
|
|
46
|
+
+handleInput(event) void
|
|
47
|
+
+updateModel(data) void
|
|
48
|
+
}
|
|
49
|
+
Controller --> Model : updates
|
|
50
|
+
Controller --> View : refreshes
|
|
51
|
+
View --> Model : reads
|
|
52
|
+
Model ..> View : notifies`
|
|
53
|
+
|
|
54
|
+
describe('ASCII class diagram — reciprocal relationships (issue #448)', () => {
|
|
55
|
+
it('renders all four relationships, including both directions of the Model/View reciprocal pair', () => {
|
|
56
|
+
const ascii = renderMermaidASCII(MVC_SOURCE, { colorMode: 'none' })
|
|
57
|
+
|
|
58
|
+
for (const label of ['updates', 'refreshes', 'reads', 'notifies']) {
|
|
59
|
+
expect(ascii).toContain(label)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('gives the reciprocal pair distinct connection columns instead of one overwriting the other', () => {
|
|
64
|
+
const ascii = renderMermaidASCII(MVC_SOURCE, { colorMode: 'none' })
|
|
65
|
+
const lines = ascii.split('\n')
|
|
66
|
+
|
|
67
|
+
const readsCol = lines.find((l) => l.includes('reads'))!.indexOf('reads')
|
|
68
|
+
const notifiesCol = lines
|
|
69
|
+
.find((l) => l.includes('notifies'))!
|
|
70
|
+
.indexOf('notifies')
|
|
71
|
+
expect(readsCol).not.toBe(notifiesCol)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('leaves a single (non-reciprocal) relationship at the plain center column', () => {
|
|
75
|
+
const ascii = renderMermaidASCII(
|
|
76
|
+
`classDiagram
|
|
77
|
+
class A
|
|
78
|
+
class B
|
|
79
|
+
A --> B : uses`,
|
|
80
|
+
{ colorMode: 'none' },
|
|
81
|
+
)
|
|
82
|
+
expect(ascii).toContain('uses')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Regression test for issue #632 — a weekly form-judge audit comparing this
|
|
88
|
+
* repo's ASCII output against real mermaid.js SVG flagged the "Class:
|
|
89
|
+
* Relationship Labels" sample (samples-data.ts): `Teacher --> Course` and
|
|
90
|
+
* `Student --> Course` are two *distinct* pairs (not a reciprocal pair
|
|
91
|
+
* between the same two classes, which #448 above already covers) that both
|
|
92
|
+
* terminate at the same target, `Course`.
|
|
93
|
+
*
|
|
94
|
+
* Root cause: `pairGroups` (the #448 fix, keyed by the unordered
|
|
95
|
+
* `[from, to]` pair) only fans relationships between the exact same two
|
|
96
|
+
* classes — it never grouped these, so both anchored at Course's identical
|
|
97
|
+
* center column, collapsing their arrowheads onto the same cell. Since
|
|
98
|
+
* both relationships' single horizontal jog then also landed on the exact
|
|
99
|
+
* same row, the later-drawn label (in the separate label-drawing pass)
|
|
100
|
+
* blotted out most of the visible line, so `Student --> Course` looked
|
|
101
|
+
* like it terminated in blank space short of Course instead of reaching it.
|
|
102
|
+
*
|
|
103
|
+
* The fix (`relFromOffset`/`relToOffset` in `class-diagram.ts`) fans a
|
|
104
|
+
* relationship at whichever single end it shares with another
|
|
105
|
+
* relationship, even when the two relationships are between otherwise
|
|
106
|
+
* unrelated classes — scoped to a direct, adjacent-level convergence (see
|
|
107
|
+
* that code's comment for why a level-skipping relationship, which already
|
|
108
|
+
* needs the separate box-collision detour routing, is excluded).
|
|
109
|
+
*/
|
|
110
|
+
describe('ASCII class diagram — relationships converging on a shared target from different sources (issue #632)', () => {
|
|
111
|
+
const STUDENT_COURSE_SOURCE = `classDiagram
|
|
112
|
+
class Teacher {
|
|
113
|
+
+String name
|
|
114
|
+
}
|
|
115
|
+
class Student {
|
|
116
|
+
+String name
|
|
117
|
+
}
|
|
118
|
+
class Course {
|
|
119
|
+
+String title
|
|
120
|
+
}
|
|
121
|
+
Teacher --> Course : teaches
|
|
122
|
+
Student --> Course : enrolled in`
|
|
123
|
+
|
|
124
|
+
it('renders both relationship labels', () => {
|
|
125
|
+
const ascii = renderMermaidASCII(STUDENT_COURSE_SOURCE, {
|
|
126
|
+
colorMode: 'none',
|
|
127
|
+
})
|
|
128
|
+
expect(ascii).toContain('teaches')
|
|
129
|
+
expect(ascii).toContain('enrolled in')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('draws two distinct arrowheads into Course instead of collapsing both onto one shared cell', () => {
|
|
133
|
+
const ascii = renderMermaidASCII(STUDENT_COURSE_SOURCE, {
|
|
134
|
+
colorMode: 'none',
|
|
135
|
+
})
|
|
136
|
+
// Before the fix, both relationships anchored at the exact same
|
|
137
|
+
// column, so only a single '▼' was ever drawn for both of them
|
|
138
|
+
// combined — the `Student --> Course` connector had no arrowhead of
|
|
139
|
+
// its own at all.
|
|
140
|
+
const arrowCount = (ascii.match(/▼/g) ?? []).length
|
|
141
|
+
expect(arrowCount).toBe(2)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('gives the two relationships distinct target columns, both landing on Course', () => {
|
|
145
|
+
const ascii = renderMermaidASCII(STUDENT_COURSE_SOURCE, {
|
|
146
|
+
colorMode: 'none',
|
|
147
|
+
})
|
|
148
|
+
const lines = ascii.split('\n')
|
|
149
|
+
const arrowCols = lines.flatMap((line) =>
|
|
150
|
+
[...line].map((ch, i) => (ch === '▼' ? i : -1)).filter((i) => i >= 0),
|
|
151
|
+
)
|
|
152
|
+
expect(arrowCols).toHaveLength(2)
|
|
153
|
+
expect(arrowCols[0]).not.toBe(arrowCols[1])
|
|
154
|
+
|
|
155
|
+
// Course's box must actually span both arrow columns — otherwise
|
|
156
|
+
// they've merely moved apart without both still landing on the target.
|
|
157
|
+
const courseRow = lines.findIndex((line) => line.includes('Course'))
|
|
158
|
+
expect(courseRow).toBeGreaterThan(0)
|
|
159
|
+
const boxRow = lines[courseRow - 1]!
|
|
160
|
+
const boxLeft = boxRow.indexOf('┌')
|
|
161
|
+
const boxRight = boxRow.indexOf('┐')
|
|
162
|
+
expect(boxLeft).toBeGreaterThanOrEqual(0)
|
|
163
|
+
expect(boxRight).toBeGreaterThan(boxLeft)
|
|
164
|
+
for (const col of arrowCols) {
|
|
165
|
+
expect(col).toBeGreaterThanOrEqual(boxLeft)
|
|
166
|
+
expect(col).toBeLessThanOrEqual(boxRight)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
})
|