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,264 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for issue #181 — semicolon-separated statements.
|
|
3
|
-
*
|
|
4
|
-
* `detectDiagramType` isolated the header by splitting on `[\n;]`, so
|
|
5
|
-
* `sequenceDiagram;A->>B: Hi` routed to the sequence pipeline — but every
|
|
6
|
-
* parser then split the body on newlines only, discarding everything after
|
|
7
|
-
* the header and rendering an empty diagram. The same gap existed on the
|
|
8
|
-
* flowchart path, where `flowchart TD;A-->B` instead threw
|
|
9
|
-
* "Invalid mermaid header".
|
|
10
|
-
*
|
|
11
|
-
* `splitStatements` is now the single definition of "what is one statement",
|
|
12
|
-
* shared by the detector and all five parser entry points.
|
|
13
|
-
*/
|
|
14
|
-
import { describe, it, expect } from 'vitest'
|
|
15
|
-
import { splitStatements, detectDiagramType } from '@zombie-mermaid/core'
|
|
16
|
-
import { renderMermaidASCII, renderMermaidSVG } from '../index.ts'
|
|
17
|
-
|
|
18
|
-
/** Project a `splitStatements` result down to just the text, for assertions
|
|
19
|
-
* that don't care about position (see the dedicated `line` tests below for
|
|
20
|
-
* position coverage). */
|
|
21
|
-
const texts = (source: string) => splitStatements(source).map((s) => s.text)
|
|
22
|
-
|
|
23
|
-
describe('splitStatements (issue #181)', () => {
|
|
24
|
-
it('splits on newlines', () => {
|
|
25
|
-
expect(texts('graph TD\nA-->B')).toEqual(['graph TD', 'A-->B'])
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('splits on semicolons', () => {
|
|
29
|
-
expect(texts('graph TD;A-->B;B-->C')).toEqual([
|
|
30
|
-
'graph TD',
|
|
31
|
-
'A-->B',
|
|
32
|
-
'B-->C',
|
|
33
|
-
])
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('drops empty statements from trailing or repeated separators', () => {
|
|
37
|
-
expect(texts('graph TD;A-->B;')).toEqual(['graph TD', 'A-->B'])
|
|
38
|
-
expect(texts('graph TD;;;A-->B')).toEqual(['graph TD', 'A-->B'])
|
|
39
|
-
expect(texts('graph TD\n\n\nA-->B')).toEqual(['graph TD', 'A-->B'])
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('trims whitespace around each statement', () => {
|
|
43
|
-
expect(texts(' graph TD ; A-->B ')).toEqual(['graph TD', 'A-->B'])
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
describe('does not split a semicolon that is not a separator', () => {
|
|
47
|
-
it('inside a double-quoted label', () => {
|
|
48
|
-
expect(texts('graph TD\nA["a; b"]-->B')).toEqual([
|
|
49
|
-
'graph TD',
|
|
50
|
-
'A["a; b"]-->B',
|
|
51
|
-
])
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('inside a single-quoted label', () => {
|
|
55
|
-
expect(texts("graph TD\nA['x; y']-->B")).toEqual([
|
|
56
|
-
'graph TD',
|
|
57
|
-
"A['x; y']-->B",
|
|
58
|
-
])
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('terminating a named character reference', () => {
|
|
62
|
-
expect(texts('graph TD\nA[&]-->B')).toEqual([
|
|
63
|
-
'graph TD',
|
|
64
|
-
'A[&]-->B',
|
|
65
|
-
])
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('terminating a numeric character reference', () => {
|
|
69
|
-
expect(texts('graph TD\nA[#]-->B')).toEqual([
|
|
70
|
-
'graph TD',
|
|
71
|
-
'A[#]-->B',
|
|
72
|
-
])
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('terminating a hex character reference', () => {
|
|
76
|
-
expect(texts('graph TD\nA[😀]-->B')).toEqual([
|
|
77
|
-
'graph TD',
|
|
78
|
-
'A[😀]-->B',
|
|
79
|
-
])
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('but still splits a real separator on the same line as an entity', () => {
|
|
83
|
-
expect(texts('graph TD;A[&]-->B;B-->C')).toEqual([
|
|
84
|
-
'graph TD',
|
|
85
|
-
'A[&]-->B',
|
|
86
|
-
'B-->C',
|
|
87
|
-
])
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('and treats a lone ampersand as an ordinary character', () => {
|
|
91
|
-
// `A & B` is flowchart's parallel-link syntax, not an entity.
|
|
92
|
-
expect(texts('graph TD;A-->B & C')).toEqual(['graph TD', 'A-->B & C'])
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
describe('comments', () => {
|
|
97
|
-
it('drops whole-line comments without splitting their semicolons', () => {
|
|
98
|
-
expect(texts('graph TD\n%% note; not code\nA-->B')).toEqual([
|
|
99
|
-
'graph TD',
|
|
100
|
-
'A-->B',
|
|
101
|
-
])
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('drops a comment that only begins after a semicolon', () => {
|
|
105
|
-
expect(texts('graph TD\nA-->B; %% note')).toEqual(['graph TD', 'A-->B'])
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('lets the comment swallow the rest of the line', () => {
|
|
109
|
-
/*
|
|
110
|
-
* A Mermaid comment runs to end of line, so a `;` inside it must not
|
|
111
|
-
* resurrect what follows. Splitting first and then discarding fragments
|
|
112
|
-
* that merely begin with `%%` left `C-->D` parsing as code.
|
|
113
|
-
*/
|
|
114
|
-
expect(texts('graph TD\nA-->B; %% note; C-->D')).toEqual([
|
|
115
|
-
'graph TD',
|
|
116
|
-
'A-->B',
|
|
117
|
-
])
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('treats %% inside a quoted label as text, not a comment', () => {
|
|
121
|
-
expect(texts('graph TD\nA["100%% done"]-->B')).toEqual([
|
|
122
|
-
'graph TD',
|
|
123
|
-
'A["100%% done"]-->B',
|
|
124
|
-
])
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
describe("Mermaid's own #-prefixed character references", () => {
|
|
129
|
-
/*
|
|
130
|
-
* Mermaid spells character references with `#` where HTML uses `&` —
|
|
131
|
-
* `#59;` is the documented way to put a literal semicolon in a label.
|
|
132
|
-
* Treating that `;` as a separator both corrupts the label and invents a
|
|
133
|
-
* statement from the remainder.
|
|
134
|
-
*/
|
|
135
|
-
it.each([
|
|
136
|
-
['A[semi #59; here]-->B', 'a numeric reference'],
|
|
137
|
-
['A[heart #9829; x]-->B', 'a symbol reference'],
|
|
138
|
-
['A[say #quot;hi#quot;]-->B', 'a named reference'],
|
|
139
|
-
])('keeps %s intact (%s)', (statement) => {
|
|
140
|
-
expect(texts(`graph TD\n ${statement}`)).toEqual(['graph TD', statement])
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('still splits a real separator on the same line as one', () => {
|
|
144
|
-
expect(texts('graph TD;A[#59;]-->B;B-->C')).toEqual([
|
|
145
|
-
'graph TD',
|
|
146
|
-
'A[#59;]-->B',
|
|
147
|
-
'B-->C',
|
|
148
|
-
])
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('leaves a bare # that is not a reference alone', () => {
|
|
152
|
-
expect(texts('graph TD;A[C# lang]-->B')).toEqual([
|
|
153
|
-
'graph TD',
|
|
154
|
-
'A[C# lang]-->B',
|
|
155
|
-
])
|
|
156
|
-
})
|
|
157
|
-
})
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
describe('splitStatements — line numbers (issue #760)', () => {
|
|
161
|
-
it('reports the 1-based physical source line for each newline-separated statement', () => {
|
|
162
|
-
const stmts = splitStatements('graph TD\nA-->B\nB-->C')
|
|
163
|
-
expect(stmts.map((s) => s.line)).toEqual([1, 2, 3])
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('carries forward the original line number across dropped blank lines', () => {
|
|
167
|
-
const stmts = splitStatements('graph TD\n\n\nA-->B')
|
|
168
|
-
expect(stmts.map((s) => ({ text: s.text, line: s.line }))).toEqual([
|
|
169
|
-
{ text: 'graph TD', line: 1 },
|
|
170
|
-
{ text: 'A-->B', line: 4 },
|
|
171
|
-
])
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('carries forward the original line number across a dropped comment line', () => {
|
|
175
|
-
const stmts = splitStatements('graph TD\n%% a comment\nA-->B')
|
|
176
|
-
expect(stmts.map((s) => ({ text: s.text, line: s.line }))).toEqual([
|
|
177
|
-
{ text: 'graph TD', line: 1 },
|
|
178
|
-
{ text: 'A-->B', line: 3 },
|
|
179
|
-
])
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('gives every semicolon-separated statement on one physical line the same line number', () => {
|
|
183
|
-
const stmts = splitStatements('graph TD\nA-->B;B-->C;C-->D')
|
|
184
|
-
expect(stmts.map((s) => s.line)).toEqual([1, 2, 2, 2])
|
|
185
|
-
})
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
describe('semicolon-separated diagrams render their bodies (issue #181)', () => {
|
|
189
|
-
const cases = [
|
|
190
|
-
{
|
|
191
|
-
type: 'flowchart' as const,
|
|
192
|
-
// Previously threw: the header regex saw the whole line.
|
|
193
|
-
source: 'flowchart TD;A-->B;B-->C',
|
|
194
|
-
expected: ['A', 'B', 'C'],
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
type: 'sequence' as const,
|
|
198
|
-
source: 'sequenceDiagram;A->>B: Hi',
|
|
199
|
-
expected: ['A', 'B', 'Hi'],
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
type: 'class' as const,
|
|
203
|
-
source: 'classDiagram;class Animal;class Dog',
|
|
204
|
-
expected: ['Animal', 'Dog'],
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
type: 'er' as const,
|
|
208
|
-
source: 'erDiagram;CUSTOMER ||--o{ ORDER : places',
|
|
209
|
-
expected: ['CUSTOMER', 'ORDER', 'places'],
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
type: 'xychart' as const,
|
|
213
|
-
source: 'xychart-beta;title "Sales";bar [1, 2, 3]',
|
|
214
|
-
expected: ['Sales'],
|
|
215
|
-
},
|
|
216
|
-
]
|
|
217
|
-
|
|
218
|
-
for (const { type, source, expected } of cases) {
|
|
219
|
-
describe(type, () => {
|
|
220
|
-
it('routes to the expected pipeline', () => {
|
|
221
|
-
expect(detectDiagramType(source)).toBe(type)
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('renders the body to ASCII instead of an empty diagram', () => {
|
|
225
|
-
const ascii = renderMermaidASCII(source, { colorMode: 'none' })
|
|
226
|
-
expect(ascii.trim()).not.toBe('')
|
|
227
|
-
for (const token of expected) expect(ascii).toContain(token)
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('renders the body to SVG instead of an empty diagram', () => {
|
|
231
|
-
const svg = renderMermaidSVG(source)
|
|
232
|
-
for (const token of expected) expect(svg).toContain(token)
|
|
233
|
-
})
|
|
234
|
-
})
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
it('accepts semicolons after a newline-separated header', () => {
|
|
238
|
-
const ascii = renderMermaidASCII('sequenceDiagram\nA->>B: Hi;B->>C: Yo', {
|
|
239
|
-
colorMode: 'none',
|
|
240
|
-
})
|
|
241
|
-
expect(ascii).toContain('Hi')
|
|
242
|
-
expect(ascii).toContain('Yo')
|
|
243
|
-
expect(ascii).toContain('C')
|
|
244
|
-
// The old behavior swallowed the second message into the first label.
|
|
245
|
-
expect(ascii).not.toContain('Hi;B')
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
it('leaves newline-separated sources rendering identically', () => {
|
|
249
|
-
const withNewlines = renderMermaidASCII('flowchart TD\nA-->B\nB-->C', {
|
|
250
|
-
colorMode: 'none',
|
|
251
|
-
})
|
|
252
|
-
const withSemicolons = renderMermaidASCII('flowchart TD;A-->B;B-->C', {
|
|
253
|
-
colorMode: 'none',
|
|
254
|
-
})
|
|
255
|
-
expect(withSemicolons).toBe(withNewlines)
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('preserves a semicolon that is part of a label', () => {
|
|
259
|
-
const ascii = renderMermaidASCII('flowchart TD\nA["a; b"]-->B', {
|
|
260
|
-
colorMode: 'none',
|
|
261
|
-
})
|
|
262
|
-
expect(ascii).toContain('a; b')
|
|
263
|
-
})
|
|
264
|
-
})
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for styles module — text measurement and constants.
|
|
3
|
-
* Theme resolution tests are in theme.test.ts (CSS custom property system).
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it, expect } from 'vitest'
|
|
6
|
-
import {
|
|
7
|
-
estimateTextWidth,
|
|
8
|
-
FONT_SIZES,
|
|
9
|
-
FONT_WEIGHTS,
|
|
10
|
-
NODE_PADDING,
|
|
11
|
-
STROKE_WIDTHS,
|
|
12
|
-
ARROW_HEAD,
|
|
13
|
-
buildSvgStyleBlock,
|
|
14
|
-
} from '@zombie-mermaid/svg-renderer'
|
|
15
|
-
import {
|
|
16
|
-
THEMES,
|
|
17
|
-
DEFAULTS,
|
|
18
|
-
fromShikiTheme,
|
|
19
|
-
buildStyleBlock,
|
|
20
|
-
svgOpenTag,
|
|
21
|
-
} from '@zombie-mermaid/core'
|
|
22
|
-
import type { DiagramColors } from '@zombie-mermaid/core'
|
|
23
|
-
|
|
24
|
-
// ============================================================================
|
|
25
|
-
// Theme system (CSS custom properties)
|
|
26
|
-
// ============================================================================
|
|
27
|
-
|
|
28
|
-
describe('THEMES', () => {
|
|
29
|
-
it('contains well-known theme palettes', () => {
|
|
30
|
-
expect(THEMES['zinc-light']).toBeDefined()
|
|
31
|
-
expect(THEMES['zinc-dark']).toBeDefined()
|
|
32
|
-
expect(THEMES['tokyo-night']).toBeDefined()
|
|
33
|
-
expect(THEMES['catppuccin-mocha']).toBeDefined()
|
|
34
|
-
expect(THEMES['nord']).toBeDefined()
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('each theme has valid bg and fg colors', () => {
|
|
38
|
-
for (const colors of Object.values(THEMES)) {
|
|
39
|
-
expect(colors.bg).toMatch(/^#[0-9a-fA-F]{6}$/)
|
|
40
|
-
expect(colors.fg).toMatch(/^#[0-9a-fA-F]{6}$/)
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
describe('DEFAULTS', () => {
|
|
46
|
-
it('provides zinc-light bg/fg', () => {
|
|
47
|
-
expect(DEFAULTS.bg).toBe('#FFFFFF')
|
|
48
|
-
expect(DEFAULTS.fg).toBe('#27272A')
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
describe('svgOpenTag', () => {
|
|
53
|
-
it('sets --bg and --fg CSS variables in inline style', () => {
|
|
54
|
-
const tag = svgOpenTag(400, 300, { bg: '#1a1b26', fg: '#a9b1d6' })
|
|
55
|
-
expect(tag).toContain('--bg:#1a1b26')
|
|
56
|
-
expect(tag).toContain('--fg:#a9b1d6')
|
|
57
|
-
expect(tag).toContain('background:var(--bg)')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('includes optional enrichment variables when provided', () => {
|
|
61
|
-
const colors: DiagramColors = {
|
|
62
|
-
bg: '#1a1b26',
|
|
63
|
-
fg: '#a9b1d6',
|
|
64
|
-
line: '#3d59a1',
|
|
65
|
-
accent: '#7aa2f7',
|
|
66
|
-
}
|
|
67
|
-
const tag = svgOpenTag(400, 300, colors)
|
|
68
|
-
expect(tag).toContain('--line:#3d59a1')
|
|
69
|
-
expect(tag).toContain('--accent:#7aa2f7')
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('omits unset enrichment variables', () => {
|
|
73
|
-
const tag = svgOpenTag(400, 300, { bg: '#fff', fg: '#000' })
|
|
74
|
-
expect(tag).not.toContain('--line')
|
|
75
|
-
expect(tag).not.toContain('--accent')
|
|
76
|
-
expect(tag).not.toContain('--muted')
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
describe('buildStyleBlock', () => {
|
|
81
|
-
it('includes derived CSS variable declarations', () => {
|
|
82
|
-
const style = buildStyleBlock('Inter', false)
|
|
83
|
-
expect(style).toContain('--_text')
|
|
84
|
-
expect(style).toContain('--_line')
|
|
85
|
-
expect(style).toContain('--_arrow')
|
|
86
|
-
expect(style).toContain('--_node-fill')
|
|
87
|
-
expect(style).toContain('--_node-stroke')
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('includes mono font class when requested', () => {
|
|
91
|
-
const withMono = buildSvgStyleBlock('Inter', true)
|
|
92
|
-
expect(withMono).toContain('.mono')
|
|
93
|
-
expect(withMono).toContain('JetBrains Mono')
|
|
94
|
-
|
|
95
|
-
const withoutMono = buildStyleBlock('Inter', false)
|
|
96
|
-
expect(withoutMono).not.toContain('.mono')
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('buildSvgStyleBlock omits the mono rule and font face when hasMonoFont is false', () => {
|
|
100
|
-
const block = buildSvgStyleBlock('Inter', false)
|
|
101
|
-
expect(block).not.toContain('.mono')
|
|
102
|
-
expect(block).not.toContain('@font-face')
|
|
103
|
-
expect(block).toContain('--_text')
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('embeds the self-hosted mono subset as a base64 woff2 data URI (#1061)', () => {
|
|
107
|
-
const block = buildSvgStyleBlock('Inter', true)
|
|
108
|
-
expect(block).toContain('@font-face')
|
|
109
|
-
expect(block).toContain('data:font/woff2;base64,')
|
|
110
|
-
expect(block).toContain("font-family: 'JetBrains Mono NL';")
|
|
111
|
-
expect(block).toContain(
|
|
112
|
-
" .mono { font-family: 'JetBrains Mono NL', 'SF Mono', 'Fira Code', ui-monospace, monospace; }",
|
|
113
|
-
)
|
|
114
|
-
expect(block).not.toContain('fonts.googleapis.com/css2?family=JetBrains')
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
describe('fromShikiTheme', () => {
|
|
119
|
-
it('extracts bg/fg from editor colors', () => {
|
|
120
|
-
const colors = fromShikiTheme({
|
|
121
|
-
type: 'dark',
|
|
122
|
-
colors: {
|
|
123
|
-
'editor.background': '#1a1b26',
|
|
124
|
-
'editor.foreground': '#a9b1d6',
|
|
125
|
-
},
|
|
126
|
-
})
|
|
127
|
-
expect(colors.bg).toBe('#1a1b26')
|
|
128
|
-
expect(colors.fg).toBe('#a9b1d6')
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it('falls back for missing editor colors', () => {
|
|
132
|
-
const dark = fromShikiTheme({ type: 'dark' })
|
|
133
|
-
expect(dark.bg).toBe('#1e1e1e')
|
|
134
|
-
expect(dark.fg).toBe('#d4d4d4')
|
|
135
|
-
|
|
136
|
-
const light = fromShikiTheme({ type: 'light' })
|
|
137
|
-
expect(light.bg).toBe('#ffffff')
|
|
138
|
-
expect(light.fg).toBe('#333333')
|
|
139
|
-
})
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
// ============================================================================
|
|
143
|
-
// Text width estimation
|
|
144
|
-
// ============================================================================
|
|
145
|
-
|
|
146
|
-
describe('estimateTextWidth', () => {
|
|
147
|
-
it('returns a positive number for non-empty text', () => {
|
|
148
|
-
const width = estimateTextWidth('Hello', 13, 500)
|
|
149
|
-
expect(width).toBeGreaterThan(0)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('returns minimum padding for empty text', () => {
|
|
153
|
-
// Empty text still returns minimum padding (fontSize * 0.15) for layout safety
|
|
154
|
-
expect(estimateTextWidth('', 13, 500)).toBeCloseTo(1.95, 1)
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
it('scales with text length', () => {
|
|
158
|
-
const short = estimateTextWidth('Hi', 13, 500)
|
|
159
|
-
const long = estimateTextWidth('Hello World', 13, 500)
|
|
160
|
-
expect(long).toBeGreaterThan(short)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('scales with font size', () => {
|
|
164
|
-
const small = estimateTextWidth('Text', 11, 500)
|
|
165
|
-
const large = estimateTextWidth('Text', 16, 500)
|
|
166
|
-
expect(large).toBeGreaterThan(small)
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('heavier weights produce wider estimates', () => {
|
|
170
|
-
const regular = estimateTextWidth('Text', 13, 400)
|
|
171
|
-
const bold = estimateTextWidth('Text', 13, 600)
|
|
172
|
-
expect(bold).toBeGreaterThan(regular)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('produces reasonable widths for typical node labels', () => {
|
|
176
|
-
// A 5-character label at 13px/500w should be roughly 35px (5 * 13 * 0.55)
|
|
177
|
-
const width = estimateTextWidth(
|
|
178
|
-
'Hello',
|
|
179
|
-
FONT_SIZES.nodeLabel,
|
|
180
|
-
FONT_WEIGHTS.nodeLabel,
|
|
181
|
-
)
|
|
182
|
-
expect(width).toBeGreaterThan(25)
|
|
183
|
-
expect(width).toBeLessThan(60)
|
|
184
|
-
})
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
// ============================================================================
|
|
188
|
-
// Exported constants
|
|
189
|
-
// ============================================================================
|
|
190
|
-
|
|
191
|
-
describe('constants', () => {
|
|
192
|
-
it('FONT_SIZES has expected values', () => {
|
|
193
|
-
expect(FONT_SIZES.nodeLabel).toBe(13)
|
|
194
|
-
expect(FONT_SIZES.edgeLabel).toBe(11)
|
|
195
|
-
expect(FONT_SIZES.groupHeader).toBe(12)
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('FONT_WEIGHTS has expected values', () => {
|
|
199
|
-
expect(FONT_WEIGHTS.nodeLabel).toBe(500)
|
|
200
|
-
expect(FONT_WEIGHTS.edgeLabel).toBe(400)
|
|
201
|
-
expect(FONT_WEIGHTS.groupHeader).toBe(600)
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
it('NODE_PADDING has expected values', () => {
|
|
205
|
-
expect(NODE_PADDING.horizontal).toBe(20)
|
|
206
|
-
expect(NODE_PADDING.vertical).toBe(10)
|
|
207
|
-
expect(NODE_PADDING.diamondExtra).toBe(24)
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('STROKE_WIDTHS has expected values', () => {
|
|
211
|
-
expect(STROKE_WIDTHS.outerBox).toBe(1)
|
|
212
|
-
expect(STROKE_WIDTHS.innerBox).toBe(0.75)
|
|
213
|
-
expect(STROKE_WIDTHS.connector).toBe(1)
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
it('ARROW_HEAD has expected values', () => {
|
|
217
|
-
expect(ARROW_HEAD.width).toBe(8)
|
|
218
|
-
expect(ARROW_HEAD.height).toBe(5)
|
|
219
|
-
})
|
|
220
|
-
})
|