zombie-mermaid 4.2.2 → 4.2.3
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/package.json +9 -7
- package/src/__tests__/architecture-integration.test.ts +0 -217
- package/src/__tests__/ascii-ansi.test.ts +0 -539
- package/src/__tests__/ascii-draw-bundles.test.ts +0 -849
- package/src/__tests__/ascii-edge-bundling-routing.test.ts +0 -214
- package/src/__tests__/ascii-edge-cross-style-overlap.test.ts +0 -216
- package/src/__tests__/ascii-grid-deep-chain.test.ts +0 -64
- package/src/__tests__/ascii-hyperlinks.test.ts +0 -364
- package/src/__tests__/ascii-nested-subgraph-sibling-order-444.test.ts +0 -108
- package/src/__tests__/ascii-nonmember-beside-frame-1283.test.ts +0 -113
- package/src/__tests__/ascii-package-boundary.test.ts +0 -291
- package/src/__tests__/ascii-parallel-edges-329.test.ts +0 -315
- package/src/__tests__/ascii-parallel-lane-border-1230.test.ts +0 -71
- package/src/__tests__/ascii-reverse-direction-edges-629.test.ts +0 -282
- package/src/__tests__/ascii-stacked-frame-geometry-1285.test.ts +0 -107
- package/src/__tests__/ascii-subgraph-bbox.test.ts +0 -139
- package/src/__tests__/ascii-subgraph-later-edgeless-301.test.ts +0 -277
- package/src/__tests__/browser.test.ts +0 -34
- package/src/__tests__/c4-integration.test.ts +0 -161
- package/src/__tests__/c4-orientation.test.ts +0 -162
- package/src/__tests__/c4-upstream-integration.test.ts +0 -211
- package/src/__tests__/class-click-interactions.test.ts +0 -232
- package/src/__tests__/class-integration.test.ts +0 -154
- package/src/__tests__/class-notes.test.ts +0 -317
- package/src/__tests__/class-parser-error-messages.test.ts +0 -162
- package/src/__tests__/class-parser.test.ts +0 -347
- package/src/__tests__/class-styling.test.ts +0 -485
- package/src/__tests__/cli-diagrams.test.ts +0 -106
- package/src/__tests__/cli-e2e.test.ts +0 -275
- package/src/__tests__/cli-entrypoint.test.ts +0 -146
- package/src/__tests__/cli-mcp-dispatch.test.ts +0 -68
- package/src/__tests__/cli-mcp.test.ts +0 -44
- package/src/__tests__/cli-png.test.ts +0 -35
- package/src/__tests__/cli-render.test.ts +0 -988
- package/src/__tests__/cli-test-helpers.ts +0 -54
- package/src/__tests__/cli-web-dispatch.test.ts +0 -62
- package/src/__tests__/cli-web.test.ts +0 -203
- package/src/__tests__/cli.test.ts +0 -1027
- package/src/__tests__/compound-flat-layout.test.ts +0 -901
- package/src/__tests__/direction-override.test.ts +0 -397
- package/src/__tests__/edge-approach-direction.test.ts +0 -295
- package/src/__tests__/edge-bundling-node-clearance.test.ts +0 -179
- package/src/__tests__/elk-layout-cache.test.ts +0 -223
- package/src/__tests__/er-click-unsupported.test.ts +0 -69
- package/src/__tests__/er-integration.test.ts +0 -531
- package/src/__tests__/er-parser-error-messages.test.ts +0 -82
- package/src/__tests__/er-parser.test.ts +0 -294
- package/src/__tests__/expanded-shapes.test.ts +0 -229
- package/src/__tests__/flowchart-back-edge-prereversal-1239.test.ts +0 -212
- package/src/__tests__/flowchart-syntax-gaps.test.ts +0 -371
- package/src/__tests__/guides-examples.test.ts +0 -181
- package/src/__tests__/integration.test.ts +0 -729
- package/src/__tests__/interactivity-config.test.ts +0 -723
- package/src/__tests__/interactivity-option.test.ts +0 -173
- package/src/__tests__/layout-disconnected.test.ts +0 -384
- package/src/__tests__/layout-engine.test.ts +0 -91
- package/src/__tests__/linkstyle.test.ts +0 -133
- package/src/__tests__/mcp-render-ascii-tool.test.ts +0 -95
- package/src/__tests__/mcp-render-svg-theme-guard.test.ts +0 -36
- package/src/__tests__/mcp-render-svg-tool.test.ts +0 -118
- package/src/__tests__/mermaid-entity-codes.test.ts +0 -114
- package/src/__tests__/monospace-metrics.test.ts +0 -74
- package/src/__tests__/multiline-labels.test.ts +0 -924
- package/src/__tests__/nested-subgraph-direction-crossing.test.ts +0 -322
- package/src/__tests__/nested-subgraph-edge-container-offset.test.ts +0 -151
- package/src/__tests__/package-info.test.ts +0 -51
- package/src/__tests__/parser.test.ts +0 -1544
- package/src/__tests__/render-options-overrides.test.ts +0 -257
- package/src/__tests__/render-options-per-type.test.ts +0 -178
- package/src/__tests__/renderer.test.ts +0 -743
- package/src/__tests__/resolve-colors.test.ts +0 -448
- package/src/__tests__/self-loop-edges.test.ts +0 -112
- package/src/__tests__/sequence-activation-check.test.ts +0 -219
- package/src/__tests__/sequence-integration.test.ts +0 -312
- package/src/__tests__/sequence-layout.test.ts +0 -1103
- package/src/__tests__/sequence-parser-syntax-errors.test.ts +0 -179
- package/src/__tests__/sequence-parser.test.ts +0 -821
- package/src/__tests__/start-marker-only-links-1386.test.ts +0 -104
- package/src/__tests__/state-composite-order-1287.test.ts +0 -132
- package/src/__tests__/state-flow-order-1240.test.ts +0 -113
- package/src/__tests__/state-styling.test.ts +0 -150
- package/src/__tests__/statements.test.ts +0 -264
- package/src/__tests__/styles.test.ts +0 -220
- package/src/__tests__/subgraph-title-edge-gap-1239.test.ts +0 -238
- package/src/__tests__/svg-accessible-name-conformance.test.ts +0 -276
- package/src/__tests__/svg-accessible-name.test.ts +0 -421
- package/src/__tests__/svg-csp-style-options.test.ts +0 -427
- package/src/__tests__/workspace-package-boundaries.test.ts +0 -336
- package/src/__tests__/xychart-integration.test.ts +0 -137
- package/src/__tests__/xychart-layout.test.ts +0 -205
- package/src/__tests__/xychart-parser-categories.test.ts +0 -78
- package/src/__tests__/xychart-parser-errors.test.ts +0 -95
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for #629: the reverse-direction half of #329.
|
|
3
|
-
*
|
|
4
|
-
* #329 fixed two edges running the *same* way between one pair of nodes
|
|
5
|
-
* (`A -->|One| B` twice) by giving every edge past the first its own offset
|
|
6
|
-
* lane. That fix keyed its grouping on the **ordered** pair
|
|
7
|
-
* `(source, target)`, so `Browser --> Server` and `Server --> Browser`
|
|
8
|
-
* landed in two different groups and neither was ever tagged as a sibling
|
|
9
|
-
* of the other. Both then routed through the same horizontal channel and
|
|
10
|
-
* reproduced #329's original defect byte for byte:
|
|
11
|
-
*
|
|
12
|
-
* ┌─────────┐ ┌────────┐
|
|
13
|
-
* │ Browser ├rrequest─►┤ Server │ <- "request"/"response" composited
|
|
14
|
-
* └─────────┘ └────────┘ and one arrowhead dropped
|
|
15
|
-
*
|
|
16
|
-
* The fix (edge-routing.ts): `parallelGroupKey` orders the two node names
|
|
17
|
-
* so a reciprocal pair shares one group — but only when the two nodes are
|
|
18
|
-
* laid out side by side on the same grid row (`sharesHorizontalChannel`),
|
|
19
|
-
* which is the layout where they actually collide. A *vertically* stacked
|
|
20
|
-
* reciprocal pair is deliberately left in separate groups: draw-arrows.ts's
|
|
21
|
-
* `drawTextOnLine` already pulls each of those labels into its own half of
|
|
22
|
-
* the shared vertical segment (#530), and that mechanism only works on a
|
|
23
|
-
* vertical segment, so routing one of the two through a lane would take it
|
|
24
|
-
* out of play. `determinePath` additionally swaps in the *alternative*
|
|
25
|
-
* start/end direction pair for a lane edge whose preferred pair is
|
|
26
|
-
* degenerate (same face on both ends, e.g. Down/Down for a backwards LR
|
|
27
|
-
* edge), a shape `buildParallelLanePath`'s offset math cannot express.
|
|
28
|
-
*/
|
|
29
|
-
import { describe, it, expect } from 'vitest'
|
|
30
|
-
import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
|
|
31
|
-
import { convertToAsciiGraph } from '../../packages/ascii-renderer/src/converter.ts'
|
|
32
|
-
import { createMapping } from '../../packages/ascii-renderer/src/grid.ts'
|
|
33
|
-
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
34
|
-
import { pathCells } from '../../packages/ascii-renderer/src/grid-occupancy.ts'
|
|
35
|
-
import type {
|
|
36
|
-
AsciiConfig,
|
|
37
|
-
AsciiEdge,
|
|
38
|
-
AsciiGraph,
|
|
39
|
-
} from '../../packages/ascii-renderer/src/types.ts'
|
|
40
|
-
|
|
41
|
-
const RIGHT_ARROW = '►'
|
|
42
|
-
const LEFT_ARROW = '◄'
|
|
43
|
-
const UP_ARROW = '▲'
|
|
44
|
-
const DOWN_ARROW = '▼'
|
|
45
|
-
|
|
46
|
-
function render(source: string): string {
|
|
47
|
-
return renderMermaidASCII(source, { colorMode: 'none' })
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function buildGraph(source: string, graphDirection: 'LR' | 'TD'): AsciiGraph {
|
|
51
|
-
const parsed = parseMermaid(source)
|
|
52
|
-
const config: AsciiConfig = {
|
|
53
|
-
useAscii: false,
|
|
54
|
-
paddingX: 5,
|
|
55
|
-
paddingY: 5,
|
|
56
|
-
boxBorderPadding: 1,
|
|
57
|
-
graphDirection,
|
|
58
|
-
}
|
|
59
|
-
const graph = convertToAsciiGraph(parsed, config)
|
|
60
|
-
createMapping(graph)
|
|
61
|
-
return graph
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function edgeBetween(
|
|
65
|
-
graph: AsciiGraph,
|
|
66
|
-
fromName: string,
|
|
67
|
-
toName: string,
|
|
68
|
-
): AsciiEdge {
|
|
69
|
-
const edge = graph.edges.find(
|
|
70
|
-
(e) => e.from.name === fromName && e.to.name === toName,
|
|
71
|
-
)
|
|
72
|
-
if (!edge) throw new Error(`no ${fromName} --> ${toName} edge in graph`)
|
|
73
|
-
return edge
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function cellKey(c: { x: number; y: number }): string {
|
|
77
|
-
return `${c.x},${c.y}`
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Total arrowheads drawn, in any of the four glyph directions. */
|
|
81
|
-
function countArrowheads(out: string): number {
|
|
82
|
-
return [...out].filter((ch) =>
|
|
83
|
-
[RIGHT_ARROW, LEFT_ARROW, UP_ARROW, DOWN_ARROW].includes(ch),
|
|
84
|
-
).length
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const BOTH_LABELLED = `graph LR
|
|
88
|
-
Browser -- request --> Server
|
|
89
|
-
Server -- response --> Browser`
|
|
90
|
-
|
|
91
|
-
describe('opposite-direction edges between one node pair (#629)', () => {
|
|
92
|
-
it('the exact issue repro: both labels render intact and uncorrupted', () => {
|
|
93
|
-
const out = render(BOTH_LABELLED)
|
|
94
|
-
|
|
95
|
-
expect(out).toContain('request')
|
|
96
|
-
expect(out).toContain('response')
|
|
97
|
-
// The reported corruption: "response"'s leading "r" landing on the cell
|
|
98
|
-
// before "request"'s own, so neither word is recoverable.
|
|
99
|
-
expect(out).not.toContain('rrequest')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('the exact issue repro: both arrowheads survive', () => {
|
|
103
|
-
const out = render(BOTH_LABELLED)
|
|
104
|
-
|
|
105
|
-
// Two declared edges, so two arrowheads — the issue's defect #2 was
|
|
106
|
-
// that only one was drawn, making the pair read as one-way.
|
|
107
|
-
expect(countArrowheads(out)).toBe(2)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('the exact issue repro: the two edges occupy different rows', () => {
|
|
111
|
-
const rows = render(BOTH_LABELLED).split('\n')
|
|
112
|
-
const requestRow = rows.findIndex((r) => r.includes('request'))
|
|
113
|
-
const responseRow = rows.findIndex((r) => r.includes('response'))
|
|
114
|
-
|
|
115
|
-
expect(requestRow).toBeGreaterThan(-1)
|
|
116
|
-
expect(responseRow).toBeGreaterThan(-1)
|
|
117
|
-
// The issue's defect #3: one shared row, so nothing in the output
|
|
118
|
-
// indicated the source had declared two edges.
|
|
119
|
-
expect(requestRow).not.toBe(responseRow)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('only the forward edge labelled: label intact, edges kept apart', () => {
|
|
123
|
-
const source = `graph LR
|
|
124
|
-
Browser -- request --> Server
|
|
125
|
-
Server --> Browser`
|
|
126
|
-
const out = render(source)
|
|
127
|
-
|
|
128
|
-
expect(out).toContain('request')
|
|
129
|
-
expect(countArrowheads(out)).toBe(2)
|
|
130
|
-
// Before the fix these two rendered as a single `◄request►` line, which
|
|
131
|
-
// reads as one bidirectional edge labelled "request" rather than a
|
|
132
|
-
// labelled edge plus an unlabelled one — so assert they were actually
|
|
133
|
-
// separated, not just that two arrowheads survived.
|
|
134
|
-
const graph = buildGraph(source, 'LR')
|
|
135
|
-
expect(edgeBetween(graph, 'Browser', 'Server').parallelLane?.index).toBe(0)
|
|
136
|
-
expect(edgeBetween(graph, 'Server', 'Browser').parallelLane?.index).toBe(1)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('only the reverse edge labelled: label intact, both arrowheads drawn', () => {
|
|
140
|
-
const out = render(`graph LR
|
|
141
|
-
Browser --> Server
|
|
142
|
-
Server -- response --> Browser`)
|
|
143
|
-
|
|
144
|
-
expect(out).toContain('response')
|
|
145
|
-
// This orientation dropped the reverse edge's own `◄` before the fix.
|
|
146
|
-
expect(countArrowheads(out)).toBe(2)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('a chain of two opposed pairs keeps all four labels', () => {
|
|
150
|
-
const out = render(`graph LR
|
|
151
|
-
A -- a1 --> B
|
|
152
|
-
B -- b1 --> A
|
|
153
|
-
B -- b2 --> C
|
|
154
|
-
C -- c1 --> B`)
|
|
155
|
-
|
|
156
|
-
// b1 and c1 vanished entirely before the fix — each was drawn onto the
|
|
157
|
-
// cells its forward partner had already claimed.
|
|
158
|
-
for (const label of ['a1', 'b1', 'b2', 'c1']) {
|
|
159
|
-
expect(out).toContain(label)
|
|
160
|
-
}
|
|
161
|
-
expect(countArrowheads(out)).toBe(4)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('routes the reciprocal pair through distinct, non-identical paths', () => {
|
|
165
|
-
const graph = buildGraph(BOTH_LABELLED, 'LR')
|
|
166
|
-
const forward = edgeBetween(graph, 'Browser', 'Server')
|
|
167
|
-
const reverse = edgeBetween(graph, 'Server', 'Browser')
|
|
168
|
-
|
|
169
|
-
const forwardCells = new Set(forward.path.flatMap((c) => cellKey(c)))
|
|
170
|
-
expect(reverse.path.map(cellKey).some((k) => !forwardCells.has(k))).toBe(
|
|
171
|
-
true,
|
|
172
|
-
)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('gives the reciprocal pair non-overlapping label lines', () => {
|
|
176
|
-
const graph = buildGraph(BOTH_LABELLED, 'LR')
|
|
177
|
-
const forward = edgeBetween(graph, 'Browser', 'Server')
|
|
178
|
-
const reverse = edgeBetween(graph, 'Server', 'Browser')
|
|
179
|
-
|
|
180
|
-
const forwardCells = new Set(pathCells(forward.labelLine).map(cellKey))
|
|
181
|
-
const overlap = pathCells(reverse.labelLine)
|
|
182
|
-
.map(cellKey)
|
|
183
|
-
.filter((k) => forwardCells.has(k))
|
|
184
|
-
expect(overlap).toEqual([])
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it('keeps the reverse edge’s lane between its own two nodes', () => {
|
|
188
|
-
const graph = buildGraph(BOTH_LABELLED, 'LR')
|
|
189
|
-
const reverse = edgeBetween(graph, 'Server', 'Browser')
|
|
190
|
-
const from = reverse.from.gridCoord!
|
|
191
|
-
const to = reverse.to.gridCoord!
|
|
192
|
-
// A node's reserved block spans 3 grid columns from its own x.
|
|
193
|
-
const minX = Math.min(from.x, to.x)
|
|
194
|
-
const maxX = Math.max(from.x, to.x) + 2
|
|
195
|
-
|
|
196
|
-
// determineStartAndEndDir hands a backwards LR edge a degenerate
|
|
197
|
-
// Down/Down pair — leave and re-enter on the same (bottom) face —
|
|
198
|
-
// which buildParallelLanePath's offset math reads as a *vertical*
|
|
199
|
-
// departure and so offsets by column. That pushes the lane, and its
|
|
200
|
-
// label with it, out past the right-hand node instead of down into the
|
|
201
|
-
// channel below the pair, and drags the horizontal jogs across both
|
|
202
|
-
// nodes' own bottom border rows on the way. determinePath swaps in the
|
|
203
|
-
// alternative straight-through pair to avoid exactly this.
|
|
204
|
-
for (const point of reverse.labelLine) {
|
|
205
|
-
expect(point.x).toBeGreaterThanOrEqual(minX)
|
|
206
|
-
expect(point.x).toBeLessThanOrEqual(maxX)
|
|
207
|
-
}
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('tags exactly one of the two as a lane edge', () => {
|
|
211
|
-
const graph = buildGraph(BOTH_LABELLED, 'LR')
|
|
212
|
-
const forward = edgeBetween(graph, 'Browser', 'Server')
|
|
213
|
-
const reverse = edgeBetween(graph, 'Server', 'Browser')
|
|
214
|
-
|
|
215
|
-
// Same group, so index 0 keeps the ordinary center path and index 1
|
|
216
|
-
// gets the offset lane.
|
|
217
|
-
expect(forward.parallelLane?.index).toBe(0)
|
|
218
|
-
expect(reverse.parallelLane?.index).toBe(1)
|
|
219
|
-
expect(reverse.parallelLane?.total).toBe(2)
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
it('never folds a lane-assigned edge into a fan-in/fan-out bundle', () => {
|
|
223
|
-
// Longest-path layering ranks a reciprocal pair on different levels (as
|
|
224
|
-
// dagre does), so B and C can no longer sit side by side with a lane
|
|
225
|
-
// group between them. The invariant still matters where a pair and a
|
|
226
|
-
// fan-in bundle (B, C --> D) share the same edges' neighbourhood: no
|
|
227
|
-
// edge of the pair may end up inside a bundle, and none may carry a lane
|
|
228
|
-
// into one.
|
|
229
|
-
const graph = buildGraph(
|
|
230
|
-
`graph TD
|
|
231
|
-
A --> B
|
|
232
|
-
A --> C
|
|
233
|
-
B --> D
|
|
234
|
-
C --> D
|
|
235
|
-
B -- x --> C
|
|
236
|
-
C -- y --> B`,
|
|
237
|
-
'TD',
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
const bundled = (graph.bundles ?? []).flatMap((b) => b.edges)
|
|
241
|
-
expect(bundled.length).toBeGreaterThan(0)
|
|
242
|
-
expect(bundled.filter((e) => e.parallelLane)).toEqual([])
|
|
243
|
-
const pair = new Set([
|
|
244
|
-
edgeBetween(graph, 'B', 'C'),
|
|
245
|
-
edgeBetween(graph, 'C', 'B'),
|
|
246
|
-
])
|
|
247
|
-
expect(bundled.filter((e) => pair.has(e))).toEqual([])
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
it('leaves a vertically stacked reciprocal pair on the #530 mechanism', () => {
|
|
251
|
-
const graph = buildGraph(
|
|
252
|
-
`graph TD
|
|
253
|
-
A -- down --> B
|
|
254
|
-
B -- up --> A`,
|
|
255
|
-
'TD',
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
// Stacked in one column, not side by side: drawTextOnLine already
|
|
259
|
-
// separates these two labels, so no lane is assigned (a lane here would
|
|
260
|
-
// route one of them out of the shared vertical segment and disable it).
|
|
261
|
-
expect(edgeBetween(graph, 'A', 'B').parallelLane).toBeUndefined()
|
|
262
|
-
expect(edgeBetween(graph, 'B', 'A').parallelLane).toBeUndefined()
|
|
263
|
-
|
|
264
|
-
const out = render(`graph TD
|
|
265
|
-
A -- down --> B
|
|
266
|
-
B -- up --> A`)
|
|
267
|
-
expect(out).toContain('down')
|
|
268
|
-
expect(out).toContain('up')
|
|
269
|
-
expect(out).toContain(UP_ARROW)
|
|
270
|
-
expect(out).toContain(DOWN_ARROW)
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
it('leaves a single `<-->` bidirectional edge merged onto one line', () => {
|
|
274
|
-
const out = render(`graph LR
|
|
275
|
-
A <--> B`)
|
|
276
|
-
|
|
277
|
-
// One declared edge, drawn as one line with a head at each end — this
|
|
278
|
-
// is a different code path from the per-edge lane assignment and must
|
|
279
|
-
// stay untouched.
|
|
280
|
-
expect(out).toContain(`${LEFT_ARROW}────${RIGHT_ARROW}`)
|
|
281
|
-
})
|
|
282
|
-
})
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for #1285 — uneven subgraph frame geometry in a stacked
|
|
3
|
-
* layout. Two of its three complaints are fixed:
|
|
4
|
-
*
|
|
5
|
-
* - a back edge running beside a frame keeps a clear column from the wall
|
|
6
|
-
* (it used to sit directly against it: `││`, then cross it with `┼┘`);
|
|
7
|
-
* - a title widened for an entering stroke keeps a clear column before the
|
|
8
|
-
* right wall (`Layer Three │`, not `Layer Three│`).
|
|
9
|
-
*
|
|
10
|
-
* The third (frames not left-aligned) is deliberate: real mermaid centres
|
|
11
|
-
* frames, so it is left alone.
|
|
12
|
-
*/
|
|
13
|
-
import { describe, it, expect } from 'vitest'
|
|
14
|
-
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
15
|
-
|
|
16
|
-
const SOURCE = `graph TD
|
|
17
|
-
subgraph L1[Layer One]
|
|
18
|
-
A
|
|
19
|
-
end
|
|
20
|
-
subgraph L2[Layer Two]
|
|
21
|
-
C
|
|
22
|
-
end
|
|
23
|
-
subgraph L3[Layer Three]
|
|
24
|
-
E
|
|
25
|
-
end
|
|
26
|
-
A --> C
|
|
27
|
-
C --> E
|
|
28
|
-
E --> A`
|
|
29
|
-
|
|
30
|
-
describe('ASCII: stacked frames keep clear columns beside walls and titles (#1285)', () => {
|
|
31
|
-
const lines = renderMermaidASCII(SOURCE).split('\n')
|
|
32
|
-
|
|
33
|
-
it('never draws the back edge directly against a frame wall', () => {
|
|
34
|
-
expect(lines.some((l) => l.includes('││'))).toBe(false)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('keeps a clear column when the edge runs beside a frame it passes', () => {
|
|
38
|
-
const out = renderMermaidASCII(`graph TD
|
|
39
|
-
subgraph S1[One]
|
|
40
|
-
a
|
|
41
|
-
end
|
|
42
|
-
subgraph S2[Two]
|
|
43
|
-
b
|
|
44
|
-
end
|
|
45
|
-
subgraph S3[Three]
|
|
46
|
-
c
|
|
47
|
-
end
|
|
48
|
-
a --> b
|
|
49
|
-
b --> c
|
|
50
|
-
a --> x
|
|
51
|
-
a --> c`)
|
|
52
|
-
const trimmed = out
|
|
53
|
-
.split('\n')
|
|
54
|
-
.map((l) => l.trimEnd())
|
|
55
|
-
.join('\n')
|
|
56
|
-
expect(trimmed).toBe(
|
|
57
|
-
[
|
|
58
|
-
'┌───────┐',
|
|
59
|
-
'│ One │',
|
|
60
|
-
'│ │',
|
|
61
|
-
'│ │',
|
|
62
|
-
'│ ┌───┐ │',
|
|
63
|
-
'│ │ │ │',
|
|
64
|
-
'│ │ a ├─┼───────────┐',
|
|
65
|
-
'│ │ │ │ │',
|
|
66
|
-
'│ └─┬─┘ │ │',
|
|
67
|
-
'│ │ │ │',
|
|
68
|
-
'└───┼───┘ │',
|
|
69
|
-
' │ │',
|
|
70
|
-
' │ │',
|
|
71
|
-
' ├───────┐ │',
|
|
72
|
-
'┌───┼─────┐ │ │',
|
|
73
|
-
'│ │ Two │ │ │',
|
|
74
|
-
'│ │ │ │ │',
|
|
75
|
-
'│ ▼ │ │ ▼',
|
|
76
|
-
'│ ┌───┐ │ │ ┌───┐',
|
|
77
|
-
'│ │ │ │ │ │ │',
|
|
78
|
-
'│ │ b │ │ │ │ x │',
|
|
79
|
-
'│ │ │ │ │ │ │',
|
|
80
|
-
'│ └─┬─┘ │ │ └───┘',
|
|
81
|
-
'│ │ │ │',
|
|
82
|
-
'└───┼─────┘ │',
|
|
83
|
-
' │ │',
|
|
84
|
-
' │ │',
|
|
85
|
-
' ├───────┘',
|
|
86
|
-
'┌───┼───────┐',
|
|
87
|
-
'│ │ Three │',
|
|
88
|
-
'│ │ │',
|
|
89
|
-
'│ ▼ │',
|
|
90
|
-
'│ ┌───┐ │',
|
|
91
|
-
'│ │ │ │',
|
|
92
|
-
'│ │ c │ │',
|
|
93
|
-
'│ │ │ │',
|
|
94
|
-
'│ └───┘ │',
|
|
95
|
-
'│ │',
|
|
96
|
-
'└───────────┘',
|
|
97
|
-
].join('\n'),
|
|
98
|
-
)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('keeps a clear column between a widened title and the right wall', () => {
|
|
102
|
-
for (const title of ['Layer Two', 'Layer Three']) {
|
|
103
|
-
const line = lines.find((l) => l.includes(title))!
|
|
104
|
-
expect(line).toMatch(new RegExp(`${title} +│`))
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
})
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for #90 — "ASCII: edge-less node inside a subgraph merges
|
|
3
|
-
* that subgraph's frame with a neighboring one."
|
|
4
|
-
*
|
|
5
|
-
* Root cause: root-node placement in `createMapping` (src/ascii/grid.ts) was
|
|
6
|
-
* entirely subgraph-agnostic. An edge-less node (treated as an initial "root"
|
|
7
|
-
* since it has no incoming edges) could land in the same row/column band as
|
|
8
|
-
* an unrelated sibling subgraph's real root, purely because both were
|
|
9
|
-
* "roots" and roots were laid out along one shared level regardless of which
|
|
10
|
-
* subgraph they belonged to. That made one subgraph's bounding box (computed
|
|
11
|
-
* as min/max over its members' actual positions) balloon out to enclose a
|
|
12
|
-
* completely unrelated sibling subgraph, corrupting both frames' borders and
|
|
13
|
-
* titles when drawn.
|
|
14
|
-
*
|
|
15
|
-
* The fix defers placement of root nodes that belong to a subgraph with
|
|
16
|
-
* *other* members that are unreachable from that root, anchoring them next
|
|
17
|
-
* to their already-placed subgraph siblings instead of the shared root
|
|
18
|
-
* level. See `src/ascii/grid.ts`'s `createMapping` (deferredRoots handling).
|
|
19
|
-
*/
|
|
20
|
-
import { describe, it, expect } from 'vitest'
|
|
21
|
-
import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
|
|
22
|
-
import { convertToAsciiGraph } from '../../packages/ascii-renderer/src/converter.ts'
|
|
23
|
-
import { createMapping } from '../../packages/ascii-renderer/src/grid.ts'
|
|
24
|
-
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
25
|
-
import type {
|
|
26
|
-
AsciiConfig,
|
|
27
|
-
AsciiGraph,
|
|
28
|
-
AsciiSubgraph,
|
|
29
|
-
} from '../../packages/ascii-renderer/src/types.ts'
|
|
30
|
-
|
|
31
|
-
function layoutFlowchart(
|
|
32
|
-
source: string,
|
|
33
|
-
direction: 'TD' | 'LR' = 'TD',
|
|
34
|
-
): AsciiGraph {
|
|
35
|
-
const parsed = parseMermaid(source)
|
|
36
|
-
const config: AsciiConfig = {
|
|
37
|
-
useAscii: false,
|
|
38
|
-
paddingX: 5,
|
|
39
|
-
paddingY: 5,
|
|
40
|
-
boxBorderPadding: 1,
|
|
41
|
-
graphDirection: direction,
|
|
42
|
-
}
|
|
43
|
-
const graph = convertToAsciiGraph(parsed, config)
|
|
44
|
-
createMapping(graph)
|
|
45
|
-
return graph
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function boxesOverlap(a: AsciiSubgraph, b: AsciiSubgraph): boolean {
|
|
49
|
-
return !(
|
|
50
|
-
a.maxX <= b.minX ||
|
|
51
|
-
b.maxX <= a.minX ||
|
|
52
|
-
a.maxY <= b.minY ||
|
|
53
|
-
b.maxY <= a.minY
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function findSubgraph(graph: AsciiGraph, name: string): AsciiSubgraph {
|
|
58
|
-
const sg = graph.subgraphs.find((s) => s.name === name)
|
|
59
|
-
if (!sg) throw new Error(`subgraph "${name}" not found`)
|
|
60
|
-
return sg
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
describe('ASCII subgraph bounding boxes stay disjoint (#90)', () => {
|
|
64
|
-
const issueRepro = `flowchart TB
|
|
65
|
-
subgraph a["Frontend tier"]
|
|
66
|
-
a1["load balancer"]
|
|
67
|
-
end
|
|
68
|
-
subgraph b["Application tier"]
|
|
69
|
-
b1["worker pool"]
|
|
70
|
-
b2["api server"]
|
|
71
|
-
end
|
|
72
|
-
a1 --> b1`
|
|
73
|
-
|
|
74
|
-
it('keeps an edge-less subgraph member from merging with a sibling subgraph (TD, exact issue repro)', () => {
|
|
75
|
-
const graph = layoutFlowchart(issueRepro, 'TD')
|
|
76
|
-
expect(graph.subgraphs.length).toBe(2)
|
|
77
|
-
|
|
78
|
-
const frontend = findSubgraph(graph, 'Frontend tier')
|
|
79
|
-
const application = findSubgraph(graph, 'Application tier')
|
|
80
|
-
expect(boxesOverlap(frontend, application)).toBe(false)
|
|
81
|
-
|
|
82
|
-
const output = renderMermaidASCII(issueRepro)
|
|
83
|
-
expect(output).toContain('Frontend tier')
|
|
84
|
-
expect(output).toContain('Application tier')
|
|
85
|
-
// Guard against the exact corrupted-title symptom from the issue, where
|
|
86
|
-
// the two titles interleaved character-by-character into
|
|
87
|
-
// "Frontendptieration tier".
|
|
88
|
-
expect(output).not.toContain('Frontendptieration')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('keeps an edge-less subgraph member from merging with a sibling subgraph (LR)', () => {
|
|
92
|
-
const lrSource = issueRepro.replace('flowchart TB', 'flowchart LR')
|
|
93
|
-
const graph = layoutFlowchart(lrSource, 'LR')
|
|
94
|
-
|
|
95
|
-
const frontend = findSubgraph(graph, 'Frontend tier')
|
|
96
|
-
const application = findSubgraph(graph, 'Application tier')
|
|
97
|
-
expect(boxesOverlap(frontend, application)).toBe(false)
|
|
98
|
-
|
|
99
|
-
const output = renderMermaidASCII(lrSource)
|
|
100
|
-
expect(output).toContain('Frontend tier')
|
|
101
|
-
expect(output).toContain('Application tier')
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('keeps multiple subgraphs — each with its own root plus an edge-less sibling — mutually disjoint', () => {
|
|
105
|
-
const source = `flowchart TD
|
|
106
|
-
subgraph a["Alpha"]
|
|
107
|
-
a1["root"]
|
|
108
|
-
end
|
|
109
|
-
subgraph b["Beta"]
|
|
110
|
-
b1["worker"]
|
|
111
|
-
b2["idle"]
|
|
112
|
-
end
|
|
113
|
-
subgraph c["Gamma"]
|
|
114
|
-
c1["worker"]
|
|
115
|
-
c2["idle"]
|
|
116
|
-
end
|
|
117
|
-
a1 --> b1
|
|
118
|
-
b1 --> c1`
|
|
119
|
-
|
|
120
|
-
const graph = layoutFlowchart(source, 'TD')
|
|
121
|
-
expect(graph.subgraphs.length).toBe(3)
|
|
122
|
-
|
|
123
|
-
for (let i = 0; i < graph.subgraphs.length; i++) {
|
|
124
|
-
for (let j = i + 1; j < graph.subgraphs.length; j++) {
|
|
125
|
-
const sgI = graph.subgraphs[i]!
|
|
126
|
-
const sgJ = graph.subgraphs[j]!
|
|
127
|
-
expect(
|
|
128
|
-
boxesOverlap(sgI, sgJ),
|
|
129
|
-
`subgraph "${sgI.name}" overlaps "${sgJ.name}"`,
|
|
130
|
-
).toBe(false)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const output = renderMermaidASCII(source)
|
|
135
|
-
expect(output).toContain('Alpha')
|
|
136
|
-
expect(output).toContain('Beta')
|
|
137
|
-
expect(output).toContain('Gamma')
|
|
138
|
-
})
|
|
139
|
-
})
|