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,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Direct coverage for edge-bundling.ts's routeBundledEdges /
|
|
3
|
-
* calculateJunctionPoint — previously untested (PR #178 review): no test
|
|
4
|
-
* imported either function, so the fan-in/fan-out routing math (junction
|
|
5
|
-
* placement, per-edge pathToJunction, shared trunk path) was only ever
|
|
6
|
-
* exercised indirectly through draw-bundles.ts's rendering tests.
|
|
7
|
-
*
|
|
8
|
-
* Also covers the LR branches directly. analyzeEdgeBundles only ever
|
|
9
|
-
* creates bundles for TD graphs (LR routing merges naturally at corners),
|
|
10
|
-
* so routeBundledEdges' LR ternaries are unreachable through any real
|
|
11
|
-
* Mermaid diagram today — but the function itself handles both directions,
|
|
12
|
-
* and after the PR #178 fix (routeEdge/tryDirectPath try both L
|
|
13
|
-
* orientations instead of trusting a possibly-wrong `dir`), the LR path is
|
|
14
|
-
* exercised here directly and pinned, rather than left as untested dead
|
|
15
|
-
* code with the same direction-confusion risk the TD side had.
|
|
16
|
-
*/
|
|
17
|
-
import { describe, it, expect } from 'vitest'
|
|
18
|
-
import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
|
|
19
|
-
import { convertToAsciiGraph } from '../../packages/ascii-renderer/src/converter.ts'
|
|
20
|
-
import { createMapping } from '../../packages/ascii-renderer/src/grid.ts'
|
|
21
|
-
import {
|
|
22
|
-
calculateJunctionPoint,
|
|
23
|
-
routeBundledEdges,
|
|
24
|
-
} from '../../packages/ascii-renderer/src/edge-bundling.ts'
|
|
25
|
-
import { Middle } from '../../packages/ascii-renderer/src/types.ts'
|
|
26
|
-
import type {
|
|
27
|
-
AsciiConfig,
|
|
28
|
-
AsciiGraph,
|
|
29
|
-
EdgeBundle,
|
|
30
|
-
} from '../../packages/ascii-renderer/src/types.ts'
|
|
31
|
-
|
|
32
|
-
function buildGraph(src: string, graphDirection: 'TD' | 'LR'): AsciiGraph {
|
|
33
|
-
const config: AsciiConfig = {
|
|
34
|
-
useAscii: false,
|
|
35
|
-
paddingX: 5,
|
|
36
|
-
paddingY: 5,
|
|
37
|
-
boxBorderPadding: 1,
|
|
38
|
-
graphDirection,
|
|
39
|
-
}
|
|
40
|
-
const graph = convertToAsciiGraph(parseMermaid(src), config)
|
|
41
|
-
createMapping(graph)
|
|
42
|
-
return graph
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const FAN_OUT_TD = `flowchart TD
|
|
46
|
-
C[Source] --> A[One]
|
|
47
|
-
C --> B[Two]`
|
|
48
|
-
|
|
49
|
-
const FAN_IN_TD = `flowchart TD
|
|
50
|
-
A[One] --> C[Target]
|
|
51
|
-
B[Two] --> C`
|
|
52
|
-
|
|
53
|
-
describe('routeBundledEdges (TD, via the real layout pipeline)', () => {
|
|
54
|
-
it('pins the fan-out junction and per-target paths', () => {
|
|
55
|
-
const graph = buildGraph(FAN_OUT_TD, 'TD')
|
|
56
|
-
expect(graph.bundles).toHaveLength(1)
|
|
57
|
-
const bundle = graph.bundles[0]!
|
|
58
|
-
expect(bundle.type).toBe('fan-out')
|
|
59
|
-
|
|
60
|
-
const C = graph.nodes.find((n) => n.name === 'C')!
|
|
61
|
-
expect(bundle.junctionPoint).toEqual({
|
|
62
|
-
x: C.gridCoord!.x + 1,
|
|
63
|
-
y: C.gridCoord!.y + 3,
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
// Every edge in the bundle shares the same trunk directions.
|
|
67
|
-
for (const edge of bundle.edges) {
|
|
68
|
-
expect(edge.startDir).toEqual({ x: 1, y: 2 }) // Down
|
|
69
|
-
expect(edge.endDir).toEqual({ x: 1, y: 0 }) // Up
|
|
70
|
-
expect(edge.pathToJunction).toBeDefined()
|
|
71
|
-
expect(edge.pathToJunction!.at(0)).toEqual(bundle.junctionPoint)
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('pins the fan-in junction and per-source paths', () => {
|
|
76
|
-
const graph = buildGraph(FAN_IN_TD, 'TD')
|
|
77
|
-
expect(graph.bundles).toHaveLength(1)
|
|
78
|
-
const bundle = graph.bundles[0]!
|
|
79
|
-
expect(bundle.type).toBe('fan-in')
|
|
80
|
-
|
|
81
|
-
const C = graph.nodes.find((n) => n.name === 'C')!
|
|
82
|
-
expect(bundle.junctionPoint).toEqual({
|
|
83
|
-
x: C.gridCoord!.x + 1,
|
|
84
|
-
y: C.gridCoord!.y - 1,
|
|
85
|
-
})
|
|
86
|
-
expect(bundle.sharedPath.at(-1)).toEqual({
|
|
87
|
-
x: C.gridCoord!.x + 1,
|
|
88
|
-
y: C.gridCoord!.y,
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
for (const edge of bundle.edges) {
|
|
92
|
-
expect(edge.startDir).toEqual({ x: 1, y: 2 }) // Down
|
|
93
|
-
expect(edge.endDir).toEqual({ x: 1, y: 0 }) // Up
|
|
94
|
-
expect(edge.pathToJunction!.at(-1)).toEqual(bundle.junctionPoint)
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('routes a 3-way fan-out with an unobstructed direct path to each target (no zigzag)', () => {
|
|
99
|
-
// Regression check for the PR #178 arrival-vs-departure direction bug:
|
|
100
|
-
// before the fix, the junction->target segment's fast path was dead
|
|
101
|
-
// for every fan-out edge except the (degenerate, always-aligned) first
|
|
102
|
-
// one, so off-center targets could get an A*-search path instead of
|
|
103
|
-
// the direct 2-segment L. Each pathToJunction here must be exactly 2
|
|
104
|
-
// segments (junction, corner, target) or, when already axis-aligned, 1.
|
|
105
|
-
const graph = buildGraph(
|
|
106
|
-
`flowchart TD
|
|
107
|
-
C[Source] --> A[One]
|
|
108
|
-
C --> B[A much longer target]
|
|
109
|
-
C --> D[Three]`,
|
|
110
|
-
'TD',
|
|
111
|
-
)
|
|
112
|
-
const bundle = graph.bundles[0]!
|
|
113
|
-
for (const edge of bundle.edges) {
|
|
114
|
-
const path = edge.pathToJunction!
|
|
115
|
-
// A direct L is at most 3 points (or 2 if already axis-aligned) —
|
|
116
|
-
// never a longer A*-search detour.
|
|
117
|
-
expect(path.length).toBeLessThanOrEqual(3)
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
describe('routeBundledEdges (LR, called directly)', () => {
|
|
123
|
-
// analyzeEdgeBundles never creates a bundle for an LR graph, so these
|
|
124
|
-
// build one by hand from a real LR-laid-out graph's nodes/edges and call
|
|
125
|
-
// routeBundledEdges directly — exercising the function's LR branches with
|
|
126
|
-
// pinned coordinates instead of leaving them untested.
|
|
127
|
-
function makeBundle(
|
|
128
|
-
type: 'fan-in' | 'fan-out',
|
|
129
|
-
graph: AsciiGraph,
|
|
130
|
-
sharedName: string,
|
|
131
|
-
): EdgeBundle {
|
|
132
|
-
const shared = graph.nodes.find((n) => n.name === sharedName)!
|
|
133
|
-
const edges = graph.edges.filter((e) =>
|
|
134
|
-
type === 'fan-out' ? e.from === shared : e.to === shared,
|
|
135
|
-
)
|
|
136
|
-
return {
|
|
137
|
-
type,
|
|
138
|
-
edges,
|
|
139
|
-
sharedNode: shared,
|
|
140
|
-
otherNodes: edges.map((e) => (type === 'fan-out' ? e.to : e.from)),
|
|
141
|
-
junctionPoint: null,
|
|
142
|
-
sharedPath: [],
|
|
143
|
-
junctionDir: Middle,
|
|
144
|
-
sharedNodeDir: Middle,
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
it('pins fan-out junction/target coordinates for an LR graph', () => {
|
|
149
|
-
const graph = buildGraph(
|
|
150
|
-
`flowchart LR
|
|
151
|
-
C[Source] --> A[One]
|
|
152
|
-
C --> B[Two]`,
|
|
153
|
-
'LR',
|
|
154
|
-
)
|
|
155
|
-
const bundle = makeBundle('fan-out', graph, 'C')
|
|
156
|
-
const C = graph.nodes.find((n) => n.name === 'C')!
|
|
157
|
-
|
|
158
|
-
const junction = calculateJunctionPoint(graph, bundle)
|
|
159
|
-
expect(junction).toEqual({ x: C.gridCoord!.x + 3, y: C.gridCoord!.y + 1 })
|
|
160
|
-
|
|
161
|
-
routeBundledEdges(graph, bundle)
|
|
162
|
-
expect(bundle.junctionPoint).toEqual(junction)
|
|
163
|
-
expect(bundle.sharedPath.at(-1)).toEqual(junction)
|
|
164
|
-
|
|
165
|
-
for (const edge of bundle.edges) {
|
|
166
|
-
const target = edge.to
|
|
167
|
-
expect(edge.startDir).toEqual({ x: 2, y: 1 }) // Right
|
|
168
|
-
expect(edge.endDir).toEqual({ x: 0, y: 1 }) // Left
|
|
169
|
-
const path = edge.pathToJunction!
|
|
170
|
-
expect(path.at(0)).toEqual(junction)
|
|
171
|
-
expect(path.at(-1)).toEqual({
|
|
172
|
-
x: target.gridCoord!.x,
|
|
173
|
-
y: target.gridCoord!.y + 1,
|
|
174
|
-
})
|
|
175
|
-
// Direct L (or already-aligned straight line) — never an A* detour.
|
|
176
|
-
expect(path.length).toBeLessThanOrEqual(3)
|
|
177
|
-
}
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it('pins fan-in junction/source coordinates for an LR graph', () => {
|
|
181
|
-
const graph = buildGraph(
|
|
182
|
-
`flowchart LR
|
|
183
|
-
A[One] --> C[Target]
|
|
184
|
-
B[Two] --> C`,
|
|
185
|
-
'LR',
|
|
186
|
-
)
|
|
187
|
-
const bundle = makeBundle('fan-in', graph, 'C')
|
|
188
|
-
const C = graph.nodes.find((n) => n.name === 'C')!
|
|
189
|
-
|
|
190
|
-
const junction = calculateJunctionPoint(graph, bundle)
|
|
191
|
-
expect(junction).toEqual({ x: C.gridCoord!.x - 1, y: C.gridCoord!.y + 1 })
|
|
192
|
-
|
|
193
|
-
routeBundledEdges(graph, bundle)
|
|
194
|
-
expect(bundle.junctionPoint).toEqual(junction)
|
|
195
|
-
expect(bundle.sharedPath.at(0)).toEqual(junction)
|
|
196
|
-
expect(bundle.sharedPath.at(-1)).toEqual({
|
|
197
|
-
x: C.gridCoord!.x,
|
|
198
|
-
y: C.gridCoord!.y + 1,
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
for (const edge of bundle.edges) {
|
|
202
|
-
const source = edge.from
|
|
203
|
-
expect(edge.startDir).toEqual({ x: 2, y: 1 }) // Right
|
|
204
|
-
expect(edge.endDir).toEqual({ x: 0, y: 1 }) // Left
|
|
205
|
-
const path = edge.pathToJunction!
|
|
206
|
-
expect(path.at(-1)).toEqual(junction)
|
|
207
|
-
expect(path.at(0)).toEqual({
|
|
208
|
-
x: source.gridCoord!.x + 2,
|
|
209
|
-
y: source.gridCoord!.y + 1,
|
|
210
|
-
})
|
|
211
|
-
expect(path.length).toBeLessThanOrEqual(3)
|
|
212
|
-
}
|
|
213
|
-
})
|
|
214
|
-
})
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for a real-world bug: `graph.grid` (grid-occupancy.ts)
|
|
3
|
-
* only tracks *node* occupancy, so two edges with neither a shared source
|
|
4
|
-
* nor a shared target — the only cases `analyzeEdgeBundles` groups, and the
|
|
5
|
-
* only relationship A*'s own occupancy check knows about — could
|
|
6
|
-
* independently route through the same empty cell. When the two edges have
|
|
7
|
-
* different line styles (e.g. a solid branch and a dotted retry/back-edge,
|
|
8
|
-
* a very ordinary shape for any diagram with a retry loop), one silently
|
|
9
|
-
* overwrote the other's glyph in the final canvas: the rendered line looked
|
|
10
|
-
* like a single corrupted half-solid, half-dotted run, with no visual
|
|
11
|
-
* indication that two distinct connections were even there.
|
|
12
|
-
*
|
|
13
|
-
* Repro: a decision node with a "No" branch to a retry node, which then
|
|
14
|
-
* dotted-links back to the start — reported against the CI/CD Pipeline
|
|
15
|
-
* sample on the project's own samples page (a subgraph made it more likely,
|
|
16
|
-
* by leaving less free routing space, but the underlying bug has nothing to
|
|
17
|
-
* do with subgraphs — this repro has none).
|
|
18
|
-
*/
|
|
19
|
-
import { describe, it, expect } from 'vitest'
|
|
20
|
-
import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
|
|
21
|
-
import { convertToAsciiGraph } from '../../packages/ascii-renderer/src/converter.ts'
|
|
22
|
-
import { createMapping } from '../../packages/ascii-renderer/src/grid.ts'
|
|
23
|
-
import {
|
|
24
|
-
isOccupied,
|
|
25
|
-
pathCells,
|
|
26
|
-
createGrid,
|
|
27
|
-
cloneGrid,
|
|
28
|
-
placeBlock,
|
|
29
|
-
} from '../../packages/ascii-renderer/src/grid-occupancy.ts'
|
|
30
|
-
import {
|
|
31
|
-
createEdgeCellStyles,
|
|
32
|
-
findStyleConflict,
|
|
33
|
-
claimPathCells,
|
|
34
|
-
} from '../../packages/ascii-renderer/src/edge-cell-styles.ts'
|
|
35
|
-
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
36
|
-
import type {
|
|
37
|
-
AsciiConfig,
|
|
38
|
-
AsciiEdge,
|
|
39
|
-
AsciiGraph,
|
|
40
|
-
} from '../../packages/ascii-renderer/src/types.ts'
|
|
41
|
-
|
|
42
|
-
const RETRY_LOOP_SOURCE = `graph TD
|
|
43
|
-
A[Push Code] --> B{Tests Pass?}
|
|
44
|
-
B -->|Yes| C[Build Image]
|
|
45
|
-
B -->|No| D[Fix & Retry]
|
|
46
|
-
D -.-> A`
|
|
47
|
-
|
|
48
|
-
function findEdge(
|
|
49
|
-
edges: AsciiEdge[],
|
|
50
|
-
fromName: string,
|
|
51
|
-
toName: string,
|
|
52
|
-
): AsciiEdge {
|
|
53
|
-
const edge = edges.find(
|
|
54
|
-
(e) => e.from.name === fromName && e.to.name === toName,
|
|
55
|
-
)
|
|
56
|
-
if (!edge) {
|
|
57
|
-
throw new Error(`Expected an edge ${fromName} -> ${toName}`)
|
|
58
|
-
}
|
|
59
|
-
return edge
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Cells owned by a node's own reserved block are excluded: both edges
|
|
64
|
-
* necessarily touch D's border where they connect to it (one incoming, one
|
|
65
|
-
* outgoing), and that shared port cell is drawn by D's own box-drawing
|
|
66
|
-
* code, not either edge's line style — see edge-cell-styles.ts's module
|
|
67
|
-
* doc. Only *open, non-node* cells are where a real cross-style collision
|
|
68
|
-
* would silently overwrite a glyph.
|
|
69
|
-
*/
|
|
70
|
-
function openPathCells(graph: AsciiGraph, path: AsciiEdge['path']): string[] {
|
|
71
|
-
return pathCells(path)
|
|
72
|
-
.filter((c) => !isOccupied(graph.grid, c))
|
|
73
|
-
.map((c) => `${c.x},${c.y}`)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
describe('cross-style edge overlap', () => {
|
|
77
|
-
it('routes a solid branch edge and an unrelated dotted back-edge through disjoint open-space cells', () => {
|
|
78
|
-
const parsed = parseMermaid(RETRY_LOOP_SOURCE)
|
|
79
|
-
const config: AsciiConfig = {
|
|
80
|
-
useAscii: false,
|
|
81
|
-
paddingX: 5,
|
|
82
|
-
paddingY: 5,
|
|
83
|
-
boxBorderPadding: 1,
|
|
84
|
-
graphDirection: 'TD',
|
|
85
|
-
}
|
|
86
|
-
const graph = convertToAsciiGraph(parsed, config)
|
|
87
|
-
createMapping(graph)
|
|
88
|
-
|
|
89
|
-
const noBranch = findEdge(graph.edges, 'B', 'D')
|
|
90
|
-
const backEdge = findEdge(graph.edges, 'D', 'A')
|
|
91
|
-
|
|
92
|
-
expect(noBranch.style).toBe('solid')
|
|
93
|
-
expect(backEdge.style).toBe('dotted')
|
|
94
|
-
|
|
95
|
-
const noBranchCells = new Set(openPathCells(graph, noBranch.path))
|
|
96
|
-
const backEdgeCells = openPathCells(graph, backEdge.path)
|
|
97
|
-
|
|
98
|
-
// Neither edge shares a source or target with the other (B->D vs
|
|
99
|
-
// D->A), so the *only* thing that can legitimately keep them apart in
|
|
100
|
-
// open routing space is this disjointness check — same-style edges are
|
|
101
|
-
// allowed (and often expected) to share cells, but these two must not.
|
|
102
|
-
const overlap = backEdgeCells.filter((key) => noBranchCells.has(key))
|
|
103
|
-
expect(overlap).toEqual([])
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('renders the retry-loop repro without throwing', () => {
|
|
107
|
-
// The original report was against a subgraph-wrapped version of this
|
|
108
|
-
// same shape; asserting on the plain render (not just internal path
|
|
109
|
-
// data, above) guards the full pipeline end to end.
|
|
110
|
-
const out = renderMermaidASCII(RETRY_LOOP_SOURCE)
|
|
111
|
-
for (const label of ['Push Code', 'Tests Pass?', 'Build Image', 'Fix']) {
|
|
112
|
-
expect(out).toContain(label)
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Regression for the crash discovered while fixing the overlap above: an
|
|
118
|
-
* early version reserved edge-path cells with a naive
|
|
119
|
-
* "x += sign(dx); y += sign(dy)" walk, which only reaches a segment's
|
|
120
|
-
* endpoint when |dx| === |dy|. `determinePath`'s Case-4 direct fallback
|
|
121
|
-
* can produce a segment with any dx/dy ratio, and this plain 4-edge
|
|
122
|
-
* fan-out is enough to trigger it — the walk never terminated and ran
|
|
123
|
-
* until the `Set` backing the occupancy grid threw "maximum size
|
|
124
|
-
* exceeded". See grid-occupancy.test.ts's `pathCells` tests for the
|
|
125
|
-
* unit-level version of this.
|
|
126
|
-
*/
|
|
127
|
-
it('does not hang or crash on a plain fan-out that exercises the direct-fallback path', () => {
|
|
128
|
-
const start = Date.now()
|
|
129
|
-
const out = renderMermaidASCII(
|
|
130
|
-
'flowchart LR\n A --> B\n A --> C\n A --> D\n A --> E',
|
|
131
|
-
{ colorMode: 'none' },
|
|
132
|
-
)
|
|
133
|
-
expect(Date.now() - start).toBeLessThan(5000)
|
|
134
|
-
for (const label of ['A', 'B', 'C', 'D', 'E']) {
|
|
135
|
-
expect(out).toContain(label)
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Regression for a CodeRabbit finding on this PR: `rerouteAroundStyleConflicts`
|
|
141
|
-
* (grid.ts) temporarily adds a conflicting cell to `graph.grid` so A*
|
|
142
|
-
* avoids it on retry, then removes the reservation once the edge is
|
|
143
|
-
* done. An earlier version passed that *same live, mutating* grid to
|
|
144
|
-
* `findStyleConflict`'s "is this cell node-owned, and therefore not a
|
|
145
|
-
* real conflict" check — so on a second re-route attempt, the cell
|
|
146
|
-
* blocked on attempt 1 looked node-occupied and got silently skipped. If
|
|
147
|
-
* A*'s direct fallback (which ignores occupancy entirely) still crossed
|
|
148
|
-
* that exact cell, the loop wrongly concluded "no conflict" and stopped,
|
|
149
|
-
* leaving the two differently-styled edges still overlapping there.
|
|
150
|
-
*
|
|
151
|
-
* The fix: pass a `nodeOnlyGrid` snapshot — cloned via `cloneGrid` right
|
|
152
|
-
* after node placement, before any edge is routed — to the conflict
|
|
153
|
-
* check instead. This test reproduces the exact mechanism directly:
|
|
154
|
-
* checking against the live (temp-mutated) grid misses the conflict;
|
|
155
|
-
* checking against a frozen snapshot correctly still reports it.
|
|
156
|
-
*/
|
|
157
|
-
it('a temporary reroute reservation on the live grid must not hide a conflict from the frozen node-only snapshot', () => {
|
|
158
|
-
const grid = createGrid()
|
|
159
|
-
placeBlock(grid, { x: 4, y: 8 }) // node D's reserved 3x3 block
|
|
160
|
-
const nodeOnlyGrid = cloneGrid(grid) // taken before any temporary reservation
|
|
161
|
-
|
|
162
|
-
const cellStyles = createEdgeCellStyles()
|
|
163
|
-
const solidPath = [
|
|
164
|
-
{ x: 2, y: 5 },
|
|
165
|
-
{ x: 5, y: 5 },
|
|
166
|
-
{ x: 5, y: 8 },
|
|
167
|
-
]
|
|
168
|
-
claimPathCells(nodeOnlyGrid, cellStyles, solidPath, 'solid')
|
|
169
|
-
|
|
170
|
-
const dottedPathThatStillCrossesTheConflict = [
|
|
171
|
-
{ x: 5, y: 1 },
|
|
172
|
-
{ x: 5, y: 8 },
|
|
173
|
-
]
|
|
174
|
-
|
|
175
|
-
// Simulate what `rerouteAroundStyleConflicts`'s retry loop does when
|
|
176
|
-
// A*'s direct fallback keeps returning the *same* unchanged path each
|
|
177
|
-
// time (occupancy-blind, by definition): each iteration finds the
|
|
178
|
-
// next not-yet-blocked conflicting cell and adds it to the live grid.
|
|
179
|
-
// With three open-space conflict cells in the path — (5,5), (5,6),
|
|
180
|
-
// (5,7) — three iterations exhaust every real conflict this way
|
|
181
|
-
// without the edge's route ever actually changing.
|
|
182
|
-
for (let i = 0; i < 3; i++) {
|
|
183
|
-
const conflict = findStyleConflict(
|
|
184
|
-
grid,
|
|
185
|
-
cellStyles,
|
|
186
|
-
dottedPathThatStillCrossesTheConflict,
|
|
187
|
-
'dotted',
|
|
188
|
-
)
|
|
189
|
-
expect(conflict).not.toBeNull()
|
|
190
|
-
grid.add(`${conflict!.x},${conflict!.y}`)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// The bug: after those three iterations, every real conflict cell now
|
|
194
|
-
// looks node-occupied on the live grid — checking against it reports
|
|
195
|
-
// "no conflict" even though the edge's path (unchanged this whole
|
|
196
|
-
// time) still overlaps the solid path in open space.
|
|
197
|
-
const missedByLiveGrid = findStyleConflict(
|
|
198
|
-
grid,
|
|
199
|
-
cellStyles,
|
|
200
|
-
dottedPathThatStillCrossesTheConflict,
|
|
201
|
-
'dotted',
|
|
202
|
-
)
|
|
203
|
-
expect(missedByLiveGrid).toBeNull()
|
|
204
|
-
|
|
205
|
-
// The fix: checking against the untouched snapshot is unaffected by
|
|
206
|
-
// any of those temporary reservations, so the real conflict is still
|
|
207
|
-
// caught (reported at the first crossing cell in path order).
|
|
208
|
-
const caughtByFrozenSnapshot = findStyleConflict(
|
|
209
|
-
nodeOnlyGrid,
|
|
210
|
-
cellStyles,
|
|
211
|
-
dottedPathThatStillCrossesTheConflict,
|
|
212
|
-
'dotted',
|
|
213
|
-
)
|
|
214
|
-
expect(caughtByFrozenSnapshot).toEqual({ x: 5, y: 5 })
|
|
215
|
-
})
|
|
216
|
-
})
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression test for #153 — `createMapping`'s `highestPositionPerLevel`
|
|
3
|
-
* level tracker was a fixed-size-100 array (`new Array(100).fill(0)`), but
|
|
4
|
-
* each level down a chain adds 4 to the tracked coordinate. A chain deeper
|
|
5
|
-
* than ~25 nodes (100 / 4) pushed the index past the preallocated bound,
|
|
6
|
-
* silently reading `undefined` (asserted as `number`) and producing `NaN`
|
|
7
|
-
* grid coordinates instead of throwing or laying out correctly.
|
|
8
|
-
*
|
|
9
|
-
* Fixed by making the tracker a sparse array with `?? 0` defaults on read,
|
|
10
|
-
* so it grows with the chain instead of capping at a fixed depth.
|
|
11
|
-
*/
|
|
12
|
-
import { describe, it, expect } from 'vitest'
|
|
13
|
-
import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
|
|
14
|
-
import { convertToAsciiGraph } from '../../packages/ascii-renderer/src/converter.ts'
|
|
15
|
-
import { createMapping } from '../../packages/ascii-renderer/src/grid.ts'
|
|
16
|
-
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
17
|
-
import type { AsciiConfig } from '../../packages/ascii-renderer/src/types.ts'
|
|
18
|
-
|
|
19
|
-
function longChainSource(depth: number): string {
|
|
20
|
-
const lines = ['flowchart TD']
|
|
21
|
-
for (let i = 1; i < depth; i++) {
|
|
22
|
-
lines.push(` n${i} --> n${i + 1}`)
|
|
23
|
-
}
|
|
24
|
-
return lines.join('\n')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe('ASCII grid layout handles chains deeper than the old fixed-size level tracker (#153)', () => {
|
|
28
|
-
it('lays out a 40-node chain with no NaN grid coordinates', () => {
|
|
29
|
-
const source = longChainSource(40)
|
|
30
|
-
const parsed = parseMermaid(source)
|
|
31
|
-
const config: AsciiConfig = {
|
|
32
|
-
useAscii: false,
|
|
33
|
-
paddingX: 5,
|
|
34
|
-
paddingY: 5,
|
|
35
|
-
boxBorderPadding: 1,
|
|
36
|
-
graphDirection: 'TD',
|
|
37
|
-
}
|
|
38
|
-
const graph = convertToAsciiGraph(parsed, config)
|
|
39
|
-
createMapping(graph)
|
|
40
|
-
|
|
41
|
-
expect(graph.nodes).toHaveLength(40)
|
|
42
|
-
for (const node of graph.nodes) {
|
|
43
|
-
expect(node.gridCoord).not.toBeNull()
|
|
44
|
-
expect(Number.isFinite(node.gridCoord!.x)).toBe(true)
|
|
45
|
-
expect(Number.isFinite(node.gridCoord!.y)).toBe(true)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Each node in the chain should sit strictly deeper (larger y) than the
|
|
49
|
-
// previous one — NaN corruption would break this monotonic ordering.
|
|
50
|
-
const yById = new Map(graph.nodes.map((n) => [n.name, n.gridCoord!.y]))
|
|
51
|
-
for (let i = 1; i < 40; i++) {
|
|
52
|
-
const y = yById.get(`n${i}`)!
|
|
53
|
-
const yNext = yById.get(`n${i + 1}`)!
|
|
54
|
-
expect(yNext).toBeGreaterThan(y)
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('renders a 40-node chain to ASCII without throwing or producing NaN in output', () => {
|
|
59
|
-
const output = renderMermaidASCII(longChainSource(40))
|
|
60
|
-
expect(output).not.toContain('NaN')
|
|
61
|
-
expect(output).toContain('n1')
|
|
62
|
-
expect(output).toContain('n40')
|
|
63
|
-
})
|
|
64
|
-
})
|