@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,432 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII sequence diagram structural "form" invariants
|
|
3
|
+
//
|
|
4
|
+
// svg-samples.visual.test.ts / ascii-samples.visual.test.ts only self-diff
|
|
5
|
+
// against a committed baseline screenshot — a defect present when that
|
|
6
|
+
// baseline was captured passes forever. This file asserts structural
|
|
7
|
+
// properties the renderer should hold regardless of any specific fixture:
|
|
8
|
+
// a block's frame must fit its own label (generalizes PR #387's fix beyond
|
|
9
|
+
// its single fixed repro), a note must not visually collide with a lifeline
|
|
10
|
+
// it isn't attached to, and an `actor` participant must render distinctly
|
|
11
|
+
// from a plain `participant` box.
|
|
12
|
+
//
|
|
13
|
+
// Cases that assert already-correct, already-fixed behavior are plain
|
|
14
|
+
// `it()`s — regression locks. Cases that assert behavior confirmed broken
|
|
15
|
+
// today use `it.fails(...)`: the assertion body is real and genuinely
|
|
16
|
+
// exercises the bug, so it fails loudly (in the good direction, since
|
|
17
|
+
// `it.fails` inverts pass/fail) the moment someone fixes it elsewhere —
|
|
18
|
+
// signaling that case should be promoted to a plain `it()`. These bugs are
|
|
19
|
+
// intentionally not fixed in this change; fixing them is separate work.
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
import { describe, it, expect } from 'vitest'
|
|
23
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
24
|
+
import { findBoxRect, findTextRect } from './helpers/ascii-form.ts'
|
|
25
|
+
|
|
26
|
+
const LONG_LABEL = 'a rather long condition label that used to get clipped'
|
|
27
|
+
const VERY_LONG_LABEL =
|
|
28
|
+
'an extremely long condition label that spans far more columns than any single lifeline gap would ever naturally provide'
|
|
29
|
+
|
|
30
|
+
function participants(names: string[]): string {
|
|
31
|
+
return names.map((n) => ` participant ${n}`).join('\n')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const BORDER_ASCII = new Set(['+', '-', '|'])
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A block's left/right wall columns, found via the row containing
|
|
38
|
+
* `anchorText` (e.g. the block type keyword, which — unlike a header/divider
|
|
39
|
+
* label — is never itself truncated, so it's a stable anchor even when the
|
|
40
|
+
* label content next to it is clipped). The walls are constant across the
|
|
41
|
+
* whole block (header, every divider, and the body all share the same two
|
|
42
|
+
* wall columns), so one anchor row is enough regardless of which row a
|
|
43
|
+
* later containment check is against.
|
|
44
|
+
*/
|
|
45
|
+
function blockFrameSpan(
|
|
46
|
+
ascii: string,
|
|
47
|
+
anchorText: string,
|
|
48
|
+
): { x0: number; x1: number } {
|
|
49
|
+
const lines = ascii.split('\n')
|
|
50
|
+
const y = lines.findIndex((l) => l.includes(anchorText))
|
|
51
|
+
if (y === -1)
|
|
52
|
+
throw new Error(`blockFrameSpan: no row containing "${anchorText}"`)
|
|
53
|
+
const line = lines[y]!
|
|
54
|
+
let x0 = line.length
|
|
55
|
+
let x1 = -1
|
|
56
|
+
for (let x = 0; x < line.length; x++) {
|
|
57
|
+
if (BORDER_ASCII.has(line[x]!)) {
|
|
58
|
+
x0 = Math.min(x0, x)
|
|
59
|
+
x1 = Math.max(x1, x)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (x1 === -1) throw new Error(`blockFrameSpan: no border chars on row ${y}`)
|
|
63
|
+
return { x0, x1 }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Block-wall-fits-label matrix (generalizes PR #387 / issue #352)
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
describe('ASCII sequence — block frame fits its own label (issue #352 family)', () => {
|
|
71
|
+
// PR #387 (issue #352's fix) closes this gap generically — for every
|
|
72
|
+
// block type, not just its own alt/else repro — by measuring the
|
|
73
|
+
// longest header/divider label up front and widening the wall (see
|
|
74
|
+
// `maxBlockLabelWidth` in src/ascii/sequence.ts) rather than deriving it
|
|
75
|
+
// purely from `minLX`/`maxLX`. The long-label matrix below was written
|
|
76
|
+
// as `it.fails` against pre-#387 `main`, documenting that the gap wasn't
|
|
77
|
+
// limited to `alt`/`else` or to one label length; now that the fix has
|
|
78
|
+
// landed, every case is a plain `it()` regression lock, same as the
|
|
79
|
+
// short-label cases above.
|
|
80
|
+
const shortLabelCases: Array<{
|
|
81
|
+
name: string
|
|
82
|
+
type: 'alt' | 'loop' | 'opt' | 'par' | 'critical'
|
|
83
|
+
actors: string[]
|
|
84
|
+
}> = [
|
|
85
|
+
{ name: 'alt, short label, 2 actors', type: 'alt', actors: ['A', 'B'] },
|
|
86
|
+
{ name: 'loop, short label, 2 actors', type: 'loop', actors: ['A', 'B'] },
|
|
87
|
+
{
|
|
88
|
+
name: 'opt, short label, 3 actors',
|
|
89
|
+
type: 'opt',
|
|
90
|
+
actors: ['A', 'B', 'C'],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'par, short label, 3 actors',
|
|
94
|
+
type: 'par',
|
|
95
|
+
actors: ['A', 'B', 'C'],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'critical, short label, 2 actors',
|
|
99
|
+
type: 'critical',
|
|
100
|
+
actors: ['A', 'B'],
|
|
101
|
+
},
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
for (const c of shortLabelCases) {
|
|
105
|
+
it(c.name, () => {
|
|
106
|
+
const src = `sequenceDiagram
|
|
107
|
+
${participants(c.actors)}
|
|
108
|
+
${c.type} ok
|
|
109
|
+
${c.actors[0]}->>${c.actors[1]}: x
|
|
110
|
+
end`
|
|
111
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
112
|
+
expect(ascii).toContain('ok')
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const longLabelCases: Array<{
|
|
117
|
+
name: string
|
|
118
|
+
type: 'alt' | 'loop' | 'opt' | 'par' | 'critical'
|
|
119
|
+
label: string
|
|
120
|
+
actors: string[]
|
|
121
|
+
}> = [
|
|
122
|
+
{
|
|
123
|
+
name: 'alt, long label, 2 actors',
|
|
124
|
+
type: 'alt',
|
|
125
|
+
label: LONG_LABEL,
|
|
126
|
+
actors: ['A', 'B'],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'alt, very long label, 2 actors',
|
|
130
|
+
type: 'alt',
|
|
131
|
+
label: VERY_LONG_LABEL,
|
|
132
|
+
actors: ['A', 'B'],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'alt, long label, 5 actors',
|
|
136
|
+
type: 'alt',
|
|
137
|
+
label: LONG_LABEL,
|
|
138
|
+
actors: ['A', 'B', 'C', 'D', 'E'],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'loop, long label, 2 actors',
|
|
142
|
+
type: 'loop',
|
|
143
|
+
label: LONG_LABEL,
|
|
144
|
+
actors: ['A', 'B'],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'opt, long label, 3 actors',
|
|
148
|
+
type: 'opt',
|
|
149
|
+
label: LONG_LABEL,
|
|
150
|
+
actors: ['A', 'B', 'C'],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'par, long label, 3 actors',
|
|
154
|
+
type: 'par',
|
|
155
|
+
label: LONG_LABEL,
|
|
156
|
+
actors: ['A', 'B', 'C'],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'critical, very long label, 2 actors',
|
|
160
|
+
type: 'critical',
|
|
161
|
+
label: VERY_LONG_LABEL,
|
|
162
|
+
actors: ['A', 'B'],
|
|
163
|
+
},
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
for (const c of longLabelCases) {
|
|
167
|
+
it(c.name, () => {
|
|
168
|
+
const src = `sequenceDiagram
|
|
169
|
+
${participants(c.actors)}
|
|
170
|
+
${c.type} ${c.label}
|
|
171
|
+
${c.actors[0]}->>${c.actors[1]}: x
|
|
172
|
+
end`
|
|
173
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
174
|
+
// Not just "does the full label appear somewhere" — it must also sit
|
|
175
|
+
// inside the block's own walls, not spill past them.
|
|
176
|
+
expect(ascii).toContain(c.label)
|
|
177
|
+
const frame = blockFrameSpan(ascii, c.type)
|
|
178
|
+
const labelRect = findTextRect(ascii, c.label)
|
|
179
|
+
expect(labelRect.x0).toBeGreaterThanOrEqual(frame.x0)
|
|
180
|
+
expect(labelRect.x1).toBeLessThanOrEqual(frame.x1)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
it('alt/else with multiple branches: every divider label survives unclipped and stays inside the frame', () => {
|
|
185
|
+
const src = `sequenceDiagram
|
|
186
|
+
participant A
|
|
187
|
+
participant B
|
|
188
|
+
alt ${LONG_LABEL}
|
|
189
|
+
A->>B: x
|
|
190
|
+
else ${VERY_LONG_LABEL}
|
|
191
|
+
A->>B: y
|
|
192
|
+
end`
|
|
193
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
194
|
+
expect(ascii).toContain(LONG_LABEL)
|
|
195
|
+
expect(ascii).toContain(VERY_LONG_LABEL)
|
|
196
|
+
// Walls are constant across the whole block, so the header row alone
|
|
197
|
+
// (anchored on "alt", never itself truncated) gives the frame span
|
|
198
|
+
// both labels — header and divider — must stay inside.
|
|
199
|
+
const frame = blockFrameSpan(ascii, 'alt')
|
|
200
|
+
for (const label of [LONG_LABEL, VERY_LONG_LABEL]) {
|
|
201
|
+
const rect = findTextRect(ascii, label)
|
|
202
|
+
expect(rect.x0).toBeGreaterThanOrEqual(frame.x0)
|
|
203
|
+
expect(rect.x1).toBeLessThanOrEqual(frame.x1)
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Note cross-contamination matrix
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
/** Column of an actor's lifeline: the horizontal midpoint of its header box. */
|
|
213
|
+
function lifelineColumn(ascii: string, actorName: string): number {
|
|
214
|
+
const rect = findBoxRect(ascii, actorName)
|
|
215
|
+
return Math.round((rect.x0 + rect.x1) / 2)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
describe('ASCII sequence — notes do not collide with an unrelated lifeline', () => {
|
|
219
|
+
// Fixed by issue #953 case A: `sequence.ts`'s note-x calculation
|
|
220
|
+
// (`nx = llX[aIdx] - nWidth - 1`) used to clamp straight to
|
|
221
|
+
// `Math.max(0, nx)`, so a note wider than the leftmost actor's (small)
|
|
222
|
+
// left margin clamped to column 0 and swallowed its own actor's lifeline
|
|
223
|
+
// column instead of sitting entirely to its left. The fix reserves room
|
|
224
|
+
// in the leftmost lifeline's own initial position (`llX[0]`) for its
|
|
225
|
+
// widest `left` note up front, so `nx` never needs clamping in the first
|
|
226
|
+
// place. Per this file's header convention, a case confirmed fixed is
|
|
227
|
+
// promoted from `it.fails` to a plain `it()` regression lock.
|
|
228
|
+
it("note left of the leftmost actor does not overlap that actor's own lifeline", () => {
|
|
229
|
+
const src = `sequenceDiagram
|
|
230
|
+
participant A as Alice
|
|
231
|
+
participant B as Bob
|
|
232
|
+
Note left of A: ${LONG_LABEL}
|
|
233
|
+
A->>B: hi`
|
|
234
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
235
|
+
// findBoxRect, not findTextRect — the note's own border/padding must
|
|
236
|
+
// also stay clear of the lifeline, not just its bare text span.
|
|
237
|
+
const noteRect = findBoxRect(ascii, LONG_LABEL)
|
|
238
|
+
const aliceCol = lifelineColumn(ascii, 'Alice')
|
|
239
|
+
expect(aliceCol < noteRect.x0 || aliceCol > noteRect.x1).toBe(true)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
// Fixed by issue #953 case B: same clamp as above, but the victim is a
|
|
243
|
+
// *different* actor's lifeline — the exact shape visible in
|
|
244
|
+
// samples-data.ts's "Sequence: Notes (Right/Left/Over)" sample ("Alice
|
|
245
|
+
// prepares" reaching into Bob's lifeline). The fix reserves a
|
|
246
|
+
// note-width-aware gap between adjacent lifelines (see `leftNoteWidth`/
|
|
247
|
+
// `rightNoteWidth` in sequence.ts), so a `left` note's own gap is sized to
|
|
248
|
+
// fit it before `nx` is ever computed.
|
|
249
|
+
it("note left of a non-leftmost actor does not overlap the left-neighbor's lifeline", () => {
|
|
250
|
+
const src = `sequenceDiagram
|
|
251
|
+
participant A as Alice
|
|
252
|
+
participant B as Bob
|
|
253
|
+
participant C as Carol
|
|
254
|
+
Note left of B: ${LONG_LABEL}
|
|
255
|
+
B->>C: hi`
|
|
256
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
257
|
+
const noteRect = findBoxRect(ascii, LONG_LABEL)
|
|
258
|
+
const aliceCol = lifelineColumn(ascii, 'Alice')
|
|
259
|
+
expect(aliceCol < noteRect.x0 || aliceCol > noteRect.x1).toBe(true)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// Fixed by issue #953 case C: the lifeline gap used to be sized only from
|
|
263
|
+
// message-label widths — note width never entered that calculation for
|
|
264
|
+
// either side. Notes draw last (after lifelines/boxes/messages/blocks),
|
|
265
|
+
// so a right-note wider than the gap to the *next* actor silently
|
|
266
|
+
// overwrote that actor's lifeline. No `Math.max(0, …)` clamp was involved
|
|
267
|
+
// — a genuinely different bug from the two above, sharing only the
|
|
268
|
+
// visible symptom. The fix folds note width into the same gap-sizing pass
|
|
269
|
+
// that already accounts for message labels.
|
|
270
|
+
it("note right of an actor does not overlap the next actor's lifeline", () => {
|
|
271
|
+
const src = `sequenceDiagram
|
|
272
|
+
participant A as Alice
|
|
273
|
+
participant B as Bob
|
|
274
|
+
participant C as Carol
|
|
275
|
+
A->>B: hi
|
|
276
|
+
Note right of B: ${LONG_LABEL}`
|
|
277
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
278
|
+
const noteRect = findBoxRect(ascii, LONG_LABEL)
|
|
279
|
+
const carolCol = lifelineColumn(ascii, 'Carol')
|
|
280
|
+
expect(carolCol < noteRect.x0 || carolCol > noteRect.x1).toBe(true)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// Fixed by issue #992: a single-actor `Note over` centers on that actor's
|
|
284
|
+
// own lifeline (`nx = llX[aIdx] - Math.floor(nWidth / 2)`), but neither
|
|
285
|
+
// half of that width was reserved during layout — so a note wider than
|
|
286
|
+
// the actor's own column spilled into whichever neighbor was closer,
|
|
287
|
+
// exactly the shape cases A-C above already fixed for `left`/`right`
|
|
288
|
+
// notes. This is the exact repro from the issue (two actors, a note over
|
|
289
|
+
// the first one). A *multi*-actor `Note over A,B` is unaffected —
|
|
290
|
+
// spanning both actors is its intended shape (see the "note over two
|
|
291
|
+
// actors" case below), not this bug.
|
|
292
|
+
it("single-actor 'over' note on the first actor does not overlap the next actor's lifeline (issue #992 repro)", () => {
|
|
293
|
+
const src = `sequenceDiagram
|
|
294
|
+
participant A as Alice
|
|
295
|
+
participant B as Bob
|
|
296
|
+
Note over A: ${LONG_LABEL}
|
|
297
|
+
A->>B: hi`
|
|
298
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
299
|
+
const noteRect = findBoxRect(ascii, LONG_LABEL)
|
|
300
|
+
const bobCol = lifelineColumn(ascii, 'Bob')
|
|
301
|
+
expect(bobCol < noteRect.x0 || bobCol > noteRect.x1).toBe(true)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// Same fix, but a middle actor: both halves of the note (left half toward
|
|
305
|
+
// Alice, right half toward Carol) need to be reserved, not just the
|
|
306
|
+
// leftmost-actor case above.
|
|
307
|
+
it("single-actor 'over' note on a middle actor does not overlap either neighbor's lifeline", () => {
|
|
308
|
+
const src = `sequenceDiagram
|
|
309
|
+
participant A as Alice
|
|
310
|
+
participant B as Bob
|
|
311
|
+
participant C as Carol
|
|
312
|
+
A->>B: hi
|
|
313
|
+
Note over B: ${LONG_LABEL}`
|
|
314
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
315
|
+
const noteRect = findBoxRect(ascii, LONG_LABEL)
|
|
316
|
+
const aliceCol = lifelineColumn(ascii, 'Alice')
|
|
317
|
+
const carolCol = lifelineColumn(ascii, 'Carol')
|
|
318
|
+
expect(aliceCol < noteRect.x0 || aliceCol > noteRect.x1).toBe(true)
|
|
319
|
+
expect(carolCol < noteRect.x0 || carolCol > noteRect.x1).toBe(true)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it("short single-actor 'over' note does not overlap the next actor", () => {
|
|
323
|
+
const src = `sequenceDiagram
|
|
324
|
+
participant A as Alice
|
|
325
|
+
participant B as Bob
|
|
326
|
+
Note over A: ok
|
|
327
|
+
A->>B: hi`
|
|
328
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
329
|
+
const noteRect = findBoxRect(ascii, 'ok')
|
|
330
|
+
const bobCol = lifelineColumn(ascii, 'Bob')
|
|
331
|
+
expect(bobCol < noteRect.x0 || bobCol > noteRect.x1).toBe(true)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
// Regression locks: short notes, which don't hit any of the three bugs
|
|
335
|
+
// above, must keep rendering without collision.
|
|
336
|
+
it('short note left of a middle actor does not overlap its left neighbor', () => {
|
|
337
|
+
const src = `sequenceDiagram
|
|
338
|
+
participant A as Alice
|
|
339
|
+
participant B as Bob
|
|
340
|
+
participant C as Carol
|
|
341
|
+
Note left of B: hi
|
|
342
|
+
B->>C: hi`
|
|
343
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
344
|
+
const noteRect = findBoxRect(ascii, 'hi')
|
|
345
|
+
const aliceCol = lifelineColumn(ascii, 'Alice')
|
|
346
|
+
expect(aliceCol < noteRect.x0 || aliceCol > noteRect.x1).toBe(true)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('short note right of an actor does not overlap the next actor', () => {
|
|
350
|
+
const src = `sequenceDiagram
|
|
351
|
+
participant A as Alice
|
|
352
|
+
participant B as Bob
|
|
353
|
+
participant C as Carol
|
|
354
|
+
A->>B: hi
|
|
355
|
+
Note right of B: ok`
|
|
356
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
357
|
+
const noteRect = findBoxRect(ascii, 'ok')
|
|
358
|
+
const carolCol = lifelineColumn(ascii, 'Carol')
|
|
359
|
+
expect(carolCol < noteRect.x0 || carolCol > noteRect.x1).toBe(true)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('note over two actors does not overlap a third, uninvolved actor', () => {
|
|
363
|
+
const src = `sequenceDiagram
|
|
364
|
+
participant A as Alice
|
|
365
|
+
participant B as Bob
|
|
366
|
+
participant C as Carol
|
|
367
|
+
A->>B: hi
|
|
368
|
+
Note over A,B: conversation complete`
|
|
369
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
370
|
+
const noteRect = findBoxRect(ascii, 'conversation complete')
|
|
371
|
+
const carolCol = lifelineColumn(ascii, 'Carol')
|
|
372
|
+
expect(carolCol < noteRect.x0 || carolCol > noteRect.x1).toBe(true)
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Actor-kind fidelity: `actor` must render distinctly from `participant`
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
type ActorPosition = 'first' | 'middle' | 'last'
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Declares `id` (as either `actor` or `participant`) at `position` among two
|
|
384
|
+
* other fixed participants — first/middle/last are only meaningful with at
|
|
385
|
+
* least one neighbor on the relevant side, so this always yields 3 declared
|
|
386
|
+
* actors, not just `id` alone.
|
|
387
|
+
*/
|
|
388
|
+
function declareActorAt(
|
|
389
|
+
position: ActorPosition,
|
|
390
|
+
kind: 'actor' | 'participant',
|
|
391
|
+
id: string,
|
|
392
|
+
): string {
|
|
393
|
+
const target = `${kind} ${id} as User`
|
|
394
|
+
const lines =
|
|
395
|
+
position === 'first'
|
|
396
|
+
? [target, 'participant S as System', 'participant DB as Database']
|
|
397
|
+
: position === 'last'
|
|
398
|
+
? ['participant S as System', 'participant DB as Database', target]
|
|
399
|
+
: ['participant S as System', target, 'participant DB as Database']
|
|
400
|
+
return lines.map((l) => ` ${l}`).join('\n')
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
describe('ASCII sequence — `actor` renders distinctly from `participant`', () => {
|
|
404
|
+
// Fixed by issue #449: `src/ascii/sequence.ts`'s `drawActorBox` now draws
|
|
405
|
+
// a small stick-figure glyph (`ACTOR_GLYPH_LINES`) above the label for
|
|
406
|
+
// `actor.type === 'actor'`, so an `actor` and a `participant` with the
|
|
407
|
+
// same label no longer render byte-for-byte identically. Per this file's
|
|
408
|
+
// header convention, a case confirmed fixed is promoted from `it.fails`
|
|
409
|
+
// to a plain `it()` regression lock.
|
|
410
|
+
it.each<[ActorPosition, string]>([
|
|
411
|
+
['first', 'U'],
|
|
412
|
+
['middle', 'U'],
|
|
413
|
+
['last', 'U'],
|
|
414
|
+
])(
|
|
415
|
+
'an %s-position `actor` looks different from a `participant` (%s)',
|
|
416
|
+
(position, id) => {
|
|
417
|
+
const asActor = renderMermaidASCII(
|
|
418
|
+
`sequenceDiagram
|
|
419
|
+
${declareActorAt(position, 'actor', id)}
|
|
420
|
+
${id}->>S: hi`,
|
|
421
|
+
{ useAscii: true },
|
|
422
|
+
)
|
|
423
|
+
const asParticipant = renderMermaidASCII(
|
|
424
|
+
`sequenceDiagram
|
|
425
|
+
${declareActorAt(position, 'participant', id)}
|
|
426
|
+
${id}->>S: hi`,
|
|
427
|
+
{ useAscii: true },
|
|
428
|
+
)
|
|
429
|
+
expect(asActor).not.toBe(asParticipant)
|
|
430
|
+
},
|
|
431
|
+
)
|
|
432
|
+
})
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII sequence diagram tests — parity gaps found comparing against
|
|
3
|
+
// AlexanderGrooff/mermaid-ascii: autonumber, bidirectional arrows, and
|
|
4
|
+
// undeclared multi-word actor names.
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest'
|
|
8
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
9
|
+
|
|
10
|
+
describe('ASCII sequence diagrams – autonumber', () => {
|
|
11
|
+
it('renders sequence numbers near each numbered arrow, in order', () => {
|
|
12
|
+
const result = renderMermaidASCII(
|
|
13
|
+
`sequenceDiagram
|
|
14
|
+
autonumber
|
|
15
|
+
A->>B: First
|
|
16
|
+
A->>B: Second`,
|
|
17
|
+
{ useAscii: false },
|
|
18
|
+
)
|
|
19
|
+
const lines = result.split('\n')
|
|
20
|
+
const firstArrowLine = lines.find((l) => l.includes('▶') && l.includes('1'))
|
|
21
|
+
const secondArrowLine = lines
|
|
22
|
+
.slice(lines.indexOf(firstArrowLine ?? ''))
|
|
23
|
+
.find((l) => l.includes('▶') && l.includes('2'))
|
|
24
|
+
expect(firstArrowLine).toBeDefined()
|
|
25
|
+
expect(secondArrowLine).toBeDefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('does not number messages before autonumber', () => {
|
|
29
|
+
const result = renderMermaidASCII(
|
|
30
|
+
`sequenceDiagram
|
|
31
|
+
A->>B: Unnumbered
|
|
32
|
+
autonumber
|
|
33
|
+
A->>B: Numbered`,
|
|
34
|
+
{ useAscii: false },
|
|
35
|
+
)
|
|
36
|
+
const lines = result.split('\n')
|
|
37
|
+
const unnumberedArrowLine = lines.find(
|
|
38
|
+
(l) => l.includes('▶') && !/[0-9]/.test(l),
|
|
39
|
+
)
|
|
40
|
+
const numberedArrowLine = lines.find(
|
|
41
|
+
(l) => l.includes('▶') && /[0-9]/.test(l),
|
|
42
|
+
)
|
|
43
|
+
expect(unnumberedArrowLine).toBeDefined()
|
|
44
|
+
expect(numberedArrowLine).toBeDefined()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('places the badge at the departure end for a right-to-left numbered message', () => {
|
|
48
|
+
// B->>A is right-to-left (B's lane is to the right of A's) — exercises
|
|
49
|
+
// the `!leftToRight` badge-placement branch, distinct from the
|
|
50
|
+
// left-to-right case in the first test above.
|
|
51
|
+
const result = renderMermaidASCII(
|
|
52
|
+
`sequenceDiagram
|
|
53
|
+
autonumber
|
|
54
|
+
A->>B: Hello
|
|
55
|
+
B->>A: Reply`,
|
|
56
|
+
{ useAscii: false },
|
|
57
|
+
)
|
|
58
|
+
const replyArrowLine = result
|
|
59
|
+
.split('\n')
|
|
60
|
+
.find((l) => l.includes('◀') && l.includes('2'))
|
|
61
|
+
expect(replyArrowLine).toBeDefined()
|
|
62
|
+
// The badge belongs at the DEPARTURE end (B, on the right), not the
|
|
63
|
+
// arrival end (A, where the arrowhead points) — assert the digit's
|
|
64
|
+
// column comes after the arrowhead's, not just that both appear
|
|
65
|
+
// somewhere on the same line (a badge misplaced at the arrival end
|
|
66
|
+
// would still satisfy a same-line-only check).
|
|
67
|
+
const arrowCol = replyArrowLine!.indexOf('◀')
|
|
68
|
+
const badgeCol = replyArrowLine!.indexOf('2')
|
|
69
|
+
expect(badgeCol).toBeGreaterThan(arrowCol)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('omits the badge (without corrupting the arrow) when it would not fit, left-to-right', () => {
|
|
73
|
+
const result = renderMermaidASCII(
|
|
74
|
+
`sequenceDiagram
|
|
75
|
+
autonumber 999999999999 1
|
|
76
|
+
A->>B: Yo`,
|
|
77
|
+
{ useAscii: false },
|
|
78
|
+
)
|
|
79
|
+
const arrowLine = result.split('\n').find((l) => l.includes('▶'))
|
|
80
|
+
expect(arrowLine).toBeDefined()
|
|
81
|
+
expect(arrowLine).not.toMatch(/[0-9]/)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('omits the badge (without corrupting the arrow) when it would not fit, right-to-left', () => {
|
|
85
|
+
const result = renderMermaidASCII(
|
|
86
|
+
`sequenceDiagram
|
|
87
|
+
participant A
|
|
88
|
+
participant B
|
|
89
|
+
autonumber 999999999999 1
|
|
90
|
+
B->>A: Yo`,
|
|
91
|
+
{ useAscii: false },
|
|
92
|
+
)
|
|
93
|
+
const arrowLine = result.split('\n').find((l) => l.includes('◀'))
|
|
94
|
+
expect(arrowLine).toBeDefined()
|
|
95
|
+
expect(arrowLine).not.toMatch(/[0-9]/)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('renders without autonumber unchanged (no stray digits near arrows)', () => {
|
|
99
|
+
const result = renderMermaidASCII(
|
|
100
|
+
`sequenceDiagram
|
|
101
|
+
A->>B: Hello`,
|
|
102
|
+
{ useAscii: false },
|
|
103
|
+
)
|
|
104
|
+
expect(result).toContain('Hello')
|
|
105
|
+
expect(result).not.toMatch(/[0-9]/)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('ASCII sequence diagrams – bidirectional arrows', () => {
|
|
110
|
+
it('draws arrowheads on both ends for a solid bidirectional arrow', () => {
|
|
111
|
+
const result = renderMermaidASCII(
|
|
112
|
+
`sequenceDiagram
|
|
113
|
+
A<<->>B: Sync call`,
|
|
114
|
+
{ useAscii: false },
|
|
115
|
+
)
|
|
116
|
+
expect(result).toContain('◀')
|
|
117
|
+
expect(result).toContain('▶')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('draws a dashed bidirectional arrow with arrowheads on both ends', () => {
|
|
121
|
+
const result = renderMermaidASCII(
|
|
122
|
+
`sequenceDiagram
|
|
123
|
+
A<<-->>B: Async call`,
|
|
124
|
+
{ useAscii: false },
|
|
125
|
+
)
|
|
126
|
+
expect(result).toContain('◀')
|
|
127
|
+
expect(result).toContain('▶')
|
|
128
|
+
expect(result).toContain('╌')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('a regular one-way arrow only gets one arrowhead', () => {
|
|
132
|
+
const result = renderMermaidASCII(
|
|
133
|
+
`sequenceDiagram
|
|
134
|
+
A->>B: Hello`,
|
|
135
|
+
{ useAscii: false },
|
|
136
|
+
)
|
|
137
|
+
const arrowLine = result.split('\n').find((l) => l.includes('▶'))
|
|
138
|
+
expect(arrowLine).toBeDefined()
|
|
139
|
+
expect(arrowLine).not.toContain('◀')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('renders bidirectional arrows in ASCII-only mode without special glyphs', () => {
|
|
143
|
+
const result = renderMermaidASCII(
|
|
144
|
+
`sequenceDiagram
|
|
145
|
+
A<<->>B: Sync call`,
|
|
146
|
+
{ useAscii: true },
|
|
147
|
+
)
|
|
148
|
+
expect(result).toContain('<')
|
|
149
|
+
expect(result).toContain('>')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('draws arrowheads on both ends when the bidirectional message runs right-to-left', () => {
|
|
153
|
+
// B is declared second (lane to the right of A), so B<<-->>A draws
|
|
154
|
+
// right-to-left — exercises the mirrored-arrowhead branch for
|
|
155
|
+
// `!leftToRight`, distinct from the left-to-right case above.
|
|
156
|
+
const result = renderMermaidASCII(
|
|
157
|
+
`sequenceDiagram
|
|
158
|
+
A->>B: Hello
|
|
159
|
+
B<<-->>A: Reply sync`,
|
|
160
|
+
{ useAscii: false },
|
|
161
|
+
)
|
|
162
|
+
const arrowLines = result
|
|
163
|
+
.split('\n')
|
|
164
|
+
.filter((l) => l.includes('◀') || l.includes('▶'))
|
|
165
|
+
expect(arrowLines).toHaveLength(2)
|
|
166
|
+
// First message (A->>B) is one-way, only gets the destination arrowhead.
|
|
167
|
+
expect(arrowLines[0]).not.toContain('◀')
|
|
168
|
+
expect(arrowLines[0]).toContain('▶')
|
|
169
|
+
// Second message (B<<-->>A) is bidirectional and right-to-left: the
|
|
170
|
+
// primary arrowhead lands on the left (A) end, the mirrored one on the
|
|
171
|
+
// right (B) end.
|
|
172
|
+
expect(arrowLines[1]).toContain('◀')
|
|
173
|
+
expect(arrowLines[1]).toContain('▶')
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('ASCII sequence diagrams – multi-word inline actor names', () => {
|
|
178
|
+
it('renders an undeclared actor name with a space and a hyphenated name', () => {
|
|
179
|
+
const result = renderMermaidASCII(
|
|
180
|
+
`sequenceDiagram
|
|
181
|
+
cron job->>customer-notifier: hi`,
|
|
182
|
+
{ useAscii: false },
|
|
183
|
+
)
|
|
184
|
+
expect(result).toContain('cron job')
|
|
185
|
+
expect(result).toContain('customer-notifier')
|
|
186
|
+
expect(result).toContain('hi')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('still renders plain single-word actor names correctly', () => {
|
|
190
|
+
const result = renderMermaidASCII(
|
|
191
|
+
`sequenceDiagram
|
|
192
|
+
Alice->>Bob: Hello`,
|
|
193
|
+
{ useAscii: false },
|
|
194
|
+
)
|
|
195
|
+
expect(result).toContain('Alice')
|
|
196
|
+
expect(result).toContain('Bob')
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe('ASCII sequence diagrams – standalone activate/deactivate (#419)', () => {
|
|
201
|
+
it('renders identically to the +/- shorthand, and does not leak the keyword as text', () => {
|
|
202
|
+
const standalone = renderMermaidASCII(
|
|
203
|
+
`sequenceDiagram
|
|
204
|
+
Alice->>John: Hello John, how are you?
|
|
205
|
+
activate John
|
|
206
|
+
John-->>Alice: Great!
|
|
207
|
+
deactivate John`,
|
|
208
|
+
{ useAscii: false },
|
|
209
|
+
)
|
|
210
|
+
const shorthand = renderMermaidASCII(
|
|
211
|
+
`sequenceDiagram
|
|
212
|
+
Alice->>+John: Hello John, how are you?
|
|
213
|
+
John-->>-Alice: Great!`,
|
|
214
|
+
{ useAscii: false },
|
|
215
|
+
)
|
|
216
|
+
expect(standalone).toBe(shorthand)
|
|
217
|
+
expect(standalone).not.toContain('activate')
|
|
218
|
+
})
|
|
219
|
+
})
|