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,743 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the SVG renderer.
|
|
3
|
-
*
|
|
4
|
-
* Uses hand-crafted PositionedGraph data to test SVG output without
|
|
5
|
-
* depending on the layout engine.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect } from 'vitest'
|
|
8
|
-
import { renderSvg } from '@zombie-mermaid/svg-renderer'
|
|
9
|
-
import type {
|
|
10
|
-
DiagramColors,
|
|
11
|
-
PositionedGraph,
|
|
12
|
-
PositionedNode,
|
|
13
|
-
PositionedEdge,
|
|
14
|
-
PositionedGroup,
|
|
15
|
-
} from '@zombie-mermaid/core'
|
|
16
|
-
|
|
17
|
-
/** Minimal positioned graph for testing */
|
|
18
|
-
function makeGraph(overrides: Partial<PositionedGraph> = {}): PositionedGraph {
|
|
19
|
-
return {
|
|
20
|
-
width: 400,
|
|
21
|
-
height: 300,
|
|
22
|
-
nodes: [],
|
|
23
|
-
edges: [],
|
|
24
|
-
groups: [],
|
|
25
|
-
...overrides,
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Helper to build a positioned node */
|
|
30
|
-
function makeNode(overrides: Partial<PositionedNode> = {}): PositionedNode {
|
|
31
|
-
return {
|
|
32
|
-
id: 'A',
|
|
33
|
-
label: 'Test',
|
|
34
|
-
shape: 'rectangle',
|
|
35
|
-
x: 100,
|
|
36
|
-
y: 100,
|
|
37
|
-
width: 80,
|
|
38
|
-
height: 40,
|
|
39
|
-
...overrides,
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Helper to build a positioned edge with arrow defaults */
|
|
44
|
-
function makeEdge(overrides: Partial<PositionedEdge> = {}): PositionedEdge {
|
|
45
|
-
return {
|
|
46
|
-
source: 'A',
|
|
47
|
-
target: 'B',
|
|
48
|
-
style: 'solid',
|
|
49
|
-
hasArrowStart: false,
|
|
50
|
-
hasArrowEnd: true,
|
|
51
|
-
points: [
|
|
52
|
-
{ x: 100, y: 120 },
|
|
53
|
-
{ x: 100, y: 200 },
|
|
54
|
-
],
|
|
55
|
-
...overrides,
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Default light colors — CSS custom properties handle actual styling */
|
|
60
|
-
const lightColors: DiagramColors = { bg: '#FFFFFF', fg: '#27272A' }
|
|
61
|
-
const darkColors: DiagramColors = { bg: '#18181B', fg: '#FAFAFA' }
|
|
62
|
-
|
|
63
|
-
// ============================================================================
|
|
64
|
-
// SVG structure
|
|
65
|
-
// ============================================================================
|
|
66
|
-
|
|
67
|
-
describe('renderSvg – SVG structure', () => {
|
|
68
|
-
it('produces a valid SVG root element', () => {
|
|
69
|
-
const svg = renderSvg(makeGraph(), lightColors)
|
|
70
|
-
expect(svg).toContain('<svg xmlns="http://www.w3.org/2000/svg"')
|
|
71
|
-
expect(svg).toContain('viewBox="0 0 400 300"')
|
|
72
|
-
expect(svg).toContain('width="400"')
|
|
73
|
-
expect(svg).toContain('height="300"')
|
|
74
|
-
expect(svg).toContain('</svg>')
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('includes <defs> with arrow markers', () => {
|
|
78
|
-
const svg = renderSvg(makeGraph(), lightColors)
|
|
79
|
-
expect(svg).toContain('<defs>')
|
|
80
|
-
expect(svg).toContain('<marker id="arrowhead"')
|
|
81
|
-
expect(svg).toContain('<marker id="arrowhead-start"')
|
|
82
|
-
expect(svg).toContain('</defs>')
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('start marker is not double-reversed (regression)', () => {
|
|
86
|
-
// orient="auto-start-reverse" already rotates the head 180° to point back
|
|
87
|
-
// out of the start node, so the polygon must match the forward (end)
|
|
88
|
-
// marker. Reversing BOTH is a double reversal: the head points into the
|
|
89
|
-
// line and disappears in librsvg/Inkscape/browsers.
|
|
90
|
-
//
|
|
91
|
-
// Extracted with plain string search (not a dynamically-built RegExp)
|
|
92
|
-
// since `id` here is always one of two hardcoded marker ids below, not
|
|
93
|
-
// attacker-controlled input.
|
|
94
|
-
const svg = renderSvg(makeGraph(), lightColors)
|
|
95
|
-
const markerBlock = (id: string): string => {
|
|
96
|
-
const openTag = `<marker id="${id}"`
|
|
97
|
-
const start = svg.indexOf(openTag)
|
|
98
|
-
expect(start).toBeGreaterThanOrEqual(0)
|
|
99
|
-
const end = svg.indexOf('</marker>', start)
|
|
100
|
-
return svg.slice(start, end)
|
|
101
|
-
}
|
|
102
|
-
const attr = (block: string, name: string): string | undefined =>
|
|
103
|
-
block.split(`${name}="`)[1]?.split('"')[0]
|
|
104
|
-
const polygonPoints = (block: string): string | undefined =>
|
|
105
|
-
block.split('<polygon points="')[1]?.split('"')[0]
|
|
106
|
-
|
|
107
|
-
const startBlock = markerBlock('arrowhead-start')
|
|
108
|
-
const endBlock = markerBlock('arrowhead')
|
|
109
|
-
|
|
110
|
-
expect(attr(startBlock, 'orient')).toBe('auto-start-reverse')
|
|
111
|
-
expect(polygonPoints(startBlock)).toBe(polygonPoints(endBlock))
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('includes embedded Google Fonts import', () => {
|
|
115
|
-
const svg = renderSvg(makeGraph(), lightColors, 'Inter')
|
|
116
|
-
expect(svg).toContain('fonts.googleapis.com')
|
|
117
|
-
expect(svg).toContain('Inter')
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('uses custom font name when specified', () => {
|
|
121
|
-
const svg = renderSvg(makeGraph(), lightColors, 'Roboto Mono')
|
|
122
|
-
// encodeURIComponent turns spaces into %20
|
|
123
|
-
expect(svg).toContain('Roboto%20Mono')
|
|
124
|
-
expect(svg).toContain("'Roboto Mono'")
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('sets CSS color variables in inline style', () => {
|
|
128
|
-
const light = renderSvg(makeGraph(), lightColors)
|
|
129
|
-
expect(light).toContain('--bg:#FFFFFF')
|
|
130
|
-
expect(light).toContain('--fg:#27272A')
|
|
131
|
-
|
|
132
|
-
const dark = renderSvg(makeGraph(), darkColors)
|
|
133
|
-
expect(dark).toContain('--bg:#18181B')
|
|
134
|
-
expect(dark).toContain('--fg:#FAFAFA')
|
|
135
|
-
})
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
// ============================================================================
|
|
139
|
-
// Original node shapes
|
|
140
|
-
// ============================================================================
|
|
141
|
-
|
|
142
|
-
describe('renderSvg – node shapes', () => {
|
|
143
|
-
it('renders rectangle with rx=0', () => {
|
|
144
|
-
const graph = makeGraph({ nodes: [makeNode({ shape: 'rectangle' })] })
|
|
145
|
-
const svg = renderSvg(graph, lightColors)
|
|
146
|
-
expect(svg).toContain('rx="0" ry="0"')
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('renders rounded rectangle with rx=6', () => {
|
|
150
|
-
const graph = makeGraph({ nodes: [makeNode({ shape: 'rounded' })] })
|
|
151
|
-
const svg = renderSvg(graph, lightColors)
|
|
152
|
-
expect(svg).toContain('rx="6" ry="6"')
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('renders stadium with rx=height/2', () => {
|
|
156
|
-
const node = makeNode({ shape: 'stadium', height: 40 })
|
|
157
|
-
const graph = makeGraph({ nodes: [node] })
|
|
158
|
-
const svg = renderSvg(graph, lightColors)
|
|
159
|
-
expect(svg).toContain('rx="20" ry="20"')
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('renders circle with <circle> element', () => {
|
|
163
|
-
const node = makeNode({ shape: 'circle', width: 60, height: 60 })
|
|
164
|
-
const graph = makeGraph({ nodes: [node] })
|
|
165
|
-
const svg = renderSvg(graph, lightColors)
|
|
166
|
-
expect(svg).toContain('<circle')
|
|
167
|
-
expect(svg).toContain('r="30"')
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('renders diamond with <polygon>', () => {
|
|
171
|
-
const node = makeNode({ shape: 'diamond', width: 80, height: 80 })
|
|
172
|
-
const graph = makeGraph({ nodes: [node] })
|
|
173
|
-
const svg = renderSvg(graph, lightColors)
|
|
174
|
-
expect(svg).toContain('<polygon')
|
|
175
|
-
expect(svg).toContain('points="140,100 180,140 140,180 100,140"')
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it('renders node labels as <text> elements', () => {
|
|
179
|
-
const graph = makeGraph({ nodes: [makeNode({ label: 'My Node' })] })
|
|
180
|
-
const svg = renderSvg(graph, lightColors)
|
|
181
|
-
expect(svg).toContain('>My Node</text>')
|
|
182
|
-
})
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
// ============================================================================
|
|
186
|
-
// New Batch 1 shapes
|
|
187
|
-
// ============================================================================
|
|
188
|
-
|
|
189
|
-
describe('renderSvg – new shapes (Batch 1)', () => {
|
|
190
|
-
it('renders subroutine with outer rect and inset vertical lines', () => {
|
|
191
|
-
const node = makeNode({ shape: 'subroutine', width: 100, height: 40 })
|
|
192
|
-
const graph = makeGraph({ nodes: [node] })
|
|
193
|
-
const svg = renderSvg(graph, lightColors)
|
|
194
|
-
// Outer rect
|
|
195
|
-
expect(svg).toContain('<rect x="100" y="100" width="100" height="40"')
|
|
196
|
-
// Left inset line at x=108 (100+8)
|
|
197
|
-
expect(svg).toContain('x1="108"')
|
|
198
|
-
// Right inset line at x=192 (100+100-8)
|
|
199
|
-
expect(svg).toContain('x1="192"')
|
|
200
|
-
expect(svg).toContain('<line')
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
it('renders double circle with two <circle> elements', () => {
|
|
204
|
-
const node = makeNode({ shape: 'doublecircle', width: 80, height: 80 })
|
|
205
|
-
const graph = makeGraph({ nodes: [node] })
|
|
206
|
-
const svg = renderSvg(graph, lightColors)
|
|
207
|
-
const circleMatches = svg.match(/<circle/g) ?? []
|
|
208
|
-
expect(circleMatches.length).toBe(2)
|
|
209
|
-
// Outer radius: min(80,80)/2 = 40, inner = 35
|
|
210
|
-
expect(svg).toContain('r="40"')
|
|
211
|
-
expect(svg).toContain('r="35"')
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
it('renders hexagon with 6-point <polygon>', () => {
|
|
215
|
-
const node = makeNode({ shape: 'hexagon', width: 100, height: 40 })
|
|
216
|
-
const graph = makeGraph({ nodes: [node] })
|
|
217
|
-
const svg = renderSvg(graph, lightColors)
|
|
218
|
-
expect(svg).toContain('<polygon')
|
|
219
|
-
// 6 points means 6 coordinate pairs separated by spaces
|
|
220
|
-
const polygonMatch = svg.match(/points="([^"]+)"/)
|
|
221
|
-
const points = polygonMatch?.[1]?.split(' ') ?? []
|
|
222
|
-
expect(points.length).toBe(6)
|
|
223
|
-
})
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
// ============================================================================
|
|
227
|
-
// New Batch 2 shapes
|
|
228
|
-
// ============================================================================
|
|
229
|
-
|
|
230
|
-
describe('renderSvg – new shapes (Batch 2)', () => {
|
|
231
|
-
it('renders cylinder with ellipses and body rect', () => {
|
|
232
|
-
const node = makeNode({ shape: 'cylinder', width: 80, height: 50 })
|
|
233
|
-
const graph = makeGraph({ nodes: [node] })
|
|
234
|
-
const svg = renderSvg(graph, lightColors)
|
|
235
|
-
// Should contain ellipses for top and bottom caps
|
|
236
|
-
const ellipseMatches = svg.match(/<ellipse/g) ?? []
|
|
237
|
-
expect(ellipseMatches.length).toBe(2)
|
|
238
|
-
// Body rect
|
|
239
|
-
expect(svg).toContain('<rect')
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('renders asymmetric / flag with 5-point <polygon>', () => {
|
|
243
|
-
const node = makeNode({ shape: 'asymmetric', width: 100, height: 40 })
|
|
244
|
-
const graph = makeGraph({ nodes: [node] })
|
|
245
|
-
const svg = renderSvg(graph, lightColors)
|
|
246
|
-
expect(svg).toContain('<polygon')
|
|
247
|
-
// Match the shape polygon (comma-separated "x,y" pairs), not the arrowhead polygons
|
|
248
|
-
// Arrowhead polygons use space-separated "x y," format, while shape polygons use "x,y x,y" format
|
|
249
|
-
const allPolygons = [...svg.matchAll(/points="([^"]+)"/g)]
|
|
250
|
-
// The shape polygon is the last one (rendered after defs/arrowheads)
|
|
251
|
-
const shapePolygon = allPolygons[allPolygons.length - 1]
|
|
252
|
-
const points = shapePolygon?.[1]?.split(' ') ?? []
|
|
253
|
-
expect(points.length).toBe(5)
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('renders trapezoid with 4-point <polygon>', () => {
|
|
257
|
-
const node = makeNode({ shape: 'trapezoid', width: 100, height: 40 })
|
|
258
|
-
const graph = makeGraph({ nodes: [node] })
|
|
259
|
-
const svg = renderSvg(graph, lightColors)
|
|
260
|
-
expect(svg).toContain('<polygon')
|
|
261
|
-
const allPolygons = [...svg.matchAll(/points="([^"]+)"/g)]
|
|
262
|
-
const shapePolygon = allPolygons[allPolygons.length - 1]
|
|
263
|
-
const points = shapePolygon?.[1]?.split(' ') ?? []
|
|
264
|
-
expect(points.length).toBe(4)
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
it('renders trapezoid-alt with 4-point <polygon>', () => {
|
|
268
|
-
const node = makeNode({ shape: 'trapezoid-alt', width: 100, height: 40 })
|
|
269
|
-
const graph = makeGraph({ nodes: [node] })
|
|
270
|
-
const svg = renderSvg(graph, lightColors)
|
|
271
|
-
expect(svg).toContain('<polygon')
|
|
272
|
-
const allPolygons = [...svg.matchAll(/points="([^"]+)"/g)]
|
|
273
|
-
const shapePolygon = allPolygons[allPolygons.length - 1]
|
|
274
|
-
const points = shapePolygon?.[1]?.split(' ') ?? []
|
|
275
|
-
expect(points.length).toBe(4)
|
|
276
|
-
})
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
// ============================================================================
|
|
280
|
-
// Batch 3: State diagram pseudostates
|
|
281
|
-
// ============================================================================
|
|
282
|
-
|
|
283
|
-
describe('renderSvg – state pseudostates', () => {
|
|
284
|
-
it('renders state-start as a filled circle', () => {
|
|
285
|
-
const node = makeNode({
|
|
286
|
-
shape: 'state-start',
|
|
287
|
-
label: '',
|
|
288
|
-
width: 28,
|
|
289
|
-
height: 28,
|
|
290
|
-
})
|
|
291
|
-
const graph = makeGraph({ nodes: [node] })
|
|
292
|
-
const svg = renderSvg(graph, lightColors)
|
|
293
|
-
expect(svg).toContain('<circle')
|
|
294
|
-
expect(svg).toContain('fill="var(--_text)"')
|
|
295
|
-
expect(svg).toContain('stroke="none"')
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
it('renders state-end as bullseye (two circles)', () => {
|
|
299
|
-
const node = makeNode({
|
|
300
|
-
shape: 'state-end',
|
|
301
|
-
label: '',
|
|
302
|
-
width: 28,
|
|
303
|
-
height: 28,
|
|
304
|
-
})
|
|
305
|
-
const graph = makeGraph({ nodes: [node] })
|
|
306
|
-
const svg = renderSvg(graph, lightColors)
|
|
307
|
-
const circleMatches = svg.match(/<circle/g) ?? []
|
|
308
|
-
expect(circleMatches.length).toBe(2)
|
|
309
|
-
// Outer is stroked, inner is filled
|
|
310
|
-
expect(svg).toContain('fill="none"')
|
|
311
|
-
expect(svg).toContain('fill="var(--_text)"')
|
|
312
|
-
})
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
// ============================================================================
|
|
316
|
-
// Edge rendering
|
|
317
|
-
// ============================================================================
|
|
318
|
-
|
|
319
|
-
describe('renderSvg – edges', () => {
|
|
320
|
-
it('renders a solid edge as <polyline> with end arrow', () => {
|
|
321
|
-
const edge = makeEdge({ style: 'solid', hasArrowEnd: true })
|
|
322
|
-
const graph = makeGraph({ edges: [edge] })
|
|
323
|
-
const svg = renderSvg(graph, lightColors)
|
|
324
|
-
expect(svg).toContain('<polyline')
|
|
325
|
-
expect(svg).toContain('points="100,120 100,200"')
|
|
326
|
-
expect(svg).toContain('marker-end="url(#arrowhead)"')
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
it('renders dotted edges with stroke-dasharray', () => {
|
|
330
|
-
const edge = makeEdge({ style: 'dotted' })
|
|
331
|
-
const graph = makeGraph({ edges: [edge] })
|
|
332
|
-
const svg = renderSvg(graph, lightColors)
|
|
333
|
-
expect(svg).toContain('stroke-dasharray="4 4"')
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
it('renders thick edges with doubled stroke width', () => {
|
|
337
|
-
const edge = makeEdge({ style: 'thick' })
|
|
338
|
-
const graph = makeGraph({ edges: [edge] })
|
|
339
|
-
const svg = renderSvg(graph, lightColors)
|
|
340
|
-
// Base connector stroke is 1px, thick is doubled to 2px
|
|
341
|
-
expect(svg).toContain('stroke-width="2"')
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
it('does not add dasharray to solid edges', () => {
|
|
345
|
-
const edge = makeEdge({ style: 'solid' })
|
|
346
|
-
const graph = makeGraph({ edges: [edge] })
|
|
347
|
-
const svg = renderSvg(graph, lightColors)
|
|
348
|
-
expect(svg).not.toContain('dasharray')
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
it('skips edges with fewer than 2 points', () => {
|
|
352
|
-
const edge = makeEdge({ points: [{ x: 0, y: 0 }] })
|
|
353
|
-
const graph = makeGraph({ edges: [edge] })
|
|
354
|
-
const svg = renderSvg(graph, lightColors)
|
|
355
|
-
expect(svg).not.toContain('<polyline')
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
it('renders no-arrow edge without marker-end', () => {
|
|
359
|
-
const edge = makeEdge({ hasArrowEnd: false, hasArrowStart: false })
|
|
360
|
-
const graph = makeGraph({ edges: [edge] })
|
|
361
|
-
const svg = renderSvg(graph, lightColors)
|
|
362
|
-
expect(svg).toContain('<polyline')
|
|
363
|
-
expect(svg).not.toContain('marker-end')
|
|
364
|
-
expect(svg).not.toContain('marker-start')
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
it('renders bidirectional edge with both markers', () => {
|
|
368
|
-
const edge = makeEdge({ hasArrowStart: true, hasArrowEnd: true })
|
|
369
|
-
const graph = makeGraph({ edges: [edge] })
|
|
370
|
-
const svg = renderSvg(graph, lightColors)
|
|
371
|
-
expect(svg).toContain('marker-end="url(#arrowhead)"')
|
|
372
|
-
expect(svg).toContain('marker-start="url(#arrowhead-start)"')
|
|
373
|
-
})
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
// ============================================================================
|
|
377
|
-
// Edge labels
|
|
378
|
-
// ============================================================================
|
|
379
|
-
|
|
380
|
-
describe('renderSvg – edge labels', () => {
|
|
381
|
-
it('renders edge label with background pill', () => {
|
|
382
|
-
const edge = makeEdge({ label: 'Yes' })
|
|
383
|
-
const graph = makeGraph({ edges: [edge] })
|
|
384
|
-
const svg = renderSvg(graph, lightColors)
|
|
385
|
-
expect(svg).toContain('>Yes</text>')
|
|
386
|
-
expect(svg).toContain('rx="2" ry="2"')
|
|
387
|
-
})
|
|
388
|
-
|
|
389
|
-
it('does not render label elements for edges without labels', () => {
|
|
390
|
-
const edge = makeEdge({ label: undefined })
|
|
391
|
-
const graph = makeGraph({ edges: [edge] })
|
|
392
|
-
const svg = renderSvg(graph, lightColors)
|
|
393
|
-
const textMatches = svg.match(/<text[^>]*>.*?<\/text>/g) ?? []
|
|
394
|
-
expect(textMatches).toHaveLength(0)
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
it('uses labelPosition when provided instead of edge midpoint', () => {
|
|
398
|
-
// Edge midpoint would be at (100, 160) given these points.
|
|
399
|
-
// labelPosition overrides to (50, 80) — verify the SVG uses that coordinate.
|
|
400
|
-
const edge = makeEdge({
|
|
401
|
-
label: 'Go',
|
|
402
|
-
points: [
|
|
403
|
-
{ x: 100, y: 120 },
|
|
404
|
-
{ x: 100, y: 200 },
|
|
405
|
-
],
|
|
406
|
-
labelPosition: { x: 50, y: 80 },
|
|
407
|
-
})
|
|
408
|
-
const graph = makeGraph({ edges: [edge] })
|
|
409
|
-
const svg = renderSvg(graph, lightColors)
|
|
410
|
-
|
|
411
|
-
// The label text should be centered at the labelPosition (x=50, y=80)
|
|
412
|
-
expect(svg).toContain('x="50" y="80"')
|
|
413
|
-
// The midpoint y=160 should NOT appear in any label-related element
|
|
414
|
-
expect(svg).not.toContain('y="160"')
|
|
415
|
-
})
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
// ============================================================================
|
|
419
|
-
// Group rendering (subgraphs)
|
|
420
|
-
// ============================================================================
|
|
421
|
-
|
|
422
|
-
describe('renderSvg – groups', () => {
|
|
423
|
-
it('renders group with outer rectangle and header band', () => {
|
|
424
|
-
const group: PositionedGroup = {
|
|
425
|
-
id: 'sg1',
|
|
426
|
-
label: 'Backend',
|
|
427
|
-
x: 20,
|
|
428
|
-
y: 20,
|
|
429
|
-
width: 200,
|
|
430
|
-
height: 150,
|
|
431
|
-
children: [],
|
|
432
|
-
}
|
|
433
|
-
const graph = makeGraph({ groups: [group] })
|
|
434
|
-
const svg = renderSvg(graph, lightColors)
|
|
435
|
-
const rectCount = (svg.match(/x="20" y="20"/g) ?? []).length
|
|
436
|
-
expect(rectCount).toBeGreaterThanOrEqual(2)
|
|
437
|
-
expect(svg).toContain('>Backend</text>')
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
it('renders nested groups recursively', () => {
|
|
441
|
-
const inner: PositionedGroup = {
|
|
442
|
-
id: 'inner',
|
|
443
|
-
label: 'Inner',
|
|
444
|
-
x: 40,
|
|
445
|
-
y: 60,
|
|
446
|
-
width: 120,
|
|
447
|
-
height: 80,
|
|
448
|
-
children: [],
|
|
449
|
-
}
|
|
450
|
-
const outer: PositionedGroup = {
|
|
451
|
-
id: 'outer',
|
|
452
|
-
label: 'Outer',
|
|
453
|
-
x: 20,
|
|
454
|
-
y: 20,
|
|
455
|
-
width: 200,
|
|
456
|
-
height: 150,
|
|
457
|
-
children: [inner],
|
|
458
|
-
}
|
|
459
|
-
const graph = makeGraph({ groups: [outer] })
|
|
460
|
-
const svg = renderSvg(graph, lightColors)
|
|
461
|
-
expect(svg).toContain('>Outer</text>')
|
|
462
|
-
expect(svg).toContain('>Inner</text>')
|
|
463
|
-
})
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
// ============================================================================
|
|
467
|
-
// Inline style support
|
|
468
|
-
// ============================================================================
|
|
469
|
-
|
|
470
|
-
describe('renderSvg – inline styles', () => {
|
|
471
|
-
it('applies inline fill override', () => {
|
|
472
|
-
const node = makeNode({ inlineStyle: { fill: '#ff0000' } })
|
|
473
|
-
const graph = makeGraph({ nodes: [node] })
|
|
474
|
-
const svg = renderSvg(graph, lightColors)
|
|
475
|
-
expect(svg).toContain('fill="#ff0000"')
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
it('applies inline stroke override', () => {
|
|
479
|
-
const node = makeNode({ inlineStyle: { stroke: '#00ff00' } })
|
|
480
|
-
const graph = makeGraph({ nodes: [node] })
|
|
481
|
-
const svg = renderSvg(graph, lightColors)
|
|
482
|
-
expect(svg).toContain('stroke="#00ff00"')
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
it('applies inline text color override', () => {
|
|
486
|
-
const node = makeNode({ inlineStyle: { color: '#0000ff' } })
|
|
487
|
-
const graph = makeGraph({ nodes: [node] })
|
|
488
|
-
const svg = renderSvg(graph, lightColors)
|
|
489
|
-
expect(svg).toContain('fill="#0000ff"')
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
it('falls back to theme when no inline style', () => {
|
|
493
|
-
const node = makeNode()
|
|
494
|
-
const graph = makeGraph({ nodes: [node] })
|
|
495
|
-
const svg = renderSvg(graph, lightColors)
|
|
496
|
-
expect(svg).toContain('fill="var(--_node-fill)"')
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
it('applies inline font-family override to the node text element (issue #57)', () => {
|
|
500
|
-
const node = makeNode({ inlineStyle: { 'font-family': 'monospace' } })
|
|
501
|
-
const graph = makeGraph({ nodes: [node] })
|
|
502
|
-
const svg = renderSvg(graph, lightColors)
|
|
503
|
-
expect(svg).toContain('style="font-family: monospace;"')
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
it('renders no per-node font-family override when no inline style is set', () => {
|
|
507
|
-
const node = makeNode()
|
|
508
|
-
const graph = makeGraph({ nodes: [node] })
|
|
509
|
-
const svg = renderSvg(graph, lightColors)
|
|
510
|
-
// The global `text { font-family: ... }` rule from the theme is still
|
|
511
|
-
// present; only the per-node inline `style="font-family: ...` override
|
|
512
|
-
// should be absent.
|
|
513
|
-
expect(svg).not.toContain('style="font-family:')
|
|
514
|
-
})
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
// ============================================================================
|
|
518
|
-
// Fill → text color contrast (issue #55)
|
|
519
|
-
// ============================================================================
|
|
520
|
-
|
|
521
|
-
describe('renderSvg – readable text color for custom fills', () => {
|
|
522
|
-
it('picks dark text for a light pastel fill in dark mode', () => {
|
|
523
|
-
// Matches the issue #55 repro: classDef external fill:#FF6B6B in dark mode.
|
|
524
|
-
const node = makeNode({ inlineStyle: { fill: '#FF6B6B' } })
|
|
525
|
-
const graph = makeGraph({ nodes: [node] })
|
|
526
|
-
const svg = renderSvg(graph, darkColors)
|
|
527
|
-
expect(svg).toContain('fill="#000000"')
|
|
528
|
-
expect(svg).not.toContain('fill="var(--_text)"')
|
|
529
|
-
})
|
|
530
|
-
|
|
531
|
-
it('picks dark text for a light green fill in dark mode', () => {
|
|
532
|
-
const node = makeNode({ inlineStyle: { fill: '#90EE90' } })
|
|
533
|
-
const graph = makeGraph({ nodes: [node] })
|
|
534
|
-
const svg = renderSvg(graph, darkColors)
|
|
535
|
-
expect(svg).toContain('fill="#000000"')
|
|
536
|
-
})
|
|
537
|
-
|
|
538
|
-
it('picks light text for a dark fill in light mode', () => {
|
|
539
|
-
const node = makeNode({ inlineStyle: { fill: '#111111' } })
|
|
540
|
-
const graph = makeGraph({ nodes: [node] })
|
|
541
|
-
const svg = renderSvg(graph, lightColors)
|
|
542
|
-
expect(svg).toContain('fill="#FFFFFF"')
|
|
543
|
-
expect(svg).not.toContain('fill="var(--_text)"')
|
|
544
|
-
})
|
|
545
|
-
|
|
546
|
-
it('keeps using the theme foreground when there is no custom fill', () => {
|
|
547
|
-
const node = makeNode()
|
|
548
|
-
const graph = makeGraph({ nodes: [node] })
|
|
549
|
-
const svg = renderSvg(graph, darkColors)
|
|
550
|
-
expect(svg).toContain('fill="var(--_text)"')
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
it('keeps using the theme foreground when the fill is unresolvable (CSS variable reference)', () => {
|
|
554
|
-
const node = makeNode({
|
|
555
|
-
inlineStyle: { fill: 'var(--some-other-var)' },
|
|
556
|
-
})
|
|
557
|
-
const graph = makeGraph({ nodes: [node] })
|
|
558
|
-
const svg = renderSvg(graph, darkColors)
|
|
559
|
-
expect(svg).toContain('fill="var(--_text)"')
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
it('keeps using the theme foreground when the fill is a named CSS color, not a hex literal', () => {
|
|
563
|
-
const node = makeNode({ inlineStyle: { fill: 'lightpink' } })
|
|
564
|
-
const graph = makeGraph({ nodes: [node] })
|
|
565
|
-
const svg = renderSvg(graph, darkColors)
|
|
566
|
-
expect(svg).toContain('fill="var(--_text)"')
|
|
567
|
-
})
|
|
568
|
-
|
|
569
|
-
it('still respects an explicit inline color override even with a custom fill', () => {
|
|
570
|
-
const node = makeNode({
|
|
571
|
-
inlineStyle: { fill: '#FF6B6B', color: '#0000ff' },
|
|
572
|
-
})
|
|
573
|
-
const graph = makeGraph({ nodes: [node] })
|
|
574
|
-
const svg = renderSvg(graph, darkColors)
|
|
575
|
-
expect(svg).toContain('fill="#0000ff"')
|
|
576
|
-
expect(svg).not.toContain('fill="#000000"')
|
|
577
|
-
})
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
// ============================================================================
|
|
581
|
-
// className (from :::className shorthand or `class A className`)
|
|
582
|
-
// ============================================================================
|
|
583
|
-
|
|
584
|
-
describe('renderSvg – custom class names', () => {
|
|
585
|
-
it('emits the class name onto the rendered node alongside the base class', () => {
|
|
586
|
-
const node = makeNode({ className: 'highlight' })
|
|
587
|
-
const graph = makeGraph({ nodes: [node] })
|
|
588
|
-
const svg = renderSvg(graph, lightColors)
|
|
589
|
-
expect(svg).toContain('<g class="node highlight" data-id="A"')
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
it('renders only the base class when the node has no custom class', () => {
|
|
593
|
-
const node = makeNode()
|
|
594
|
-
const graph = makeGraph({ nodes: [node] })
|
|
595
|
-
const svg = renderSvg(graph, lightColors)
|
|
596
|
-
expect(svg).toContain('<g class="node" data-id="A"')
|
|
597
|
-
expect(svg).not.toMatch(/<g class="node [^"]/)
|
|
598
|
-
})
|
|
599
|
-
|
|
600
|
-
it('drops a class name that is not a valid CSS identifier rather than emitting it raw', () => {
|
|
601
|
-
const node = makeNode({ className: 'evil" onmouseover="alert(1)' })
|
|
602
|
-
const graph = makeGraph({ nodes: [node] })
|
|
603
|
-
const svg = renderSvg(graph, lightColors)
|
|
604
|
-
expect(svg).toContain('<g class="node" data-id="A"')
|
|
605
|
-
expect(svg).not.toContain('onmouseover')
|
|
606
|
-
})
|
|
607
|
-
})
|
|
608
|
-
|
|
609
|
-
// ============================================================================
|
|
610
|
-
// XML escaping
|
|
611
|
-
// ============================================================================
|
|
612
|
-
|
|
613
|
-
describe('renderSvg – XML escaping', () => {
|
|
614
|
-
it('escapes special characters in node labels', () => {
|
|
615
|
-
const node = makeNode({ label: '<script> & "quotes" \'apos\'' })
|
|
616
|
-
const graph = makeGraph({ nodes: [node] })
|
|
617
|
-
const svg = renderSvg(graph, lightColors)
|
|
618
|
-
expect(svg).toContain('<script>')
|
|
619
|
-
expect(svg).toContain('&')
|
|
620
|
-
expect(svg).toContain('"quotes"')
|
|
621
|
-
expect(svg).toContain(''apos'')
|
|
622
|
-
})
|
|
623
|
-
|
|
624
|
-
it('escapes special characters in edge labels', () => {
|
|
625
|
-
const edge = makeEdge({ label: 'A & B > C' })
|
|
626
|
-
const graph = makeGraph({ edges: [edge] })
|
|
627
|
-
const svg = renderSvg(graph, lightColors)
|
|
628
|
-
expect(svg).toContain('A & B > C')
|
|
629
|
-
})
|
|
630
|
-
|
|
631
|
-
it('escapes special characters in group labels', () => {
|
|
632
|
-
const group: PositionedGroup = {
|
|
633
|
-
id: 'g1',
|
|
634
|
-
label: 'A < B',
|
|
635
|
-
x: 0,
|
|
636
|
-
y: 0,
|
|
637
|
-
width: 100,
|
|
638
|
-
height: 100,
|
|
639
|
-
children: [],
|
|
640
|
-
}
|
|
641
|
-
const graph = makeGraph({ groups: [group] })
|
|
642
|
-
const svg = renderSvg(graph, lightColors)
|
|
643
|
-
expect(svg).toContain('A < B')
|
|
644
|
-
})
|
|
645
|
-
})
|
|
646
|
-
|
|
647
|
-
// ============================================================================
|
|
648
|
-
// Security: inline style injection prevention
|
|
649
|
-
// ============================================================================
|
|
650
|
-
|
|
651
|
-
describe('renderSvg – inline style XSS prevention', () => {
|
|
652
|
-
it('escapes attribute injection in inline style fill', () => {
|
|
653
|
-
const node = makeNode({
|
|
654
|
-
inlineStyle: { fill: 'red" onmouseover="alert(1)' },
|
|
655
|
-
})
|
|
656
|
-
const graph = makeGraph({ nodes: [node] })
|
|
657
|
-
const svg = renderSvg(graph, lightColors)
|
|
658
|
-
expect(svg).not.toContain('onmouseover="alert')
|
|
659
|
-
expect(svg).toContain('red" onmouseover="alert(1)')
|
|
660
|
-
})
|
|
661
|
-
|
|
662
|
-
it('escapes element injection in inline style fill', () => {
|
|
663
|
-
const node = makeNode({
|
|
664
|
-
inlineStyle: { fill: 'red"/><svg onload="alert(1)"><rect fill="x' },
|
|
665
|
-
})
|
|
666
|
-
const graph = makeGraph({ nodes: [node] })
|
|
667
|
-
const svg = renderSvg(graph, lightColors)
|
|
668
|
-
expect(svg).not.toContain('<svg onload')
|
|
669
|
-
expect(svg).toContain('<svg onload=')
|
|
670
|
-
})
|
|
671
|
-
|
|
672
|
-
it('escapes injection in inline style stroke', () => {
|
|
673
|
-
const node = makeNode({
|
|
674
|
-
inlineStyle: { stroke: 'blue" onclick="alert(1)' },
|
|
675
|
-
})
|
|
676
|
-
const graph = makeGraph({ nodes: [node] })
|
|
677
|
-
const svg = renderSvg(graph, lightColors)
|
|
678
|
-
expect(svg).not.toContain('onclick="alert')
|
|
679
|
-
expect(svg).toContain('blue" onclick="alert(1)')
|
|
680
|
-
})
|
|
681
|
-
|
|
682
|
-
it('escapes injection in inline style stroke-width', () => {
|
|
683
|
-
const node = makeNode({
|
|
684
|
-
inlineStyle: { 'stroke-width': '2" onmouseover="alert(1)' },
|
|
685
|
-
})
|
|
686
|
-
const graph = makeGraph({ nodes: [node] })
|
|
687
|
-
const svg = renderSvg(graph, lightColors)
|
|
688
|
-
expect(svg).not.toContain('onmouseover="alert')
|
|
689
|
-
expect(svg).toContain('2" onmouseover="alert(1)')
|
|
690
|
-
})
|
|
691
|
-
|
|
692
|
-
it('escapes injection in inline style color', () => {
|
|
693
|
-
const node = makeNode({
|
|
694
|
-
inlineStyle: { color: 'green" onfocus="alert(1)' },
|
|
695
|
-
})
|
|
696
|
-
const graph = makeGraph({ nodes: [node] })
|
|
697
|
-
const svg = renderSvg(graph, lightColors)
|
|
698
|
-
expect(svg).not.toContain('onfocus="alert')
|
|
699
|
-
expect(svg).toContain('green" onfocus="alert(1)')
|
|
700
|
-
})
|
|
701
|
-
|
|
702
|
-
it('escapes injection in inline style font-family', () => {
|
|
703
|
-
const node = makeNode({
|
|
704
|
-
inlineStyle: { 'font-family': 'monospace" onmouseover="alert(1)' },
|
|
705
|
-
})
|
|
706
|
-
const graph = makeGraph({ nodes: [node] })
|
|
707
|
-
const svg = renderSvg(graph, lightColors)
|
|
708
|
-
expect(svg).not.toContain('onmouseover="alert')
|
|
709
|
-
expect(svg).toContain('monospace" onmouseover="alert(1)')
|
|
710
|
-
})
|
|
711
|
-
})
|
|
712
|
-
|
|
713
|
-
// ============================================================================
|
|
714
|
-
// Theme application
|
|
715
|
-
// ============================================================================
|
|
716
|
-
|
|
717
|
-
describe('renderSvg – CSS variable theming', () => {
|
|
718
|
-
it('uses CSS variables for styling (light colors)', () => {
|
|
719
|
-
const node = makeNode()
|
|
720
|
-
const graph = makeGraph({ nodes: [node] })
|
|
721
|
-
const svg = renderSvg(graph, lightColors)
|
|
722
|
-
// SVG uses CSS custom property references, not hardcoded colors
|
|
723
|
-
expect(svg).toContain('var(--_node-fill)')
|
|
724
|
-
expect(svg).toContain('var(--_node-stroke)')
|
|
725
|
-
expect(svg).toContain('var(--_text)')
|
|
726
|
-
})
|
|
727
|
-
|
|
728
|
-
it('uses same CSS variables with dark colors', () => {
|
|
729
|
-
const node = makeNode()
|
|
730
|
-
const graph = makeGraph({ nodes: [node] })
|
|
731
|
-
const svg = renderSvg(graph, darkColors)
|
|
732
|
-
// Same CSS variable structure — colors differ via --bg/--fg on the SVG tag
|
|
733
|
-
expect(svg).toContain('var(--_node-fill)')
|
|
734
|
-
expect(svg).toContain('var(--_node-stroke)')
|
|
735
|
-
expect(svg).toContain('var(--_text)')
|
|
736
|
-
expect(svg).toContain('--bg:#18181B')
|
|
737
|
-
})
|
|
738
|
-
|
|
739
|
-
it('arrow marker uses CSS variable for fill', () => {
|
|
740
|
-
const svg = renderSvg(makeGraph(), lightColors)
|
|
741
|
-
expect(svg).toContain('fill="var(--_arrow)"')
|
|
742
|
-
})
|
|
743
|
-
})
|