@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,458 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII ER diagram relationship-label/box-corruption regression tests
|
|
3
|
+
// (GitHub issue #350)
|
|
4
|
+
//
|
|
5
|
+
// erDiagram relationship lines and labels were drawn without checking
|
|
6
|
+
// whether the straight-line path they took actually stayed clear of other
|
|
7
|
+
// entities' boxes. Two entities placed in the same row of the layout grid
|
|
8
|
+
// were always connected with a straight horizontal line even when a third
|
|
9
|
+
// entity sat physically between them (e.g. ORDER↔SHIPMENT with LINE_ITEM in
|
|
10
|
+
// between), so the line and its label were composited directly on top of
|
|
11
|
+
// that third entity's border and attribute text — a well-formed-looking but
|
|
12
|
+
// wrong attribute name (e.g. "int totalCents" silently became
|
|
13
|
+
// "int hasalCents"). A parallel bug existed for vertical connections: the
|
|
14
|
+
// horizontal jog between two entities in different rows used the naive
|
|
15
|
+
// vertical midpoint, which could still land inside a row-mate that was
|
|
16
|
+
// taller than the connection's own upper entity.
|
|
17
|
+
//
|
|
18
|
+
// The fix makes relationship routing obstruction-aware (detour around a
|
|
19
|
+
// row-mate that sits between two connected entities; clamp the vertical
|
|
20
|
+
// jog into the row-gap band that's actually free of every entity) and adds
|
|
21
|
+
// a last-resort occupancy guard so a relationship line, marker, or label
|
|
22
|
+
// can never overwrite a cell already reserved by an entity box or by an
|
|
23
|
+
// earlier relationship's label — see setCGuarded in src/ascii/er-diagram.ts.
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
import { describe, it, expect } from 'vitest'
|
|
27
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
28
|
+
|
|
29
|
+
/** Assert every one of `needles` appears verbatim, exactly once, in `text`. */
|
|
30
|
+
function expectAllPresentOnce(text: string, needles: string[]): void {
|
|
31
|
+
for (const needle of needles) {
|
|
32
|
+
const count = text.split(needle).length - 1
|
|
33
|
+
expect(count, `expected "${needle}" to appear exactly once`).toBe(1)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The exact minimal repro from issue #350.
|
|
38
|
+
const REPRO_MINIMAL = `erDiagram
|
|
39
|
+
ORDER ||--|{ LINE_ITEM : contains
|
|
40
|
+
ORDER ||--o| SHIPMENT : fulfilled_by
|
|
41
|
+
PRODUCT ||--o{ LINE_ITEM : appears_in
|
|
42
|
+
WAREHOUSE ||--o{ SHIPMENT : dispatches
|
|
43
|
+
ORDER {
|
|
44
|
+
string id
|
|
45
|
+
string customerId
|
|
46
|
+
int totalCents
|
|
47
|
+
string status
|
|
48
|
+
}
|
|
49
|
+
LINE_ITEM {
|
|
50
|
+
string orderId
|
|
51
|
+
string productId
|
|
52
|
+
int quantity
|
|
53
|
+
int unitPriceCents
|
|
54
|
+
}
|
|
55
|
+
PRODUCT {
|
|
56
|
+
string sku
|
|
57
|
+
string title
|
|
58
|
+
int priceCents
|
|
59
|
+
}
|
|
60
|
+
`
|
|
61
|
+
|
|
62
|
+
// The fuller repro from issue #350 — more entities, more relationships
|
|
63
|
+
// crowding the same rows, including two labels ("has" / "fulfilled_by")
|
|
64
|
+
// that both need to route through the same row-gap band.
|
|
65
|
+
const REPRO_FULL = `erDiagram
|
|
66
|
+
CUSTOMER ||--o{ ORDER : places
|
|
67
|
+
CUSTOMER ||--o{ ADDRESS : has
|
|
68
|
+
ORDER ||--|{ LINE_ITEM : contains
|
|
69
|
+
ORDER ||--o| SHIPMENT : fulfilled_by
|
|
70
|
+
PRODUCT ||--o{ LINE_ITEM : appears_in
|
|
71
|
+
PRODUCT }o--|| CATEGORY : belongs_to
|
|
72
|
+
WAREHOUSE ||--o{ SHIPMENT : dispatches
|
|
73
|
+
|
|
74
|
+
CUSTOMER {
|
|
75
|
+
string id
|
|
76
|
+
string email
|
|
77
|
+
string name
|
|
78
|
+
date createdAt
|
|
79
|
+
}
|
|
80
|
+
ORDER {
|
|
81
|
+
string id
|
|
82
|
+
string customerId
|
|
83
|
+
int totalCents
|
|
84
|
+
string status
|
|
85
|
+
}
|
|
86
|
+
LINE_ITEM {
|
|
87
|
+
string orderId
|
|
88
|
+
string productId
|
|
89
|
+
int quantity
|
|
90
|
+
int unitPriceCents
|
|
91
|
+
}
|
|
92
|
+
PRODUCT {
|
|
93
|
+
string sku
|
|
94
|
+
string title
|
|
95
|
+
int priceCents
|
|
96
|
+
}
|
|
97
|
+
`
|
|
98
|
+
|
|
99
|
+
describe('ASCII ER relationship labels never corrupt entity boxes (issue #350)', () => {
|
|
100
|
+
it('renders every attribute name verbatim in the minimal repro', () => {
|
|
101
|
+
const ascii = renderMermaidASCII(REPRO_MINIMAL, { colorMode: 'none' })
|
|
102
|
+
expectAllPresentOnce(ascii, [
|
|
103
|
+
'string id',
|
|
104
|
+
'string customerId',
|
|
105
|
+
'int totalCents',
|
|
106
|
+
'string status',
|
|
107
|
+
'string orderId',
|
|
108
|
+
'string productId',
|
|
109
|
+
'int quantity',
|
|
110
|
+
'int unitPriceCents',
|
|
111
|
+
'string sku',
|
|
112
|
+
'string title',
|
|
113
|
+
'int priceCents',
|
|
114
|
+
])
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('renders every relationship label in full in the minimal repro', () => {
|
|
118
|
+
const ascii = renderMermaidASCII(REPRO_MINIMAL, { colorMode: 'none' })
|
|
119
|
+
expect(ascii).toContain('contains')
|
|
120
|
+
expect(ascii).toContain('fulfilled_by')
|
|
121
|
+
expect(ascii).toContain('appears_in')
|
|
122
|
+
expect(ascii).toContain('dispatches')
|
|
123
|
+
// The original bug doubled a trailing character when a label was
|
|
124
|
+
// partially overwritten (e.g. "fulfilled_byy").
|
|
125
|
+
expect(ascii).not.toContain('fulfilled_byy')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('does not produce the specific corrupted values from issue #350', () => {
|
|
129
|
+
const ascii = renderMermaidASCII(REPRO_MINIMAL, { colorMode: 'none' })
|
|
130
|
+
// "int unitPriceCents" (LINE_ITEM) must never read as though a label
|
|
131
|
+
// overwrote part of it, and "productId"/"quantity" must survive.
|
|
132
|
+
expect(ascii).not.toContain('fulfilled_byy')
|
|
133
|
+
expect(ascii).toContain('productId')
|
|
134
|
+
expect(ascii).toContain('quantity')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('renders every attribute name verbatim in the fuller repro', () => {
|
|
138
|
+
const ascii = renderMermaidASCII(REPRO_FULL, { colorMode: 'none' })
|
|
139
|
+
// "string id" is intentionally excluded here — CUSTOMER and ORDER both
|
|
140
|
+
// declare it, so it legitimately appears twice.
|
|
141
|
+
expectAllPresentOnce(ascii, [
|
|
142
|
+
'string email',
|
|
143
|
+
'string name',
|
|
144
|
+
'date createdAt',
|
|
145
|
+
'string customerId',
|
|
146
|
+
'int totalCents',
|
|
147
|
+
'string status',
|
|
148
|
+
'string orderId',
|
|
149
|
+
'string productId',
|
|
150
|
+
'int quantity',
|
|
151
|
+
'int unitPriceCents',
|
|
152
|
+
'string sku',
|
|
153
|
+
'string title',
|
|
154
|
+
'int priceCents',
|
|
155
|
+
])
|
|
156
|
+
// The issue's headline corruption: "int totalCents" silently became
|
|
157
|
+
// "int hasalCents" when the "has" label (CUSTOMER↔ADDRESS, skipping
|
|
158
|
+
// over ORDER) was composited into ORDER's box.
|
|
159
|
+
expect(ascii).not.toContain('hasalCents')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('renders every relationship label in full in the fuller repro', () => {
|
|
163
|
+
const ascii = renderMermaidASCII(REPRO_FULL, { colorMode: 'none' })
|
|
164
|
+
expect(ascii).toContain('places')
|
|
165
|
+
expect(ascii).toContain('has')
|
|
166
|
+
expect(ascii).toContain('contains')
|
|
167
|
+
expect(ascii).toContain('fulfilled_by')
|
|
168
|
+
expect(ascii).toContain('appears_in')
|
|
169
|
+
expect(ascii).toContain('belongs_to')
|
|
170
|
+
expect(ascii).toContain('dispatches')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('keeps every entity box border fully intact (no line/label punched through a border)', () => {
|
|
174
|
+
const ascii = renderMermaidASCII(REPRO_MINIMAL, { colorMode: 'none' })
|
|
175
|
+
const lines = ascii.split('\n')
|
|
176
|
+
// Every box's header row (e.g. "│ LINE_ITEM ") must still open with the
|
|
177
|
+
// left border directly followed by a space, never a relationship glyph.
|
|
178
|
+
for (const name of [
|
|
179
|
+
'ORDER',
|
|
180
|
+
'LINE_ITEM',
|
|
181
|
+
'SHIPMENT',
|
|
182
|
+
'PRODUCT',
|
|
183
|
+
'WAREHOUSE',
|
|
184
|
+
]) {
|
|
185
|
+
const headerLine = lines.find((l) => l.includes(`│ ${name}`))
|
|
186
|
+
expect(headerLine, `expected a header line for ${name}`).toBeDefined()
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('detects attribute corruption the same way the issue does: every declared attribute name must appear verbatim', () => {
|
|
191
|
+
// Mirrors the issue's own "Detection" section: erDiagram attribute names
|
|
192
|
+
// are known at parse time, so corruption shows up as a missing name.
|
|
193
|
+
const ascii = renderMermaidASCII(REPRO_MINIMAL, { colorMode: 'none' })
|
|
194
|
+
const declaredAttributeNames = [
|
|
195
|
+
'id',
|
|
196
|
+
'customerId',
|
|
197
|
+
'totalCents',
|
|
198
|
+
'status',
|
|
199
|
+
'orderId',
|
|
200
|
+
'productId',
|
|
201
|
+
'quantity',
|
|
202
|
+
'unitPriceCents',
|
|
203
|
+
'sku',
|
|
204
|
+
'title',
|
|
205
|
+
'priceCents',
|
|
206
|
+
]
|
|
207
|
+
for (const name of declaredAttributeNames) {
|
|
208
|
+
expect(
|
|
209
|
+
ascii,
|
|
210
|
+
`expected declared attribute "${name}" to survive`,
|
|
211
|
+
).toContain(name)
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('ASCII ER relationship label routing — adjacent-entity regression (issue #350)', () => {
|
|
217
|
+
it('keeps a short label between two adjacent entities compact (no unnecessary detour)', () => {
|
|
218
|
+
const ascii = renderMermaidASCII(
|
|
219
|
+
`erDiagram
|
|
220
|
+
A ||--o{ B : x`,
|
|
221
|
+
{ colorMode: 'none' },
|
|
222
|
+
)
|
|
223
|
+
const lines = ascii.split('\n')
|
|
224
|
+
const line = lines.find((l) => l.includes('A') && l.includes('│'))
|
|
225
|
+
expect(line).toBeDefined()
|
|
226
|
+
expect(line!.length).toBeLessThan(30)
|
|
227
|
+
expect(ascii).toContain('x')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('renders a normal two-entity diagram exactly as before (direct line, no detour)', () => {
|
|
231
|
+
const ascii = renderMermaidASCII(
|
|
232
|
+
`erDiagram
|
|
233
|
+
CUSTOMER ||--o{ ORDER : places`,
|
|
234
|
+
{ colorMode: 'none' },
|
|
235
|
+
)
|
|
236
|
+
// No third entity to dodge, so the connector should sit on a single row
|
|
237
|
+
// directly below the connecting line — not detoured further down.
|
|
238
|
+
const lines = ascii.split('\n')
|
|
239
|
+
const placesIdx = lines.findIndex((l) => l.includes('places'))
|
|
240
|
+
const connectorIdx = lines.findIndex(
|
|
241
|
+
(l) => l.includes('○') || l.includes('╢') || l.includes('╟'),
|
|
242
|
+
)
|
|
243
|
+
expect(placesIdx).toBeGreaterThan(-1)
|
|
244
|
+
expect(connectorIdx).toBeGreaterThan(-1)
|
|
245
|
+
// The label sits exactly one row below the connector row in the direct
|
|
246
|
+
// (undetoured) case.
|
|
247
|
+
expect(placesIdx).toBe(connectorIdx + 1)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('renders three entities in a row with adjacent-only relationships without detouring', () => {
|
|
251
|
+
// B sits between A and C, but the only relationships are A-B and B-C —
|
|
252
|
+
// both adjacent pairs, so neither needs to route around anything.
|
|
253
|
+
const ascii = renderMermaidASCII(
|
|
254
|
+
`erDiagram
|
|
255
|
+
A ||--o{ B : ab
|
|
256
|
+
B ||--o{ C : bc`,
|
|
257
|
+
{ colorMode: 'none' },
|
|
258
|
+
)
|
|
259
|
+
expect(ascii).toContain('ab')
|
|
260
|
+
expect(ascii).toContain('bc')
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe('ASCII ER relationship label routing — long labels and crowded areas (issue #350)', () => {
|
|
265
|
+
it('routes a long label between two non-adjacent same-row entities around the entity between them', () => {
|
|
266
|
+
// ORDER and SHIPMENT are placed in the same row with LINE_ITEM between
|
|
267
|
+
// them (5-entity component ⇒ 3 columns per row); "fulfilled_by" must
|
|
268
|
+
// detour around LINE_ITEM rather than being written into its box.
|
|
269
|
+
const ascii = renderMermaidASCII(REPRO_MINIMAL, { colorMode: 'none' })
|
|
270
|
+
expect(ascii).toContain('fulfilled_by')
|
|
271
|
+
expect(ascii).toContain('string productId')
|
|
272
|
+
expect(ascii).toContain('int quantity')
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('keeps multiple relationship labels distinct when several need to route through the same row-gap band', () => {
|
|
276
|
+
// In REPRO_FULL, CUSTOMER↔ADDRESS ("has") and ORDER↔SHIPMENT
|
|
277
|
+
// ("fulfilled_by") both detour through the row-gap band below
|
|
278
|
+
// CUSTOMER/ORDER/ADDRESS's row. Neither should silently disappear or
|
|
279
|
+
// get merged into the other.
|
|
280
|
+
const ascii = renderMermaidASCII(REPRO_FULL, { colorMode: 'none' })
|
|
281
|
+
expect(ascii).toContain('has')
|
|
282
|
+
expect(ascii).toContain('fulfilled_by')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('does not corrupt attribute text with a longer, multi-word relationship label', () => {
|
|
286
|
+
const ascii = renderMermaidASCII(
|
|
287
|
+
`erDiagram
|
|
288
|
+
CUSTOMER ||--o{ ORDER : places
|
|
289
|
+
CUSTOMER ||--o{ ADDRESS : "ships to"
|
|
290
|
+
ORDER {
|
|
291
|
+
string id
|
|
292
|
+
int totalCents
|
|
293
|
+
}
|
|
294
|
+
CUSTOMER {
|
|
295
|
+
string id
|
|
296
|
+
string name
|
|
297
|
+
}`,
|
|
298
|
+
{ colorMode: 'none' },
|
|
299
|
+
)
|
|
300
|
+
expect(ascii).toContain('ships to')
|
|
301
|
+
expect(ascii).toContain('int totalCents')
|
|
302
|
+
expect(ascii).toContain('string name')
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// ============================================================================
|
|
307
|
+
// Follow-up findings from CodeRabbit's review of the #350 fix (PR #391) —
|
|
308
|
+
// both are the same "silently overwritten" shape as the original issue,
|
|
309
|
+
// just between two relationships instead of a relationship and a box.
|
|
310
|
+
// ============================================================================
|
|
311
|
+
|
|
312
|
+
describe('ASCII ER relationship label routing — parallel relationships (CodeRabbit follow-up)', () => {
|
|
313
|
+
it('keeps both labels when two relationships connect the exact same adjacent pair (same-row)', () => {
|
|
314
|
+
// A↔B are adjacent (no obstruction), so both "first" and "second" land
|
|
315
|
+
// on the identical direct-path row unless the label search kicks in.
|
|
316
|
+
const ascii = renderMermaidASCII(
|
|
317
|
+
`erDiagram
|
|
318
|
+
A ||--o{ B : first
|
|
319
|
+
A ||--o{ B : second`,
|
|
320
|
+
{ colorMode: 'none' },
|
|
321
|
+
)
|
|
322
|
+
expect(ascii).toContain('first')
|
|
323
|
+
expect(ascii).toContain('second')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('keeps both labels when two relationships connect the exact same vertical pair', () => {
|
|
327
|
+
// A (row1) and C (row2) are directly above/below each other; both
|
|
328
|
+
// "first" and "second" would compute the identical band midpoint
|
|
329
|
+
// without pickBandY's collision search.
|
|
330
|
+
const ascii = renderMermaidASCII(
|
|
331
|
+
`erDiagram
|
|
332
|
+
A ||--o{ B : sibling
|
|
333
|
+
A ||--o{ C : first
|
|
334
|
+
A ||--o{ C : second`,
|
|
335
|
+
{ colorMode: 'none' },
|
|
336
|
+
)
|
|
337
|
+
expect(ascii).toContain('sibling')
|
|
338
|
+
expect(ascii).toContain('first')
|
|
339
|
+
expect(ascii).toContain('second')
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
describe('ASCII ER relationship routing — multi-row obstruction (CodeRabbit follow-up)', () => {
|
|
344
|
+
it('keeps a relationship connected and boxes uncorrupted when it skips two rows', () => {
|
|
345
|
+
// 7 entities -> maxPerRow = 3 -> row1: A,B,C; row2: D,E,F; row3: G.
|
|
346
|
+
// A-G shares D's column two rows down, so the single-row-gap clamp
|
|
347
|
+
// alone doesn't cover it — this needs the free-column bypass.
|
|
348
|
+
const ascii = renderMermaidASCII(
|
|
349
|
+
`erDiagram
|
|
350
|
+
A ||--o{ B : ab
|
|
351
|
+
B ||--o{ C : bc
|
|
352
|
+
C ||--o{ D : cd
|
|
353
|
+
D ||--o{ E : de
|
|
354
|
+
E ||--o{ F : ef
|
|
355
|
+
F ||--o{ G : fg
|
|
356
|
+
A ||--o{ G : ag
|
|
357
|
+
A {
|
|
358
|
+
string id
|
|
359
|
+
string name
|
|
360
|
+
}
|
|
361
|
+
D {
|
|
362
|
+
string id
|
|
363
|
+
string title
|
|
364
|
+
}
|
|
365
|
+
G {
|
|
366
|
+
string id
|
|
367
|
+
string label
|
|
368
|
+
}`,
|
|
369
|
+
{ colorMode: 'none' },
|
|
370
|
+
)
|
|
371
|
+
// No box content lost.
|
|
372
|
+
expect(ascii).toContain('string id')
|
|
373
|
+
expect(ascii).toContain('string name')
|
|
374
|
+
expect(ascii).toContain('string title')
|
|
375
|
+
expect(ascii).toContain('string label')
|
|
376
|
+
// Every relationship label survives, including the multi-row one.
|
|
377
|
+
expect(ascii).toContain('ab')
|
|
378
|
+
expect(ascii).toContain('bc')
|
|
379
|
+
expect(ascii).toContain('cd')
|
|
380
|
+
expect(ascii).toContain('de')
|
|
381
|
+
expect(ascii).toContain('ef')
|
|
382
|
+
expect(ascii).toContain('fg')
|
|
383
|
+
expect(ascii).toContain('ag')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('routes the multi-row relationship through a column with no box at all (not a coincidentally-empty cell)', () => {
|
|
387
|
+
// Same shape as above, but every entity carries an attribute so a
|
|
388
|
+
// "disconnected" line landing inside a box (rather than routing
|
|
389
|
+
// around it) would corrupt visible attribute text, not just an empty
|
|
390
|
+
// interior cell.
|
|
391
|
+
const ascii = renderMermaidASCII(
|
|
392
|
+
`erDiagram
|
|
393
|
+
A ||--o{ B : ab
|
|
394
|
+
B ||--o{ C : bc
|
|
395
|
+
C ||--o{ D : cd
|
|
396
|
+
D ||--o{ E : de
|
|
397
|
+
E ||--o{ F : ef
|
|
398
|
+
F ||--o{ G : fg
|
|
399
|
+
A ||--o{ G : ag
|
|
400
|
+
A { string a1 }
|
|
401
|
+
B { string b1 }
|
|
402
|
+
C { string c1 }
|
|
403
|
+
D { string d1 }
|
|
404
|
+
E { string e1 }
|
|
405
|
+
F { string f1 }
|
|
406
|
+
G { string g1 }`,
|
|
407
|
+
{ colorMode: 'none' },
|
|
408
|
+
)
|
|
409
|
+
for (const attr of ['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1']) {
|
|
410
|
+
expect(ascii, `expected attribute "${attr}" to survive`).toContain(attr)
|
|
411
|
+
}
|
|
412
|
+
expect(ascii).toContain('ag')
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('draws the multi-row line outside the intervening box, not just around a coincidental gap', () => {
|
|
416
|
+
// A precise, geometry-aware check (not just "content survives"):
|
|
417
|
+
// setCGuarded alone would already stop attribute-text corruption even
|
|
418
|
+
// with the multi-row bypass disabled — it just leaves the line
|
|
419
|
+
// discontinuous through D's box, which "content survives" assertions
|
|
420
|
+
// can't tell apart from a real, routed connection. This test locates
|
|
421
|
+
// D's own top-border row and asserts a line/marker character exists
|
|
422
|
+
// *outside* D's box width on that exact row — proof the connector
|
|
423
|
+
// steps around D's box rather than merely being guarded away inside
|
|
424
|
+
// it (verified against a version of this fix with the bypass
|
|
425
|
+
// disabled: the extra character outside D's box does not appear).
|
|
426
|
+
const ascii = renderMermaidASCII(
|
|
427
|
+
`erDiagram
|
|
428
|
+
A ||--o{ B : ab
|
|
429
|
+
B ||--o{ C : bc
|
|
430
|
+
C ||--o{ D : cd
|
|
431
|
+
D ||--o{ E : de
|
|
432
|
+
E ||--o{ F : ef
|
|
433
|
+
F ||--o{ G : fg
|
|
434
|
+
A ||--o{ G : ag
|
|
435
|
+
A {
|
|
436
|
+
string id
|
|
437
|
+
string name
|
|
438
|
+
}
|
|
439
|
+
D {
|
|
440
|
+
string id
|
|
441
|
+
string title
|
|
442
|
+
}
|
|
443
|
+
G {
|
|
444
|
+
string id
|
|
445
|
+
string label
|
|
446
|
+
}`,
|
|
447
|
+
{ colorMode: 'none' },
|
|
448
|
+
)
|
|
449
|
+
const lines = ascii.split('\n')
|
|
450
|
+
const dTopIdx = lines.findIndex(
|
|
451
|
+
(l, i) => l.includes('┌') && lines[i + 1]?.includes('│ D'),
|
|
452
|
+
)
|
|
453
|
+
expect(dTopIdx, 'expected to find D’s top-border row').toBeGreaterThan(-1)
|
|
454
|
+
const dTopLine = lines[dTopIdx]!
|
|
455
|
+
const afterDBox = dTopLine.slice(dTopLine.indexOf('┐') + 1)
|
|
456
|
+
expect(afterDBox).toMatch(/[│╢╟○]/)
|
|
457
|
+
})
|
|
458
|
+
})
|