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,277 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for #301 — "ASCII backend: a subgraph that carries no
|
|
3
|
-
* edge is absorbed into an earlier subgraph's frame and its title is
|
|
4
|
-
* deleted."
|
|
5
|
-
*
|
|
6
|
-
* This is the mirror image of #90/#143 (see ascii-subgraph-bbox.test.ts):
|
|
7
|
-
* that fix deferred an edge-less *root* node's placement so it wouldn't be
|
|
8
|
-
* laid out at the shared root level alongside an unrelated sibling
|
|
9
|
-
* subgraph's root. But the deferred node still found its free slot via a
|
|
10
|
-
* subgraph-agnostic blind slide (`reserveSpotInGrid`'s built-in collision
|
|
11
|
-
* handling) — sliding one grid step at a time along the shared axis with no
|
|
12
|
-
* notion of subgraph ownership. When an *earlier* subgraph has such a
|
|
13
|
-
* deferred node AND a *later*, entirely edge-less sibling subgraph already
|
|
14
|
-
* claimed the very next slot, the blind slide walked straight through that
|
|
15
|
-
* later subgraph's root to find free space beyond it — landing the deferred
|
|
16
|
-
* node on the far side. That sandwiched the later subgraph's node between
|
|
17
|
-
* the earlier subgraph's own members, so the earlier subgraph's bounding box
|
|
18
|
-
* (min/max over its members' positions) ballooned out to enclose the later
|
|
19
|
-
* one, corrupting both frames and dropping the later subgraph's title
|
|
20
|
-
* entirely.
|
|
21
|
-
*
|
|
22
|
-
* The fix (`src/ascii/grid.ts`): attach each subgraph's deferred nodes to
|
|
23
|
-
* whichever of its own roots gets placed first, immediately, before control
|
|
24
|
-
* returns to the root-placement loop — so an unrelated subgraph's root can
|
|
25
|
-
* never claim the slot a deferred sibling needs (`deferredByTopSg` /
|
|
26
|
-
* `placeDeferredSiblingsNextToRoot`). A `findSubgraphAdjacentSlot` fallback
|
|
27
|
-
* additionally keeps the later pass (for deferred nodes whose anchor is
|
|
28
|
-
* itself a non-root) from enclosing a foreign subgraph's node when sliding.
|
|
29
|
-
*/
|
|
30
|
-
import { describe, it, expect } from 'vitest'
|
|
31
|
-
import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
|
|
32
|
-
import { convertToAsciiGraph } from '../../packages/ascii-renderer/src/converter.ts'
|
|
33
|
-
import { createMapping } from '../../packages/ascii-renderer/src/grid.ts'
|
|
34
|
-
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
35
|
-
import type {
|
|
36
|
-
AsciiConfig,
|
|
37
|
-
AsciiGraph,
|
|
38
|
-
AsciiSubgraph,
|
|
39
|
-
} from '../../packages/ascii-renderer/src/types.ts'
|
|
40
|
-
|
|
41
|
-
function layoutFlowchart(
|
|
42
|
-
source: string,
|
|
43
|
-
direction: 'TD' | 'LR' = 'TD',
|
|
44
|
-
): AsciiGraph {
|
|
45
|
-
const parsed = parseMermaid(source)
|
|
46
|
-
const config: AsciiConfig = {
|
|
47
|
-
useAscii: false,
|
|
48
|
-
paddingX: 5,
|
|
49
|
-
paddingY: 5,
|
|
50
|
-
boxBorderPadding: 1,
|
|
51
|
-
graphDirection: direction,
|
|
52
|
-
}
|
|
53
|
-
const graph = convertToAsciiGraph(parsed, config)
|
|
54
|
-
createMapping(graph)
|
|
55
|
-
return graph
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function boxesOverlap(a: AsciiSubgraph, b: AsciiSubgraph): boolean {
|
|
59
|
-
return !(
|
|
60
|
-
a.maxX <= b.minX ||
|
|
61
|
-
b.maxX <= a.minX ||
|
|
62
|
-
a.maxY <= b.minY ||
|
|
63
|
-
b.maxY <= a.minY
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function findSubgraph(graph: AsciiGraph, name: string): AsciiSubgraph {
|
|
68
|
-
const sg = graph.subgraphs.find((s) => s.name === name)
|
|
69
|
-
if (!sg) throw new Error(`subgraph "${name}" not found`)
|
|
70
|
-
return sg
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Whether a node's drawn box sits fully inside a subgraph's bounding box. */
|
|
74
|
-
function nodeInsideBox(
|
|
75
|
-
graph: AsciiGraph,
|
|
76
|
-
nodeName: string,
|
|
77
|
-
sg: AsciiSubgraph,
|
|
78
|
-
): boolean {
|
|
79
|
-
const node = graph.nodes.find((n) => n.name === nodeName)
|
|
80
|
-
if (!node?.drawingCoord || !node.drawing) {
|
|
81
|
-
throw new Error(`node "${nodeName}" wasn't drawn`)
|
|
82
|
-
}
|
|
83
|
-
const minX = node.drawingCoord.x
|
|
84
|
-
const minY = node.drawingCoord.y
|
|
85
|
-
const maxX = minX + node.drawing.length - 1
|
|
86
|
-
const maxY = minY + node.drawing[0]!.length - 1
|
|
87
|
-
return (
|
|
88
|
-
minX >= sg.minX && maxX <= sg.maxX && minY >= sg.minY && maxY <= sg.maxY
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
describe('ASCII: a later edge-less subgraph is not absorbed into an earlier one (#301)', () => {
|
|
93
|
-
const issueRepro = `flowchart TB
|
|
94
|
-
subgraph a["Group A"]
|
|
95
|
-
a1["alpha"]
|
|
96
|
-
a2["bravo"]
|
|
97
|
-
a3["charlie"]
|
|
98
|
-
end
|
|
99
|
-
subgraph b["Group B"]
|
|
100
|
-
b1["delta"]
|
|
101
|
-
end
|
|
102
|
-
a1 --> a2`
|
|
103
|
-
|
|
104
|
-
it('keeps two subgraphs disjoint: earlier one has an edge-less member, later one carries no edge at all (exact issue repro)', () => {
|
|
105
|
-
const graph = layoutFlowchart(issueRepro, 'TD')
|
|
106
|
-
expect(graph.subgraphs.length).toBe(2)
|
|
107
|
-
|
|
108
|
-
const groupA = findSubgraph(graph, 'Group A')
|
|
109
|
-
const groupB = findSubgraph(graph, 'Group B')
|
|
110
|
-
expect(boxesOverlap(groupA, groupB)).toBe(false)
|
|
111
|
-
|
|
112
|
-
// delta belongs to Group B and must be drawn inside Group B's frame,
|
|
113
|
-
// not dragged into Group A's.
|
|
114
|
-
expect(nodeInsideBox(graph, 'b1', groupB)).toBe(true)
|
|
115
|
-
expect(nodeInsideBox(graph, 'b1', groupA)).toBe(false)
|
|
116
|
-
|
|
117
|
-
const output = renderMermaidASCII(issueRepro)
|
|
118
|
-
expect(output).toContain('Group A')
|
|
119
|
-
expect(output).toContain('Group B')
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('keeps the same two subgraphs disjoint in LR', () => {
|
|
123
|
-
const lrSource = issueRepro.replace('flowchart TB', 'flowchart LR')
|
|
124
|
-
const graph = layoutFlowchart(lrSource, 'LR')
|
|
125
|
-
|
|
126
|
-
const groupA = findSubgraph(graph, 'Group A')
|
|
127
|
-
const groupB = findSubgraph(graph, 'Group B')
|
|
128
|
-
expect(boxesOverlap(groupA, groupB)).toBe(false)
|
|
129
|
-
expect(nodeInsideBox(graph, 'b1', groupB)).toBe(true)
|
|
130
|
-
|
|
131
|
-
const output = renderMermaidASCII(lrSource)
|
|
132
|
-
expect(output).toContain('Group A')
|
|
133
|
-
expect(output).toContain('Group B')
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('does not collapse a third, equally edge-less subgraph into the same frame (N-subgraph case)', () => {
|
|
137
|
-
const source = `flowchart TB
|
|
138
|
-
subgraph a["Group A"]
|
|
139
|
-
a1["alpha"]
|
|
140
|
-
a2["bravo"]
|
|
141
|
-
a3["charlie"]
|
|
142
|
-
end
|
|
143
|
-
subgraph b["Group B"]
|
|
144
|
-
b1["delta"]
|
|
145
|
-
end
|
|
146
|
-
subgraph c["Group C"]
|
|
147
|
-
c1["echo"]
|
|
148
|
-
end
|
|
149
|
-
a1 --> a2`
|
|
150
|
-
|
|
151
|
-
const graph = layoutFlowchart(source, 'TD')
|
|
152
|
-
expect(graph.subgraphs.length).toBe(3)
|
|
153
|
-
|
|
154
|
-
const groupA = findSubgraph(graph, 'Group A')
|
|
155
|
-
const groupB = findSubgraph(graph, 'Group B')
|
|
156
|
-
const groupC = findSubgraph(graph, 'Group C')
|
|
157
|
-
|
|
158
|
-
for (const [nameI, sgI] of [
|
|
159
|
-
['Group A', groupA],
|
|
160
|
-
['Group B', groupB],
|
|
161
|
-
['Group C', groupC],
|
|
162
|
-
] as const) {
|
|
163
|
-
for (const [nameJ, sgJ] of [
|
|
164
|
-
['Group A', groupA],
|
|
165
|
-
['Group B', groupB],
|
|
166
|
-
['Group C', groupC],
|
|
167
|
-
] as const) {
|
|
168
|
-
if (nameI >= nameJ) continue
|
|
169
|
-
expect(
|
|
170
|
-
boxesOverlap(sgI, sgJ),
|
|
171
|
-
`subgraph "${nameI}" overlaps "${nameJ}"`,
|
|
172
|
-
).toBe(false)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
expect(nodeInsideBox(graph, 'b1', groupB)).toBe(true)
|
|
177
|
-
expect(nodeInsideBox(graph, 'c1', groupC)).toBe(true)
|
|
178
|
-
expect(nodeInsideBox(graph, 'b1', groupA)).toBe(false)
|
|
179
|
-
expect(nodeInsideBox(graph, 'c1', groupA)).toBe(false)
|
|
180
|
-
|
|
181
|
-
const output = renderMermaidASCII(source)
|
|
182
|
-
expect(output).toContain('Group A')
|
|
183
|
-
expect(output).toContain('Group B')
|
|
184
|
-
expect(output).toContain('Group C')
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it('slides a second edge-less sibling past the first instead of colliding with it', () => {
|
|
188
|
-
// a3 and a4 are both edge-less and both unreachable from a1, so both
|
|
189
|
-
// get deferred and both attach to a1. The second one (a4) must slide
|
|
190
|
-
// past the first (a3) — exercising findSubgraphAdjacentSlot's "occupant
|
|
191
|
-
// belongs to the same subgraph, keep sliding" branch — rather than
|
|
192
|
-
// colliding with it.
|
|
193
|
-
const source = `flowchart TB
|
|
194
|
-
subgraph a["Group A"]
|
|
195
|
-
a1["alpha"]
|
|
196
|
-
a2["bravo"]
|
|
197
|
-
a3["charlie"]
|
|
198
|
-
a4["echo"]
|
|
199
|
-
end
|
|
200
|
-
a1 --> a2`
|
|
201
|
-
|
|
202
|
-
const graph = layoutFlowchart(source, 'TD')
|
|
203
|
-
const groupA = findSubgraph(graph, 'Group A')
|
|
204
|
-
expect(groupA.nodes.length).toBe(4)
|
|
205
|
-
|
|
206
|
-
const a3 = graph.nodes.find((n) => n.name === 'a3')!
|
|
207
|
-
const a4 = graph.nodes.find((n) => n.name === 'a4')!
|
|
208
|
-
expect(a3.gridCoord).not.toBeNull()
|
|
209
|
-
expect(a4.gridCoord).not.toBeNull()
|
|
210
|
-
// Distinct slots — a4 didn't overwrite a3's spot.
|
|
211
|
-
expect(a4.gridCoord).not.toEqual(a3.gridCoord)
|
|
212
|
-
|
|
213
|
-
const output = renderMermaidASCII(source)
|
|
214
|
-
expect(output).toContain('Group A')
|
|
215
|
-
expect(output).toContain('charlie')
|
|
216
|
-
expect(output).toContain('echo')
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('falls back gracefully (no crash, titles intact) when a non-root anchor is foreign-blocked on both sides', () => {
|
|
220
|
-
// b2 is edge-less and unreachable from b1, so it's deferred and anchors
|
|
221
|
-
// to b1 — but b1 is itself a *non-root* (a child of a1 via an edge), so
|
|
222
|
-
// this only resolves in the later fallback pass, after an unrelated
|
|
223
|
-
// external node's own child (e2) has already claimed the slot
|
|
224
|
-
// immediately next to b1, and the opposite slot is off-grid (negative).
|
|
225
|
-
// findSubgraphAdjacentSlot correctly refuses to slide through e2 in
|
|
226
|
-
// either direction (returning null), so this exercises its
|
|
227
|
-
// "foreign-blocked" path on both sides — but with no free adjacent slot
|
|
228
|
-
// available, the caller falls back to the pre-existing (#143-era) blind
|
|
229
|
-
// slide, which can still enclose e2. That fallback is a known, narrow
|
|
230
|
-
// limitation (see the "Defensive fallback" comment in grid.ts) distinct
|
|
231
|
-
// from the bug #301 fixes: it only arises when the deferred node's
|
|
232
|
-
// anchor is itself a non-root, not for the reported root-anchored case.
|
|
233
|
-
// This test guards against a crash or dropped title, not full
|
|
234
|
-
// disjointness.
|
|
235
|
-
const source = `flowchart TB
|
|
236
|
-
subgraph a["Group A"]
|
|
237
|
-
a1["alpha"]
|
|
238
|
-
end
|
|
239
|
-
subgraph b["Group B"]
|
|
240
|
-
b1["bravo"]
|
|
241
|
-
b2["charlie"]
|
|
242
|
-
end
|
|
243
|
-
e1["delta"]
|
|
244
|
-
a1 --> b1
|
|
245
|
-
e1 --> e2["echo"]`
|
|
246
|
-
|
|
247
|
-
const graph = layoutFlowchart(source, 'TD')
|
|
248
|
-
expect(graph.nodes.find((n) => n.name === 'b2')?.gridCoord).not.toBeNull()
|
|
249
|
-
|
|
250
|
-
const output = renderMermaidASCII(source)
|
|
251
|
-
expect(output).toContain('Group A')
|
|
252
|
-
expect(output).toContain('Group B')
|
|
253
|
-
expect(output).toContain('charlie')
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('OK case: continues to work when every node in the earlier subgraph is on an edge', () => {
|
|
257
|
-
const source = `flowchart TB
|
|
258
|
-
subgraph a["Group A"]
|
|
259
|
-
a1["alpha"]
|
|
260
|
-
a2["bravo"]
|
|
261
|
-
end
|
|
262
|
-
subgraph b["Group B"]
|
|
263
|
-
b1["delta"]
|
|
264
|
-
end
|
|
265
|
-
a1 --> a2`
|
|
266
|
-
|
|
267
|
-
const graph = layoutFlowchart(source, 'TD')
|
|
268
|
-
const groupA = findSubgraph(graph, 'Group A')
|
|
269
|
-
const groupB = findSubgraph(graph, 'Group B')
|
|
270
|
-
expect(boxesOverlap(groupA, groupB)).toBe(false)
|
|
271
|
-
expect(nodeInsideBox(graph, 'b1', groupB)).toBe(true)
|
|
272
|
-
|
|
273
|
-
const output = renderMermaidASCII(source)
|
|
274
|
-
expect(output).toContain('Group A')
|
|
275
|
-
expect(output).toContain('Group B')
|
|
276
|
-
})
|
|
277
|
-
})
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the browser entry point.
|
|
3
|
-
*
|
|
4
|
-
* `src/browser.ts` runs in a plain `<script>` context where `window` is
|
|
5
|
-
* ambient, but our test environment is Node, so we stub a fake `window`
|
|
6
|
-
* before importing the module and reset the module registry so the
|
|
7
|
-
* top-level assignment actually re-runs.
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, expect, afterEach, vi } from 'vitest'
|
|
10
|
-
import type { MermaidBrowserGlobal } from '../browser.ts'
|
|
11
|
-
|
|
12
|
-
describe('browser entry point', () => {
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
vi.unstubAllGlobals()
|
|
15
|
-
vi.resetModules()
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('attaches the mermaid API surface to window.__mermaid', async () => {
|
|
19
|
-
const fakeWindow: { __mermaid?: MermaidBrowserGlobal } = {}
|
|
20
|
-
vi.stubGlobal('window', fakeWindow)
|
|
21
|
-
|
|
22
|
-
await import('../browser.ts')
|
|
23
|
-
|
|
24
|
-
const api = fakeWindow.__mermaid
|
|
25
|
-
expect(api).toBeDefined()
|
|
26
|
-
if (!api) throw new Error('window.__mermaid was not set')
|
|
27
|
-
expect(api.renderMermaidSVGAsync).toBeTypeOf('function')
|
|
28
|
-
expect(api.renderMermaidASCII).toBeTypeOf('function')
|
|
29
|
-
expect(api.diagramColorsToAsciiTheme).toBeTypeOf('function')
|
|
30
|
-
expect(api.getSeriesColor).toBeTypeOf('function')
|
|
31
|
-
expect(api.THEMES).toBeTypeOf('object')
|
|
32
|
-
expect(typeof api.CHART_ACCENT_FALLBACK).not.toBe('undefined')
|
|
33
|
-
})
|
|
34
|
-
})
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { renderMermaidSVG, renderMermaidASCII } from '../index.ts'
|
|
3
|
-
|
|
4
|
-
// End-to-end coverage for C4 diagrams through both front doors (the
|
|
5
|
-
// dedicated C4 renderers, not the flowchart pipeline). The fixtures ported
|
|
6
|
-
// from lukilabs/beautiful-mermaid#71 live in c4-upstream-integration.test.ts.
|
|
7
|
-
|
|
8
|
-
const CONTEXT = `C4Context
|
|
9
|
-
title System Context for Internet Banking
|
|
10
|
-
Person(customer, "Personal Banking Customer", "A customer of the bank")
|
|
11
|
-
Enterprise_Boundary(b0, "Bank") {
|
|
12
|
-
System(banking, "Internet Banking System", "Lets customers view accounts")
|
|
13
|
-
SystemDb(ledger, "Ledger", "Stores transactions")
|
|
14
|
-
}
|
|
15
|
-
System_Ext(mail, "E-mail System", "Sends e-mails")
|
|
16
|
-
Rel(customer, banking, "Uses", "HTTPS")
|
|
17
|
-
Rel(banking, ledger, "Reads from and writes to")
|
|
18
|
-
Rel_Back(customer, mail, "Sends e-mails to")
|
|
19
|
-
Rel(banking, mail, "Sends e-mail using")`
|
|
20
|
-
|
|
21
|
-
const CONTAINER = `C4Container
|
|
22
|
-
Person(user, "User")
|
|
23
|
-
System_Boundary(sys, "Shop") {
|
|
24
|
-
Container(web, "Web App", "React", "Storefront")
|
|
25
|
-
Container(api, "API", "Node.js", "Business logic")
|
|
26
|
-
ContainerDb(db, "Database", "PostgreSQL", "Orders")
|
|
27
|
-
ContainerQueue(q, "Events", "Kafka")
|
|
28
|
-
}
|
|
29
|
-
System_Ext(pay, "Payments")
|
|
30
|
-
Rel(user, web, "Browses")
|
|
31
|
-
Rel(web, api, "Calls", "JSON/HTTPS")
|
|
32
|
-
Rel(api, db, "Reads/writes", "SQL")
|
|
33
|
-
BiRel(api, q, "Publishes", "AMQP")
|
|
34
|
-
Rel(api, pay, "Charges cards")`
|
|
35
|
-
|
|
36
|
-
const COMPONENT = `C4Component
|
|
37
|
-
Container_Boundary(api, "API") {
|
|
38
|
-
Component(ctl, "Controller", "Express", "Routes")
|
|
39
|
-
Component(svc, "Service", "TypeScript", "Rules")
|
|
40
|
-
}
|
|
41
|
-
ContainerDb(db, "Database", "Postgres")
|
|
42
|
-
Rel(ctl, svc, "Uses")
|
|
43
|
-
Rel(svc, db, "Reads")`
|
|
44
|
-
|
|
45
|
-
describe('C4 SVG rendering', () => {
|
|
46
|
-
for (const [name, src] of [
|
|
47
|
-
['context', CONTEXT],
|
|
48
|
-
['container', CONTAINER],
|
|
49
|
-
['component', COMPONENT],
|
|
50
|
-
] as const) {
|
|
51
|
-
it(`renders a ${name} diagram`, () => {
|
|
52
|
-
const svg = renderMermaidSVG(src)
|
|
53
|
-
expect(svg.startsWith('<svg')).toBe(true)
|
|
54
|
-
expect(svg).toContain('</svg>')
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
it('draws element names, type lines and boundary labels', () => {
|
|
59
|
-
const svg = renderMermaidSVG(CONTAINER)
|
|
60
|
-
for (const text of [
|
|
61
|
-
'Web App',
|
|
62
|
-
'[Container: React]',
|
|
63
|
-
'[Container: PostgreSQL]',
|
|
64
|
-
'[Software System]',
|
|
65
|
-
'Shop',
|
|
66
|
-
'Calls',
|
|
67
|
-
'[JSON/HTTPS]',
|
|
68
|
-
]) {
|
|
69
|
-
expect(svg).toContain(text)
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('applies the C4 palette to people and external systems', () => {
|
|
74
|
-
const svg = renderMermaidSVG(CONTEXT)
|
|
75
|
-
expect(svg).toContain('#08427b') // person
|
|
76
|
-
expect(svg).toContain('#1168bd') // internal system
|
|
77
|
-
expect(svg).toContain('#999999') // external system
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('draws a cylinder for Db variants', () => {
|
|
81
|
-
const db = renderMermaidSVG(
|
|
82
|
-
'C4Context\nSystemDb(d, "DB")\nSystem(s, "S")\nRel(s, d, "x")',
|
|
83
|
-
)
|
|
84
|
-
const plain = renderMermaidSVG(
|
|
85
|
-
'C4Context\nSystem(d, "DB")\nSystem(s, "S")\nRel(s, d, "x")',
|
|
86
|
-
)
|
|
87
|
-
// Only the cylinder has elliptical caps.
|
|
88
|
-
expect(db).toContain('<ellipse')
|
|
89
|
-
expect(plain).not.toContain('<ellipse')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('ignores the direction option in SVG, as Mermaid has none for C4', () => {
|
|
93
|
-
// Shapes are placed in rows in declaration order, whatever the direction.
|
|
94
|
-
const strip = (svg: string) => svg.replace(/zm-title-\d+/g, 'zm-title')
|
|
95
|
-
const tb = strip(renderMermaidSVG(COMPONENT))
|
|
96
|
-
for (const direction of ['BT', 'LR', 'RL'] as const) {
|
|
97
|
-
expect(strip(renderMermaidSVG(COMPONENT, { direction }))).toBe(tb)
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('surfaces parse errors with a line number', () => {
|
|
102
|
-
expect(() =>
|
|
103
|
-
renderMermaidSVG('C4Context\nSystem(a, "A")\nSystem(a, "B")'),
|
|
104
|
-
).toThrow(/line 3: duplicate alias/)
|
|
105
|
-
})
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
describe('C4 ASCII rendering', () => {
|
|
109
|
-
for (const [name, src] of [
|
|
110
|
-
['context', CONTEXT],
|
|
111
|
-
['container', CONTAINER],
|
|
112
|
-
['component', COMPONENT],
|
|
113
|
-
] as const) {
|
|
114
|
-
it(`renders a ${name} diagram`, () => {
|
|
115
|
-
const out = renderMermaidASCII(src)
|
|
116
|
-
expect(out.length).toBeGreaterThan(0)
|
|
117
|
-
expect(out).toMatch(/[┌+]/)
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
it('shows element names, type lines and boundary titles', () => {
|
|
122
|
-
const out = renderMermaidASCII(CONTAINER)
|
|
123
|
-
for (const text of ['Web App', '[Container: React]', 'Shop', 'Calls']) {
|
|
124
|
-
expect(out).toContain(text)
|
|
125
|
-
}
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('is deterministic', () => {
|
|
129
|
-
expect(renderMermaidASCII(COMPONENT)).toBe(renderMermaidASCII(COMPONENT))
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('surfaces parse errors with a line number', () => {
|
|
133
|
-
expect(() => renderMermaidASCII('C4Context\nBogus(a, "A")')).toThrow(
|
|
134
|
-
/line 2: unknown C4 macro "Bogus"/,
|
|
135
|
-
)
|
|
136
|
-
})
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
describe('Rel_Back arrow direction', () => {
|
|
140
|
-
const src = `C4Context
|
|
141
|
-
System(a, "A")
|
|
142
|
-
System(b, "B")
|
|
143
|
-
Rel_Back(a, b, "Returns")`
|
|
144
|
-
|
|
145
|
-
it('puts the SVG arrowhead at the declared "from" end', () => {
|
|
146
|
-
const svg = renderMermaidSVG(src)
|
|
147
|
-
const d =
|
|
148
|
-
/<path class="c4-relationship"[^>]* d="M ([\d.-]+),([\d.-]+) /.exec(svg)!
|
|
149
|
-
const [sx, sy] = [Number(d[1]), Number(d[2])]
|
|
150
|
-
const tip = /<polygon class="c4-arrowhead" points="([\d.]+),([\d.]+) /.exec(
|
|
151
|
-
svg,
|
|
152
|
-
)!
|
|
153
|
-
expect(Math.hypot(Number(tip[1]) - sx, Number(tip[2]) - sy)).toBeLessThan(1)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('draws exactly one ASCII arrowhead, pointing up at "from"', () => {
|
|
157
|
-
const out = renderMermaidASCII(src)
|
|
158
|
-
expect(out).toContain('▲')
|
|
159
|
-
expect(out).not.toContain('▼')
|
|
160
|
-
})
|
|
161
|
-
})
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { renderMermaidSVG, renderMermaidASCII } from '../index.ts'
|
|
3
|
-
|
|
4
|
-
// `RenderOptions.direction` through the C4 renderers.
|
|
5
|
-
//
|
|
6
|
-
// SVG follows Mermaid's C4 renderer, which has no direction: shapes are placed
|
|
7
|
-
// in rows in declaration order, so the option changes nothing. What must hold
|
|
8
|
-
// in every direction is that each arrowhead sits on the right end of its
|
|
9
|
-
// relationship (`Rel` at `to`, `Rel_Back` at `from`, `BiRel` at both).
|
|
10
|
-
//
|
|
11
|
-
// ASCII keeps its own layout: top to bottom, `BT` flips it, and `LR`/`RL`
|
|
12
|
-
// render like `TB`.
|
|
13
|
-
|
|
14
|
-
const DIRECTIONS = ['TB', 'BT', 'LR', 'RL'] as const
|
|
15
|
-
type Dir = (typeof DIRECTIONS)[number]
|
|
16
|
-
|
|
17
|
-
const SOURCE = `C4Context
|
|
18
|
-
Person(a, "A")
|
|
19
|
-
Enterprise_Boundary(b0, "Bank") {
|
|
20
|
-
System(b, "B")
|
|
21
|
-
SystemDb(c, "C")
|
|
22
|
-
}
|
|
23
|
-
System_Ext(d, "D")
|
|
24
|
-
Rel(a, b, "Uses", "HTTPS")
|
|
25
|
-
Rel(b, c, "Reads from and writes to")
|
|
26
|
-
Rel_Back(a, d, "Sends to")
|
|
27
|
-
BiRel(b, d, "Syncs")`
|
|
28
|
-
|
|
29
|
-
const svgOf = (dir: Dir): string => renderMermaidSVG(SOURCE, { direction: dir })
|
|
30
|
-
|
|
31
|
-
interface Line {
|
|
32
|
-
from: string
|
|
33
|
-
to: string
|
|
34
|
-
start: [number, number]
|
|
35
|
-
end: [number, number]
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Each relationship's end points, read from its path (`M sx,sy L|Q ... ex,ey`). */
|
|
39
|
-
function lines(svg: string): Line[] {
|
|
40
|
-
return [
|
|
41
|
-
...svg.matchAll(
|
|
42
|
-
/<path class="c4-relationship" data-from="([^"]+)" data-to="([^"]+)"[^>]* d="([^"]+)"/g,
|
|
43
|
-
),
|
|
44
|
-
].map((m) => {
|
|
45
|
-
const nums = m[3]!.match(/-?[\d.]+/g)!.map(Number)
|
|
46
|
-
return {
|
|
47
|
-
from: m[1]!,
|
|
48
|
-
to: m[2]!,
|
|
49
|
-
start: [nums[0]!, nums[1]!],
|
|
50
|
-
end: [nums[nums.length - 2]!, nums[nums.length - 1]!],
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Arrowhead tips: the first vertex of each arrowhead polygon. */
|
|
56
|
-
function tips(svg: string): [number, number][] {
|
|
57
|
-
return [
|
|
58
|
-
...svg.matchAll(
|
|
59
|
-
/<polygon class="c4-arrowhead" points="(-?[\d.]+),(-?[\d.]+) /g,
|
|
60
|
-
),
|
|
61
|
-
].map((m) => [Number(m[1]), Number(m[2])])
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const near = (p: [number, number], q: [number, number]): boolean =>
|
|
65
|
-
Math.abs(p[0] - q[0]) < 1 && Math.abs(p[1] - q[1]) < 1
|
|
66
|
-
|
|
67
|
-
describe.each(DIRECTIONS)('C4 SVG, direction %s', (dir) => {
|
|
68
|
-
const svg = svgOf(dir)
|
|
69
|
-
const line = (from: string, to: string): Line => {
|
|
70
|
-
const l = lines(svg).find((x) => x.from === from && x.to === to)
|
|
71
|
-
if (!l) throw new Error(`no relationship ${from} -> ${to}`)
|
|
72
|
-
return l
|
|
73
|
-
}
|
|
74
|
-
const headsAt = (p: [number, number]): number =>
|
|
75
|
-
tips(svg).filter((t) => near(t, p)).length
|
|
76
|
-
|
|
77
|
-
it('puts the arrowhead at "to" for Rel', () => {
|
|
78
|
-
const l = line('a', 'b')
|
|
79
|
-
expect(headsAt(l.end)).toBe(1)
|
|
80
|
-
expect(headsAt(l.start)).toBe(0)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('puts the arrowhead at "from" for Rel_Back', () => {
|
|
84
|
-
const l = line('a', 'd')
|
|
85
|
-
expect(headsAt(l.start)).toBe(1)
|
|
86
|
-
expect(headsAt(l.end)).toBe(0)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('puts an arrowhead at both ends for BiRel', () => {
|
|
90
|
-
const l = line('b', 'd')
|
|
91
|
-
expect(headsAt(l.start)).toBe(1)
|
|
92
|
-
expect(headsAt(l.end)).toBe(1)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('draws one arrowhead per Rel and two for BiRel', () => {
|
|
96
|
-
expect(tips(svg)).toHaveLength(1 + 1 + 1 + 2)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('is the same drawing in every direction', () => {
|
|
100
|
-
const strip = (s: string) => s.replace(/zm-title-\d+/g, 'zm-title')
|
|
101
|
-
expect(strip(svg)).toBe(strip(svgOf('TB')))
|
|
102
|
-
})
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
describe('C4 ASCII orientations', () => {
|
|
106
|
-
const ascii = (dir: Dir): string =>
|
|
107
|
-
renderMermaidASCII(SOURCE, { direction: dir })
|
|
108
|
-
|
|
109
|
-
it('draws arrowheads on the right ends in TB', () => {
|
|
110
|
-
const out = ascii('TB')
|
|
111
|
-
// Rel, Rel, and one end each of Rel_Back and BiRel point down; the
|
|
112
|
-
// Rel_Back head and the other BiRel head point up.
|
|
113
|
-
expect([...out].filter((c) => c === '▼')).toHaveLength(3)
|
|
114
|
-
expect([...out].filter((c) => c === '▲')).toHaveLength(2)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('reverses the rows and the arrowheads in BT', () => {
|
|
118
|
-
const tb = ascii('TB')
|
|
119
|
-
const bt = ascii('BT')
|
|
120
|
-
expect([...bt].filter((c) => c === '▲')).toHaveLength(
|
|
121
|
-
[...tb].filter((c) => c === '▼').length,
|
|
122
|
-
)
|
|
123
|
-
expect([...bt].filter((c) => c === '▼')).toHaveLength(
|
|
124
|
-
[...tb].filter((c) => c === '▲').length,
|
|
125
|
-
)
|
|
126
|
-
expect(bt.indexOf('[Person]')).toBeGreaterThan(
|
|
127
|
-
bt.indexOf('[Software System]'),
|
|
128
|
-
)
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it('flips the contents of a boundary too in BT', () => {
|
|
132
|
-
const rows = ascii('BT').split('\n')
|
|
133
|
-
const at = (re: RegExp) => rows.findIndex((l) => re.test(l))
|
|
134
|
-
// B -> C flows upward, so C sits above B inside the frame.
|
|
135
|
-
expect(at(/│ +C +│/)).toBeLessThan(at(/│ +B +│/))
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it.each(['TB', 'BT'] as const)(
|
|
139
|
-
'keeps every relationship label in %s, wrapped if it must be',
|
|
140
|
-
(dir) => {
|
|
141
|
-
const out = ascii(dir)
|
|
142
|
-
for (const word of [
|
|
143
|
-
'Uses',
|
|
144
|
-
'[HTTPS]',
|
|
145
|
-
'Reads',
|
|
146
|
-
'from',
|
|
147
|
-
'and',
|
|
148
|
-
'writes',
|
|
149
|
-
'to',
|
|
150
|
-
'Sends',
|
|
151
|
-
'Syncs',
|
|
152
|
-
]) {
|
|
153
|
-
expect(out, word).toContain(word)
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
it('is top-to-bottom only: LR and RL render exactly like TB', () => {
|
|
159
|
-
expect(ascii('LR')).toBe(ascii('TB'))
|
|
160
|
-
expect(ascii('RL')).toBe(ascii('TB'))
|
|
161
|
-
})
|
|
162
|
-
})
|