@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,187 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII class diagram — cross-level connector jog corrupting an intervening
|
|
3
|
+
// box's text
|
|
4
|
+
//
|
|
5
|
+
// Discovered incidentally while writing ascii-class-box-occupancy.test.ts's
|
|
6
|
+
// coverage for issue #953/PR #957 (the *same-level* relationship cycle's
|
|
7
|
+
// detour line lacking an occupancy guard). This is a distinct bug in a
|
|
8
|
+
// different branch of src/ascii/class-diagram.ts's relationship-drawing
|
|
9
|
+
// pass — the "target is below source" (and, by the same missing guard,
|
|
10
|
+
// "target is above source") branch, used whenever a relationship's two
|
|
11
|
+
// classes land on different levels.
|
|
12
|
+
//
|
|
13
|
+
// That branch's "no collision" routing draws a straight horizontal jog
|
|
14
|
+
// connecting the source's lane column to the target's lane column at some
|
|
15
|
+
// row between them. The *only* check that decided this was safe to draw
|
|
16
|
+
// without a detour (`findClearColumn`) tests whether the source's own lane
|
|
17
|
+
// column is itself clear of boxes across the full row range — it says
|
|
18
|
+
// nothing about whether the horizontal jog's row is clear all the way over
|
|
19
|
+
// to the target's column. When a class ends up positioned far from its
|
|
20
|
+
// actual parent's column — e.g. a level's only child is always placed at
|
|
21
|
+
// the leftmost slot regardless of which sibling at the level above is its
|
|
22
|
+
// parent — and a *different*, taller same-level sibling's box sits between
|
|
23
|
+
// those two columns, the jog cuts straight through that sibling's box,
|
|
24
|
+
// silently replacing whichever attribute/method row it crosses.
|
|
25
|
+
//
|
|
26
|
+
// Fixed by giving src/ascii/class-diagram.ts the same kind of box-occupancy
|
|
27
|
+
// guard (`boxCells`/`setCGuarded`) src/ascii/er-diagram.ts already has for
|
|
28
|
+
// issue #350, scoped to the cross-level branches only — the same-level
|
|
29
|
+
// branch's own detour (PR #957's scope) is untouched here.
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
import { describe, it, expect } from 'vitest'
|
|
33
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
34
|
+
import { findBoxRect, rectsOverlap } from './helpers/ascii-form.ts'
|
|
35
|
+
|
|
36
|
+
function expectNoBoxOverlap(ascii: string, names: string[]): void {
|
|
37
|
+
const rects = names.map((n) => ({ n, r: findBoxRect(ascii, n) }))
|
|
38
|
+
for (let i = 0; i < rects.length; i++) {
|
|
39
|
+
for (let j = i + 1; j < rects.length; j++) {
|
|
40
|
+
expect(
|
|
41
|
+
rectsOverlap(rects[i]!.r, rects[j]!.r),
|
|
42
|
+
`${rects[i]!.n} and ${rects[j]!.n} boxes overlap`,
|
|
43
|
+
).toBe(false)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function manyAttrs(prefix: string, count: number): string {
|
|
49
|
+
return Array.from(
|
|
50
|
+
{ length: count },
|
|
51
|
+
(_, i) => ` +String ${prefix}${i}`,
|
|
52
|
+
).join('\n')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Assert every one of `needles` appears verbatim, exactly once, in `text`. */
|
|
56
|
+
function expectAllPresentOnce(text: string, needles: string[]): void {
|
|
57
|
+
for (const needle of needles) {
|
|
58
|
+
const count = text.split(needle).length - 1
|
|
59
|
+
expect(count, `expected "${needle}" to appear exactly once`).toBe(1)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('ASCII class diagrams — cross-level connector jog never corrupts a box', () => {
|
|
64
|
+
it('root-level sibling sits between a source and its far-off, sole child (minimal repro)', () => {
|
|
65
|
+
// Left/TallMid/RightSrc are three unrelated level-0 roots (no cycle
|
|
66
|
+
// involved, unlike the original report — isolates this bug from the
|
|
67
|
+
// separately-tracked same-level detour issue). Kid is RightSrc's only
|
|
68
|
+
// child, so it lands at the leftmost slot of level 1 regardless of
|
|
69
|
+
// RightSrc's own (rightmost) column — the "no collision" routing then
|
|
70
|
+
// has to jog the whole width of the canvas, straight through TallMid.
|
|
71
|
+
const src = `classDiagram
|
|
72
|
+
class Left
|
|
73
|
+
class TallMid {
|
|
74
|
+
${manyAttrs('b', 6)}
|
|
75
|
+
}
|
|
76
|
+
class RightSrc
|
|
77
|
+
class Kid
|
|
78
|
+
RightSrc --> Kid`
|
|
79
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
80
|
+
expectAllPresentOnce(ascii, [
|
|
81
|
+
'+ b0: String',
|
|
82
|
+
'+ b1: String',
|
|
83
|
+
'+ b2: String',
|
|
84
|
+
'+ b3: String',
|
|
85
|
+
'+ b4: String',
|
|
86
|
+
'+ b5: String',
|
|
87
|
+
])
|
|
88
|
+
expectNoBoxOverlap(ascii, ['Left', 'TallMid', 'RightSrc', 'Kid'])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('originally-reported repro: same-level cycle plus an unrelated root/child pair', () => {
|
|
92
|
+
// The exact topology this bug was first reported against. Alpha/Mid/
|
|
93
|
+
// Charlie form a same-level relationship cycle (falls to level 0 via
|
|
94
|
+
// the level-BFS's "no root" fallback); Root is a genuine level-0 root
|
|
95
|
+
// (declared after the cycle) with its own child Kid at level 1. Only
|
|
96
|
+
// "+ b4: String" is asserted here — the cross-level bug this file
|
|
97
|
+
// covers. "+ b1: String" is a *separate*, already-tracked corruption
|
|
98
|
+
// (the same-level cycle's own detour, issue #953/PR #957) and is
|
|
99
|
+
// deliberately not asserted on so this suite stays independent of
|
|
100
|
+
// that fix landing.
|
|
101
|
+
const src = `classDiagram
|
|
102
|
+
class Alpha
|
|
103
|
+
class Mid {
|
|
104
|
+
${manyAttrs('b', 6)}
|
|
105
|
+
}
|
|
106
|
+
class Charlie
|
|
107
|
+
class Root
|
|
108
|
+
class Kid
|
|
109
|
+
Alpha --> Mid
|
|
110
|
+
Mid --> Charlie
|
|
111
|
+
Charlie --> Alpha
|
|
112
|
+
Root --> Kid`
|
|
113
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
114
|
+
expect(
|
|
115
|
+
ascii,
|
|
116
|
+
'"+ b4: String" was overwritten by Root->Kid\'s jog',
|
|
117
|
+
).toContain('+ b4: String')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('deeper tree: a tall level-0 sibling between a level-1 source and its level-2 grandchild', () => {
|
|
121
|
+
const src = `classDiagram
|
|
122
|
+
class A
|
|
123
|
+
class TallMid {
|
|
124
|
+
${manyAttrs('b', 6)}
|
|
125
|
+
}
|
|
126
|
+
class B
|
|
127
|
+
class Kid1
|
|
128
|
+
class GrandKid
|
|
129
|
+
B --> Kid1
|
|
130
|
+
Kid1 --> GrandKid`
|
|
131
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
132
|
+
expectAllPresentOnce(ascii, [
|
|
133
|
+
'+ b0: String',
|
|
134
|
+
'+ b1: String',
|
|
135
|
+
'+ b2: String',
|
|
136
|
+
'+ b3: String',
|
|
137
|
+
'+ b4: String',
|
|
138
|
+
'+ b5: String',
|
|
139
|
+
])
|
|
140
|
+
expectNoBoxOverlap(ascii, ['A', 'TallMid', 'B', 'Kid1', 'GrandKid'])
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('more siblings: several unrelated roots plus a separate parent/child pair, inheritance marker', () => {
|
|
144
|
+
const src = `classDiagram
|
|
145
|
+
class Left
|
|
146
|
+
class TallMid {
|
|
147
|
+
${manyAttrs('d', 6)}
|
|
148
|
+
}
|
|
149
|
+
class RightSrc
|
|
150
|
+
class Kid
|
|
151
|
+
RightSrc <|-- Kid`
|
|
152
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
153
|
+
expectAllPresentOnce(ascii, [
|
|
154
|
+
'+ d0: String',
|
|
155
|
+
'+ d1: String',
|
|
156
|
+
'+ d2: String',
|
|
157
|
+
'+ d3: String',
|
|
158
|
+
'+ d4: String',
|
|
159
|
+
'+ d5: String',
|
|
160
|
+
])
|
|
161
|
+
expectNoBoxOverlap(ascii, ['Left', 'TallMid', 'RightSrc', 'Kid'])
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('"target above source" branch with a from-side marker renders without corrupting a box', () => {
|
|
165
|
+
// The "above" branch (this file's second `else if`) has no
|
|
166
|
+
// findClearColumn collision-avoidance at all — it always draws the
|
|
167
|
+
// straight jog/vertical/horizontal/vertical/jog path, so it's just as
|
|
168
|
+
// exposed to this bug as the "below" branch, and is guarded the same
|
|
169
|
+
// way here. A `Root --> A` / `A <|-- B` / `B --> A` back-edge forces
|
|
170
|
+
// A's level above B's (the level-assignment BFS keeps the *longest*
|
|
171
|
+
// path to a node, so B->A's back edge pushes A past B) — the
|
|
172
|
+
// `A <|-- B` edge then runs from the now-deeper A up to the
|
|
173
|
+
// shallower B, taking the "above" branch with its hollow-triangle
|
|
174
|
+
// marker at the 'from' end (A), a combination no other test in this
|
|
175
|
+
// file exercises.
|
|
176
|
+
const src = `classDiagram
|
|
177
|
+
class Root
|
|
178
|
+
class A
|
|
179
|
+
class B
|
|
180
|
+
class Extra
|
|
181
|
+
Root --> A
|
|
182
|
+
A <|-- B
|
|
183
|
+
B --> A`
|
|
184
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
185
|
+
expectNoBoxOverlap(ascii, ['Root', 'A', 'B', 'Extra'])
|
|
186
|
+
})
|
|
187
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for #487 — "ASCII class diagram: relationship labels
|
|
3
|
+
* ignore detour routing, appear attached to wrong box."
|
|
4
|
+
*
|
|
5
|
+
* Root cause: `renderClassAscii` anchored every relationship's label at the
|
|
6
|
+
* *idealized straight-line* midpoint between its source and target
|
|
7
|
+
* connection columns (`idealMidX = Math.floor((fromCX + toCX) / 2)`), even
|
|
8
|
+
* when the relationship's line actually detours around an intermediate box
|
|
9
|
+
* (the `needsDetour` / `findClearColumn` branch). On the "Class: MVC
|
|
10
|
+
* Architecture" sample, `Controller --> View : refreshes` detours around
|
|
11
|
+
* `Model` to reach `View`, but its label still anchored on the straight
|
|
12
|
+
* Controller/View midpoint — which happened to coincide with
|
|
13
|
+
* `Controller --> Model : updates`'s own (non-detoured, correct) label one
|
|
14
|
+
* row below it, making both labels read as though they terminated at
|
|
15
|
+
* `Model`.
|
|
16
|
+
*
|
|
17
|
+
* The fix (`computeLabelAnchor` in `class-diagram.ts`) anchors a detoured
|
|
18
|
+
* relationship's label on its *actual routed path* instead: the midpoint of
|
|
19
|
+
* the detour's vertical trunk (falling back to the longer horizontal jog
|
|
20
|
+
* when the trunk has no vertical room), flush against the trunk's clear
|
|
21
|
+
* side rather than centered on it — centering a label on the trunk's single
|
|
22
|
+
* clear column would re-overlap the very box the trunk was routed around,
|
|
23
|
+
* since `findClearColumn` only guarantees that one column is clear, not a
|
|
24
|
+
* whole label-width window.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, it, expect } from 'vitest'
|
|
28
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
29
|
+
import { findBoxRect } from './helpers/ascii-form.ts'
|
|
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 — detour-aware relationship label placement (issue #487)', () => {
|
|
55
|
+
it("places `refreshes` on its own row, off `updates`'s arrowhead column", () => {
|
|
56
|
+
const ascii = renderMermaidASCII(MVC_SOURCE, { colorMode: 'none' })
|
|
57
|
+
const lines = ascii.split('\n')
|
|
58
|
+
|
|
59
|
+
const updatesRow = lines.findIndex((l) => l.includes('updates'))
|
|
60
|
+
const refreshesRow = lines.findIndex((l) => l.includes('refreshes'))
|
|
61
|
+
expect(updatesRow).toBeGreaterThanOrEqual(0)
|
|
62
|
+
expect(refreshesRow).toBeGreaterThanOrEqual(0)
|
|
63
|
+
|
|
64
|
+
// Before the fix, both labels sat on adjacent rows directly above the
|
|
65
|
+
// single arrowhead entering Model, reading as though both relationships
|
|
66
|
+
// terminated there. `refreshes` must not share `updates`'s column range
|
|
67
|
+
// (the sole visual cue, pre-fix, that made them look like the same
|
|
68
|
+
// termination) on any row, and in particular not on `updates`'s own row.
|
|
69
|
+
expect(refreshesRow).not.toBe(updatesRow)
|
|
70
|
+
|
|
71
|
+
const updatesLine = lines[updatesRow]!
|
|
72
|
+
const updatesStart = updatesLine.indexOf('updates')
|
|
73
|
+
const updatesEnd = updatesStart + 'updates'.length - 1
|
|
74
|
+
|
|
75
|
+
const refreshesLine = lines[refreshesRow]!
|
|
76
|
+
const refreshesStart = refreshesLine.indexOf('refreshes')
|
|
77
|
+
const refreshesEnd = refreshesStart + 'refreshes'.length - 1
|
|
78
|
+
|
|
79
|
+
// No column overlap between the two labels anywhere they might appear.
|
|
80
|
+
const overlapsColumns =
|
|
81
|
+
refreshesStart <= updatesEnd && updatesStart <= refreshesEnd
|
|
82
|
+
expect(overlapsColumns).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("anchors `refreshes` past Model's right border, not centered inside its footprint", () => {
|
|
86
|
+
const ascii = renderMermaidASCII(MVC_SOURCE, { colorMode: 'none' })
|
|
87
|
+
const lines = ascii.split('\n')
|
|
88
|
+
|
|
89
|
+
// Model's box right edge (column-based, not a literal-column-0
|
|
90
|
+
// assumption — issue #971 can legitimately shift Model's own column
|
|
91
|
+
// when it has a real single qualifying parent to align under, same as
|
|
92
|
+
// it does here against Controller). `refreshes` — the label for the
|
|
93
|
+
// relationship that detours around Model to reach View — must land
|
|
94
|
+
// strictly to the right of that edge on every row it occupies, never
|
|
95
|
+
// overlapping Model's own footprint the way the straight-line midpoint
|
|
96
|
+
// used to (issue #487).
|
|
97
|
+
const modelRect = findBoxRect(ascii, 'Model')
|
|
98
|
+
const modelRightBorder = modelRect.x1
|
|
99
|
+
expect(modelRightBorder).toBeGreaterThan(0)
|
|
100
|
+
|
|
101
|
+
const refreshesRow = lines.findIndex((l) => l.includes('refreshes'))
|
|
102
|
+
expect(refreshesRow).toBeGreaterThanOrEqual(0)
|
|
103
|
+
const refreshesStart = lines[refreshesRow]!.indexOf('refreshes')
|
|
104
|
+
expect(refreshesStart).toBeGreaterThan(modelRightBorder)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('does not move any non-detoured relationship label (reads, notifies, updates stay put)', () => {
|
|
108
|
+
const ascii = renderMermaidASCII(MVC_SOURCE, { colorMode: 'none' })
|
|
109
|
+
for (const label of ['updates', 'reads', 'notifies', 'refreshes']) {
|
|
110
|
+
expect(ascii).toContain(label)
|
|
111
|
+
}
|
|
112
|
+
expect(ascii).not.toContain('…')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII class diagram compartment tests (issue #87)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
7
|
+
|
|
8
|
+
describe('ASCII class diagram box compartments', () => {
|
|
9
|
+
it('omits the attrs compartment for a class with methods but no attributes (regression)', () => {
|
|
10
|
+
// Before the fix, buildClassSections() always returned a 3-section box
|
|
11
|
+
// once methods were non-empty, even with zero attrs — drawMultiBox then
|
|
12
|
+
// rendered that empty section as a visible blank compartment.
|
|
13
|
+
const ascii = renderMermaidASCII(
|
|
14
|
+
`classDiagram
|
|
15
|
+
class Animal {
|
|
16
|
+
+makeSound()
|
|
17
|
+
}`,
|
|
18
|
+
{ useAscii: true },
|
|
19
|
+
)
|
|
20
|
+
const lines = ascii.split('\n').filter((l) => l.trim().length > 0)
|
|
21
|
+
// A box with N sections has N+1 horizontal border lines (top, N-1
|
|
22
|
+
// internal dividers, bottom). 2 sections (header + methods) -> 3.
|
|
23
|
+
const dividerCount = lines.filter((l) => /^\+-+\+$/.test(l.trim())).length
|
|
24
|
+
expect(dividerCount).toBe(3)
|
|
25
|
+
expect(ascii).toContain('makeSound')
|
|
26
|
+
expect(ascii).not.toMatch(/\|\s*\|\n\+-+\+\n\|\s*\+makeSound/)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('omits the methods compartment for a class with attrs but no methods (no regression)', () => {
|
|
30
|
+
const ascii = renderMermaidASCII(
|
|
31
|
+
`classDiagram
|
|
32
|
+
class Point {
|
|
33
|
+
+int x
|
|
34
|
+
}`,
|
|
35
|
+
{ useAscii: true },
|
|
36
|
+
)
|
|
37
|
+
expect(ascii).toContain('x')
|
|
38
|
+
const lines = ascii.split('\n').filter((l) => l.trim().length > 0)
|
|
39
|
+
const dividerCount = lines.filter((l) => /^\+-+\+$/.test(l.trim())).length
|
|
40
|
+
expect(dividerCount).toBe(3)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('renders both compartments for a class with attrs and methods', () => {
|
|
44
|
+
const ascii = renderMermaidASCII(
|
|
45
|
+
`classDiagram
|
|
46
|
+
class Circle {
|
|
47
|
+
+radius int
|
|
48
|
+
+draw() void
|
|
49
|
+
}`,
|
|
50
|
+
{ useAscii: true },
|
|
51
|
+
)
|
|
52
|
+
expect(ascii).toContain('radius')
|
|
53
|
+
expect(ascii).toContain('draw')
|
|
54
|
+
const lines = ascii.split('\n').filter((l) => l.trim().length > 0)
|
|
55
|
+
const dividerCount = lines.filter((l) => /^\+-+\+$/.test(l.trim())).length
|
|
56
|
+
expect(dividerCount).toBe(4)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('renders header only for a class with neither attrs nor methods', () => {
|
|
60
|
+
const ascii = renderMermaidASCII(
|
|
61
|
+
`classDiagram
|
|
62
|
+
class Empty`,
|
|
63
|
+
{ useAscii: true },
|
|
64
|
+
)
|
|
65
|
+
expect(ascii).toContain('Empty')
|
|
66
|
+
const lines = ascii.split('\n').filter((l) => l.trim().length > 0)
|
|
67
|
+
const dividerCount = lines.filter((l) => /^\+-+\+$/.test(l.trim())).length
|
|
68
|
+
expect(dividerCount).toBe(2)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for a pre-existing, previously-untested branch in
|
|
3
|
+
* `renderClassAscii`'s label placement: when a relationship's label would
|
|
4
|
+
* land inside another class's box (e.g. a "skip" edge whose two endpoints
|
|
5
|
+
* straddle a third class positioned between them), the renderer must find
|
|
6
|
+
* a clear gap instead of overwriting that box's content.
|
|
7
|
+
*
|
|
8
|
+
* `A --> C : skips`, with `B` (from the unrelated `A --> B --> C` chain)
|
|
9
|
+
* positioned directly between them, puts `skips`'s ideal label position
|
|
10
|
+
* inside B's box — this covers `renderClassAscii`'s `labelInBox` /
|
|
11
|
+
* gap-search fallback.
|
|
12
|
+
*
|
|
13
|
+
* Since #487's detour-aware label placement, `skips` (which detours around
|
|
14
|
+
* B) anchors on its own detour trunk — a column past B's right border —
|
|
15
|
+
* rather than searching for an entirely box-free *row*. That can legitimately
|
|
16
|
+
* put the label on the same terminal row as one of B's content lines (the
|
|
17
|
+
* same pattern the MVC sample's `refreshes` label uses, sharing a row with
|
|
18
|
+
* Model's compartment separator), as long as it never overlaps B's own
|
|
19
|
+
* *columns* — this test now checks that non-overlap directly instead of
|
|
20
|
+
* requiring a different row entirely.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from 'vitest'
|
|
24
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
25
|
+
|
|
26
|
+
describe('ASCII class diagram — label collision with an intermediate box', () => {
|
|
27
|
+
it('moves a skip-edge label out of an intermediate class box instead of overwriting it', () => {
|
|
28
|
+
const ascii = renderMermaidASCII(
|
|
29
|
+
`classDiagram
|
|
30
|
+
class A
|
|
31
|
+
class B
|
|
32
|
+
class C
|
|
33
|
+
A --> B
|
|
34
|
+
B --> C
|
|
35
|
+
A --> C : skips`,
|
|
36
|
+
{ colorMode: 'none' },
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const lines = ascii.split('\n')
|
|
40
|
+
const labelRow = lines.findIndex((l) => l.includes('skips'))
|
|
41
|
+
expect(labelRow).toBeGreaterThanOrEqual(0)
|
|
42
|
+
|
|
43
|
+
// B's box must still render intact — "B" itself, and both of its box
|
|
44
|
+
// borders — nowhere overwritten by the "skips" label text: wherever the
|
|
45
|
+
// label lands (even on the same row as one of B's own lines), its
|
|
46
|
+
// columns must fall entirely outside B's box columns.
|
|
47
|
+
const bRow = lines.findIndex((l) => l.includes('│ B │'))
|
|
48
|
+
expect(bRow).toBeGreaterThanOrEqual(0)
|
|
49
|
+
const bLine = lines[bRow]!
|
|
50
|
+
const bBoxStart = bLine.indexOf('│ B │')
|
|
51
|
+
const bBoxEnd = bBoxStart + '│ B │'.length - 1
|
|
52
|
+
|
|
53
|
+
const labelLine = lines[labelRow]!
|
|
54
|
+
const labelStart = labelLine.indexOf('skips')
|
|
55
|
+
const labelEnd = labelStart + 'skips'.length - 1
|
|
56
|
+
const overlapsOnSharedRow =
|
|
57
|
+
labelRow === bRow && labelStart <= bBoxEnd && bBoxStart <= labelEnd
|
|
58
|
+
expect(overlapsOnSharedRow).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for issue #531: at a small enough `paddingY`, a
|
|
3
|
+
* relationship label could vanish from ASCII class-diagram output with no
|
|
4
|
+
* error, no ellipsis, and no trace.
|
|
5
|
+
*
|
|
6
|
+
* Root cause: `renderClassAscii` resolves each relationship label's row in
|
|
7
|
+
* two independent places. `computeLabelAnchor` gives a label a "raw" ideal
|
|
8
|
+
* row (`baseMidY`). If that row lands inside a box, a runtime fallback (the
|
|
9
|
+
* "box-gap scan") searches the from/to gap for the nearest row that is at
|
|
10
|
+
* least clear of boxes, and the draw pass uses whatever row the fallback
|
|
11
|
+
* picked. Separately, a horizontal-territory precompute (added for issue
|
|
12
|
+
* #447, to stop two same-row labels from overwriting each other) decides
|
|
13
|
+
* which labels need X-territory splitting by comparing each pair's row
|
|
14
|
+
* bands — but it computed those bands from the *raw* `baseMidY`, before the
|
|
15
|
+
* box-gap-scan fallback could move a label to a completely different row.
|
|
16
|
+
* Two relationships whose *ideal* rows never overlapped could still both
|
|
17
|
+
* resolve, via the fallback, to the *same actual* row — invisibly to the
|
|
18
|
+
* territory precompute, which never split their columns because it never
|
|
19
|
+
* saw them as colliding. The later relationship's unconditional `setC`
|
|
20
|
+
* write then silently overwrote the earlier one's already-drawn label.
|
|
21
|
+
*
|
|
22
|
+
* A second, related gap: even once territory rowStart/rowEnd values reflect
|
|
23
|
+
* the real (post-fallback) row, the split algorithm only ever compared a
|
|
24
|
+
* label against its immediate neighbor by `idealMidX` order. When a third
|
|
25
|
+
* label — on a different, non-colliding row — happened to sit between two
|
|
26
|
+
* same-row colliding labels in `idealMidX` order, the pair that actually
|
|
27
|
+
* needed splitting was never adjacent in that order and so was never
|
|
28
|
+
* compared at all.
|
|
29
|
+
*
|
|
30
|
+
* The fix (`resolveLabelFinalY` in class-diagram.ts) resolves each label's
|
|
31
|
+
* final row once, up front, and both the territory precompute and the draw
|
|
32
|
+
* pass reuse that single resolved value — so they can never disagree.
|
|
33
|
+
* Territory splitting also now scans outward for the nearest row-*actually*-
|
|
34
|
+
* overlapping neighbor instead of stopping at the immediate one. Neither
|
|
35
|
+
* fix is specific to detour routing (issue #487/#489's "route around an
|
|
36
|
+
* intermediate box" mechanism): the second test below reproduces the same
|
|
37
|
+
* silent-drop failure with a diagram where no relationship detours at all,
|
|
38
|
+
* confirming the bug (and the fix) live in the general label-placement
|
|
39
|
+
* mechanism, not the detour-trunk logic.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { describe, it, expect } from 'vitest'
|
|
43
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
44
|
+
|
|
45
|
+
/** paddingY values below the default (5) that the issue found all reproduce
|
|
46
|
+
* the same corruption — the vertical gap floors at 1 row, so nothing about
|
|
47
|
+
* it changes at more extreme negative values. */
|
|
48
|
+
const tightPaddingYValues = [4, 3, 2, 1, 0, -1, -5, -10]
|
|
49
|
+
|
|
50
|
+
describe('ASCII class diagram — relationship label row collision (issue #531)', () => {
|
|
51
|
+
describe("detour-involving repro (the issue's own minimal repro)", () => {
|
|
52
|
+
const src = `classDiagram
|
|
53
|
+
class Model
|
|
54
|
+
class View
|
|
55
|
+
class Controller
|
|
56
|
+
Controller --> Model : updates
|
|
57
|
+
Controller --> View : refreshes
|
|
58
|
+
View --> Model : reads
|
|
59
|
+
Model ..> View : notifies`
|
|
60
|
+
|
|
61
|
+
it('renders every label in full at the default paddingY', () => {
|
|
62
|
+
const out = renderMermaidASCII(src, { colorMode: 'none' })
|
|
63
|
+
expect(out).toContain('updates')
|
|
64
|
+
expect(out).toContain('refreshes')
|
|
65
|
+
expect(out).toContain('reads')
|
|
66
|
+
expect(out).toContain('notifies')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it.each(tightPaddingYValues)(
|
|
70
|
+
'never silently drops a label at paddingY=%d',
|
|
71
|
+
(paddingY) => {
|
|
72
|
+
const out = renderMermaidASCII(src, { colorMode: 'none', paddingY })
|
|
73
|
+
|
|
74
|
+
// Three of the four labels have room to render in full regardless.
|
|
75
|
+
expect(out).toContain('updates')
|
|
76
|
+
expect(out).toContain('reads')
|
|
77
|
+
expect(out).toContain('notifies')
|
|
78
|
+
|
|
79
|
+
// `refreshes` shares a row with `updates` at this tight spacing and
|
|
80
|
+
// must yield some of its width via territory-splitting/truncation
|
|
81
|
+
// (issue #447's established, accepted behavior for genuinely
|
|
82
|
+
// insufficient space) — but it must never disappear *entirely* the
|
|
83
|
+
// way it (or `updates`, depending on exact geometry) did before the
|
|
84
|
+
// fix. Assert the real invariant rather than pinning the exact
|
|
85
|
+
// truncated glyphs: the row that carries `updates` must carry some
|
|
86
|
+
// non-blank content besides it, where the old bug left nothing.
|
|
87
|
+
const line = out.split('\n').find((l) => l.includes('updates'))
|
|
88
|
+
expect(line).toBeDefined()
|
|
89
|
+
const withoutUpdates = line!.replace('updates', '').trim()
|
|
90
|
+
expect(withoutUpdates.length).toBeGreaterThan(0)
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('non-detour repro (general mechanism, no detour routing involved)', () => {
|
|
96
|
+
// A -> B, B -> C (adjacent, direct) plus A -> C (a "skip" relationship
|
|
97
|
+
// whose direct straight-line midpoint lands inside B's box, triggering
|
|
98
|
+
// the same box-gap-scan fallback purely from vertical box geometry —
|
|
99
|
+
// no relationship here ever detours horizontally). Confirms the bug (and
|
|
100
|
+
// the fix) are independent of the #487/#489 detour-trunk logic, per the
|
|
101
|
+
// issue's own "Scope note".
|
|
102
|
+
const src = `classDiagram
|
|
103
|
+
class A
|
|
104
|
+
class B
|
|
105
|
+
class C
|
|
106
|
+
A --> B : sib1
|
|
107
|
+
B --> C : sib2
|
|
108
|
+
A --> C : longlabelhere`
|
|
109
|
+
|
|
110
|
+
it('renders every label in full at the default paddingY', () => {
|
|
111
|
+
const out = renderMermaidASCII(src, { colorMode: 'none' })
|
|
112
|
+
expect(out).toContain('sib1')
|
|
113
|
+
expect(out).toContain('sib2')
|
|
114
|
+
expect(out).toContain('longlabelhere')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it.each(tightPaddingYValues)(
|
|
118
|
+
'never silently drops a label at paddingY=%d',
|
|
119
|
+
(paddingY) => {
|
|
120
|
+
const out = renderMermaidASCII(src, { colorMode: 'none', paddingY })
|
|
121
|
+
|
|
122
|
+
expect(out).toContain('sib1')
|
|
123
|
+
expect(out).toContain('sib2')
|
|
124
|
+
|
|
125
|
+
// `longlabelhere` shares a row with `sib1` at this tight spacing and
|
|
126
|
+
// gets truncated rather than disappearing outright — see the note
|
|
127
|
+
// in the sibling describe block above.
|
|
128
|
+
const line = out.split('\n').find((l) => l.includes('sib1'))
|
|
129
|
+
expect(line).toBeDefined()
|
|
130
|
+
const withoutSib1 = line!.replace('sib1', '').trim()
|
|
131
|
+
expect(withoutSib1.length).toBeGreaterThan(0)
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for a bug discovered while integrating issue #447 with
|
|
3
|
+
* issue #448: `renderClassAscii`'s label-territory precomputation
|
|
4
|
+
* (added for #447) split/truncated two labels whenever their *horizontal*
|
|
5
|
+
* positions were close, even when the two relationships render on
|
|
6
|
+
* completely different rows and never actually visually collide — e.g.
|
|
7
|
+
* one class's two separate outgoing edges to two different targets at
|
|
8
|
+
* different heights (`Controller --> Model : updates` and
|
|
9
|
+
* `Controller --> View : refreshes` in the "Class: MVC Architecture"
|
|
10
|
+
* sample, which — before this fix — rendered as `refre…`/`…ates` even
|
|
11
|
+
* though nothing else was drawn anywhere near those cells).
|
|
12
|
+
*
|
|
13
|
+
* The fix tracks each label's row span (rowStart/rowEnd, mirroring the
|
|
14
|
+
* draw pass's own baseMidY calculation) and only treats two labels as
|
|
15
|
+
* needing territory-splitting when their row spans actually overlap.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from 'vitest'
|
|
19
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
20
|
+
|
|
21
|
+
describe('ASCII class diagram — label territory is row-aware (issue #447 follow-up)', () => {
|
|
22
|
+
it('does not truncate two labels whose relationships share a source but target different, differently-positioned classes', () => {
|
|
23
|
+
// Uses the full "Class: MVC Architecture" sample source (samples-data.ts)
|
|
24
|
+
// — reproducing this needs its exact box layout; a hand-trimmed subset
|
|
25
|
+
// (e.g. dropping the `reads`/`notifies` pair) shifts row positions
|
|
26
|
+
// enough that `updates`/`refreshes` no longer collide, defeating the
|
|
27
|
+
// regression check.
|
|
28
|
+
const ascii = renderMermaidASCII(
|
|
29
|
+
`classDiagram
|
|
30
|
+
class Model {
|
|
31
|
+
-data Map
|
|
32
|
+
+getData() Map
|
|
33
|
+
+setData(key, val) void
|
|
34
|
+
+notify() void
|
|
35
|
+
}
|
|
36
|
+
class View {
|
|
37
|
+
-model Model
|
|
38
|
+
+render() void
|
|
39
|
+
+update() void
|
|
40
|
+
}
|
|
41
|
+
class Controller {
|
|
42
|
+
-model Model
|
|
43
|
+
-view View
|
|
44
|
+
+handleInput(event) void
|
|
45
|
+
+updateModel(data) void
|
|
46
|
+
}
|
|
47
|
+
Controller --> Model : updates
|
|
48
|
+
Controller --> View : refreshes
|
|
49
|
+
View --> Model : reads
|
|
50
|
+
Model ..> View : notifies`,
|
|
51
|
+
{ colorMode: 'none' },
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
expect(ascii).toContain('updates')
|
|
55
|
+
expect(ascii).toContain('refreshes')
|
|
56
|
+
})
|
|
57
|
+
})
|