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.
Files changed (92) hide show
  1. package/package.json +9 -7
  2. package/src/__tests__/architecture-integration.test.ts +0 -217
  3. package/src/__tests__/ascii-ansi.test.ts +0 -539
  4. package/src/__tests__/ascii-draw-bundles.test.ts +0 -849
  5. package/src/__tests__/ascii-edge-bundling-routing.test.ts +0 -214
  6. package/src/__tests__/ascii-edge-cross-style-overlap.test.ts +0 -216
  7. package/src/__tests__/ascii-grid-deep-chain.test.ts +0 -64
  8. package/src/__tests__/ascii-hyperlinks.test.ts +0 -364
  9. package/src/__tests__/ascii-nested-subgraph-sibling-order-444.test.ts +0 -108
  10. package/src/__tests__/ascii-nonmember-beside-frame-1283.test.ts +0 -113
  11. package/src/__tests__/ascii-package-boundary.test.ts +0 -291
  12. package/src/__tests__/ascii-parallel-edges-329.test.ts +0 -315
  13. package/src/__tests__/ascii-parallel-lane-border-1230.test.ts +0 -71
  14. package/src/__tests__/ascii-reverse-direction-edges-629.test.ts +0 -282
  15. package/src/__tests__/ascii-stacked-frame-geometry-1285.test.ts +0 -107
  16. package/src/__tests__/ascii-subgraph-bbox.test.ts +0 -139
  17. package/src/__tests__/ascii-subgraph-later-edgeless-301.test.ts +0 -277
  18. package/src/__tests__/browser.test.ts +0 -34
  19. package/src/__tests__/c4-integration.test.ts +0 -161
  20. package/src/__tests__/c4-orientation.test.ts +0 -162
  21. package/src/__tests__/c4-upstream-integration.test.ts +0 -211
  22. package/src/__tests__/class-click-interactions.test.ts +0 -232
  23. package/src/__tests__/class-integration.test.ts +0 -154
  24. package/src/__tests__/class-notes.test.ts +0 -317
  25. package/src/__tests__/class-parser-error-messages.test.ts +0 -162
  26. package/src/__tests__/class-parser.test.ts +0 -347
  27. package/src/__tests__/class-styling.test.ts +0 -485
  28. package/src/__tests__/cli-diagrams.test.ts +0 -106
  29. package/src/__tests__/cli-e2e.test.ts +0 -275
  30. package/src/__tests__/cli-entrypoint.test.ts +0 -146
  31. package/src/__tests__/cli-mcp-dispatch.test.ts +0 -68
  32. package/src/__tests__/cli-mcp.test.ts +0 -44
  33. package/src/__tests__/cli-png.test.ts +0 -35
  34. package/src/__tests__/cli-render.test.ts +0 -988
  35. package/src/__tests__/cli-test-helpers.ts +0 -54
  36. package/src/__tests__/cli-web-dispatch.test.ts +0 -62
  37. package/src/__tests__/cli-web.test.ts +0 -203
  38. package/src/__tests__/cli.test.ts +0 -1027
  39. package/src/__tests__/compound-flat-layout.test.ts +0 -901
  40. package/src/__tests__/direction-override.test.ts +0 -397
  41. package/src/__tests__/edge-approach-direction.test.ts +0 -295
  42. package/src/__tests__/edge-bundling-node-clearance.test.ts +0 -179
  43. package/src/__tests__/elk-layout-cache.test.ts +0 -223
  44. package/src/__tests__/er-click-unsupported.test.ts +0 -69
  45. package/src/__tests__/er-integration.test.ts +0 -531
  46. package/src/__tests__/er-parser-error-messages.test.ts +0 -82
  47. package/src/__tests__/er-parser.test.ts +0 -294
  48. package/src/__tests__/expanded-shapes.test.ts +0 -229
  49. package/src/__tests__/flowchart-back-edge-prereversal-1239.test.ts +0 -212
  50. package/src/__tests__/flowchart-syntax-gaps.test.ts +0 -371
  51. package/src/__tests__/guides-examples.test.ts +0 -181
  52. package/src/__tests__/integration.test.ts +0 -729
  53. package/src/__tests__/interactivity-config.test.ts +0 -723
  54. package/src/__tests__/interactivity-option.test.ts +0 -173
  55. package/src/__tests__/layout-disconnected.test.ts +0 -384
  56. package/src/__tests__/layout-engine.test.ts +0 -91
  57. package/src/__tests__/linkstyle.test.ts +0 -133
  58. package/src/__tests__/mcp-render-ascii-tool.test.ts +0 -95
  59. package/src/__tests__/mcp-render-svg-theme-guard.test.ts +0 -36
  60. package/src/__tests__/mcp-render-svg-tool.test.ts +0 -118
  61. package/src/__tests__/mermaid-entity-codes.test.ts +0 -114
  62. package/src/__tests__/monospace-metrics.test.ts +0 -74
  63. package/src/__tests__/multiline-labels.test.ts +0 -924
  64. package/src/__tests__/nested-subgraph-direction-crossing.test.ts +0 -322
  65. package/src/__tests__/nested-subgraph-edge-container-offset.test.ts +0 -151
  66. package/src/__tests__/package-info.test.ts +0 -51
  67. package/src/__tests__/parser.test.ts +0 -1544
  68. package/src/__tests__/render-options-overrides.test.ts +0 -257
  69. package/src/__tests__/render-options-per-type.test.ts +0 -178
  70. package/src/__tests__/renderer.test.ts +0 -743
  71. package/src/__tests__/resolve-colors.test.ts +0 -448
  72. package/src/__tests__/self-loop-edges.test.ts +0 -112
  73. package/src/__tests__/sequence-activation-check.test.ts +0 -219
  74. package/src/__tests__/sequence-integration.test.ts +0 -312
  75. package/src/__tests__/sequence-layout.test.ts +0 -1103
  76. package/src/__tests__/sequence-parser-syntax-errors.test.ts +0 -179
  77. package/src/__tests__/sequence-parser.test.ts +0 -821
  78. package/src/__tests__/start-marker-only-links-1386.test.ts +0 -104
  79. package/src/__tests__/state-composite-order-1287.test.ts +0 -132
  80. package/src/__tests__/state-flow-order-1240.test.ts +0 -113
  81. package/src/__tests__/state-styling.test.ts +0 -150
  82. package/src/__tests__/statements.test.ts +0 -264
  83. package/src/__tests__/styles.test.ts +0 -220
  84. package/src/__tests__/subgraph-title-edge-gap-1239.test.ts +0 -238
  85. package/src/__tests__/svg-accessible-name-conformance.test.ts +0 -276
  86. package/src/__tests__/svg-accessible-name.test.ts +0 -421
  87. package/src/__tests__/svg-csp-style-options.test.ts +0 -427
  88. package/src/__tests__/workspace-package-boundaries.test.ts +0 -336
  89. package/src/__tests__/xychart-integration.test.ts +0 -137
  90. package/src/__tests__/xychart-layout.test.ts +0 -205
  91. package/src/__tests__/xychart-parser-categories.test.ts +0 -78
  92. 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
- })