@opendata-ai/openchart-vanilla 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/index.d.ts +327 -0
  2. package/dist/index.js +4745 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/simulation-worker.js +1196 -0
  5. package/package.json +58 -0
  6. package/src/__test-fixtures__/dom.ts +42 -0
  7. package/src/__test-fixtures__/specs.ts +187 -0
  8. package/src/__tests__/edit-events.test.ts +747 -0
  9. package/src/__tests__/events.test.ts +336 -0
  10. package/src/__tests__/export.test.ts +150 -0
  11. package/src/__tests__/mount.test.ts +219 -0
  12. package/src/__tests__/svg-renderer.test.ts +609 -0
  13. package/src/__tests__/table-mount.test.ts +484 -0
  14. package/src/__tests__/tooltip.test.ts +201 -0
  15. package/src/export.ts +105 -0
  16. package/src/graph/__tests__/canvas-renderer.test.ts +704 -0
  17. package/src/graph/__tests__/graph-mount.test.ts +213 -0
  18. package/src/graph/__tests__/interaction.test.ts +205 -0
  19. package/src/graph/__tests__/keyboard.test.ts +653 -0
  20. package/src/graph/__tests__/search.test.ts +88 -0
  21. package/src/graph/__tests__/simulation.test.ts +233 -0
  22. package/src/graph/__tests__/spatial-index.test.ts +142 -0
  23. package/src/graph/__tests__/zoom.test.ts +195 -0
  24. package/src/graph/canvas-renderer.ts +660 -0
  25. package/src/graph/interaction.ts +359 -0
  26. package/src/graph/keyboard.ts +208 -0
  27. package/src/graph/search.ts +50 -0
  28. package/src/graph/simulation-worker-url.ts +30 -0
  29. package/src/graph/simulation-worker.ts +265 -0
  30. package/src/graph/simulation.ts +350 -0
  31. package/src/graph/spatial-index.ts +121 -0
  32. package/src/graph/types.ts +44 -0
  33. package/src/graph/worker-protocol.ts +67 -0
  34. package/src/graph/zoom.ts +104 -0
  35. package/src/graph-mount.ts +675 -0
  36. package/src/index.ts +56 -0
  37. package/src/mount.ts +1639 -0
  38. package/src/renderers/table-cells.ts +444 -0
  39. package/src/resize-observer.ts +46 -0
  40. package/src/svg-renderer.ts +914 -0
  41. package/src/table-keyboard.ts +266 -0
  42. package/src/table-mount.ts +532 -0
  43. package/src/table-renderer.ts +350 -0
  44. package/src/tooltip.ts +120 -0
@@ -0,0 +1,609 @@
1
+ /**
2
+ * SVG renderer integration tests.
3
+ *
4
+ * Tests the full pipeline: spec -> compileChart -> renderChartSVG -> DOM.
5
+ * Verifies that each chart type produces the correct SVG mark elements,
6
+ * and that chart furniture (chrome, axes, legend, gridlines) renders properly.
7
+ */
8
+
9
+ import type { ChartSpec, CompileOptions } from '@opendata-ai/openchart-engine';
10
+ import { compileChart } from '@opendata-ai/openchart-engine';
11
+ import { afterEach, describe, expect, it } from 'vitest';
12
+ import { createContainer } from '../__test-fixtures__/dom';
13
+ import {
14
+ barSpec,
15
+ columnSpec,
16
+ lineSpec,
17
+ multiSeriesBarSpec,
18
+ pieSpec,
19
+ scatterSpec,
20
+ singleSeriesLineSpec,
21
+ } from '../__test-fixtures__/specs';
22
+ import { renderChartSVG } from '../svg-renderer';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Helpers
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const COMPILE_OPTS: CompileOptions = { width: 600, height: 400 };
29
+
30
+ /**
31
+ * Compile a spec and render into a fresh container.
32
+ * Returns both the SVG element and the container for querying.
33
+ */
34
+ function renderSpec(spec: ChartSpec, opts: CompileOptions = COMPILE_OPTS) {
35
+ const container = createContainer(opts.width, opts.height);
36
+ const layout = compileChart(spec, opts);
37
+ const svg = renderChartSVG(layout, container);
38
+ return { svg, container, layout };
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Cleanup
43
+ // ---------------------------------------------------------------------------
44
+
45
+ afterEach(() => {
46
+ document.body.innerHTML = '';
47
+ });
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Line chart marks
51
+ // ---------------------------------------------------------------------------
52
+
53
+ describe('line chart SVG rendering', () => {
54
+ it('renders <path> elements with valid d attribute for each series', () => {
55
+ const { svg } = renderSpec(lineSpec);
56
+ const paths = svg.querySelectorAll('.viz-mark-line path');
57
+ expect(paths.length).toBeGreaterThan(0);
58
+
59
+ for (const path of paths) {
60
+ const d = path.getAttribute('d');
61
+ expect(d).not.toBeNull();
62
+ // d attribute should start with M (moveTo) and contain curve commands
63
+ expect(d).toMatch(/^M/);
64
+ expect(d!.length).toBeGreaterThan(5);
65
+ }
66
+ });
67
+
68
+ it('creates a mark group per series in multi-series line chart', () => {
69
+ const { svg } = renderSpec(lineSpec);
70
+ const lineGroups = svg.querySelectorAll('.viz-mark-line');
71
+ // lineSpec has 2 series (US and UK)
72
+ expect(lineGroups.length).toBe(2);
73
+ });
74
+
75
+ it('each line mark group has a data-mark-id attribute', () => {
76
+ const { svg } = renderSpec(lineSpec);
77
+ const lineGroups = svg.querySelectorAll('.viz-mark-line');
78
+ for (const group of lineGroups) {
79
+ const markId = group.getAttribute('data-mark-id');
80
+ expect(markId).not.toBeNull();
81
+ expect(markId).toMatch(/^line-/);
82
+ }
83
+ });
84
+
85
+ it('each line mark group has a data-series attribute', () => {
86
+ const { svg } = renderSpec(lineSpec);
87
+ const lineGroups = svg.querySelectorAll('.viz-mark-line');
88
+ const seriesNames = new Set<string>();
89
+ for (const group of lineGroups) {
90
+ const series = group.getAttribute('data-series');
91
+ expect(series).not.toBeNull();
92
+ seriesNames.add(series!);
93
+ }
94
+ expect(seriesNames.has('US')).toBe(true);
95
+ expect(seriesNames.has('UK')).toBe(true);
96
+ });
97
+
98
+ it('paths have stroke color and non-zero stroke width', () => {
99
+ const { svg } = renderSpec(lineSpec);
100
+ const paths = svg.querySelectorAll('.viz-mark-line path');
101
+ for (const path of paths) {
102
+ const stroke = path.getAttribute('stroke');
103
+ expect(stroke).not.toBeNull();
104
+ expect(stroke).not.toBe('none');
105
+ const strokeWidth = Number(path.getAttribute('stroke-width'));
106
+ expect(strokeWidth).toBeGreaterThan(0);
107
+ }
108
+ });
109
+ });
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Bar chart marks
113
+ // ---------------------------------------------------------------------------
114
+
115
+ describe('bar chart SVG rendering', () => {
116
+ it('renders <rect> elements for each data point', () => {
117
+ const { svg } = renderSpec(barSpec);
118
+ const rects = svg.querySelectorAll('.viz-mark-rect rect');
119
+ // barSpec has 3 data points
120
+ expect(rects.length).toBe(3);
121
+ });
122
+
123
+ it('rect elements have width and height > 0', () => {
124
+ const { svg } = renderSpec(barSpec);
125
+ const rects = svg.querySelectorAll('.viz-mark-rect rect');
126
+ for (const rect of rects) {
127
+ const width = Number(rect.getAttribute('width'));
128
+ const height = Number(rect.getAttribute('height'));
129
+ expect(width).toBeGreaterThan(0);
130
+ expect(height).toBeGreaterThan(0);
131
+ }
132
+ });
133
+
134
+ it('rect marks have data-mark-id attributes', () => {
135
+ const { svg } = renderSpec(barSpec);
136
+ const markGroups = svg.querySelectorAll('.viz-mark-rect');
137
+ for (const group of markGroups) {
138
+ const markId = group.getAttribute('data-mark-id');
139
+ expect(markId).not.toBeNull();
140
+ expect(markId).toMatch(/^rect-/);
141
+ }
142
+ });
143
+
144
+ it('bar rects are oriented horizontally (width varies, y is categorical)', () => {
145
+ const { svg } = renderSpec(barSpec);
146
+ const rects = svg.querySelectorAll('.viz-mark-rect rect');
147
+ const widths = Array.from(rects).map((r) => Number(r.getAttribute('width')));
148
+ // Different data values should produce different widths
149
+ const uniqueWidths = new Set(widths);
150
+ expect(uniqueWidths.size).toBeGreaterThan(1);
151
+ });
152
+ });
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Column chart marks
156
+ // ---------------------------------------------------------------------------
157
+
158
+ describe('column chart SVG rendering', () => {
159
+ it('renders <rect> elements oriented vertically', () => {
160
+ const { svg } = renderSpec(columnSpec);
161
+ const rects = svg.querySelectorAll('.viz-mark-rect rect');
162
+ expect(rects.length).toBe(3);
163
+ });
164
+
165
+ it('column rects have varying heights (vertical orientation)', () => {
166
+ const { svg } = renderSpec(columnSpec);
167
+ const rects = svg.querySelectorAll('.viz-mark-rect rect');
168
+ const heights = Array.from(rects).map((r) => Number(r.getAttribute('height')));
169
+ // Different revenue values should produce different heights
170
+ const uniqueHeights = new Set(heights);
171
+ expect(uniqueHeights.size).toBeGreaterThan(1);
172
+ // All heights should be positive
173
+ for (const h of heights) {
174
+ expect(h).toBeGreaterThan(0);
175
+ }
176
+ });
177
+
178
+ it('column rects have positive width', () => {
179
+ const { svg } = renderSpec(columnSpec);
180
+ const rects = svg.querySelectorAll('.viz-mark-rect rect');
181
+ for (const rect of rects) {
182
+ const width = Number(rect.getAttribute('width'));
183
+ expect(width).toBeGreaterThan(0);
184
+ }
185
+ });
186
+ });
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Scatter chart marks
190
+ // ---------------------------------------------------------------------------
191
+
192
+ describe('scatter chart SVG rendering', () => {
193
+ it('renders <circle> elements for each data point', () => {
194
+ const { svg } = renderSpec(scatterSpec);
195
+ const circles = svg.querySelectorAll('.viz-mark-point');
196
+ // scatterSpec has 4 data points
197
+ expect(circles.length).toBe(4);
198
+ });
199
+
200
+ it('circles have valid cx, cy, and r attributes', () => {
201
+ const { svg } = renderSpec(scatterSpec);
202
+ const circles = svg.querySelectorAll('.viz-mark-point');
203
+ for (const circle of circles) {
204
+ const cx = Number(circle.getAttribute('cx'));
205
+ const cy = Number(circle.getAttribute('cy'));
206
+ const r = Number(circle.getAttribute('r'));
207
+ expect(cx).toBeTypeOf('number');
208
+ expect(cy).toBeTypeOf('number');
209
+ expect(r).toBeGreaterThan(0);
210
+ // cx and cy should be within the SVG dimensions
211
+ expect(cx).toBeGreaterThanOrEqual(0);
212
+ expect(cy).toBeGreaterThanOrEqual(0);
213
+ }
214
+ });
215
+
216
+ it('scatter marks have data-mark-id attributes', () => {
217
+ const { svg } = renderSpec(scatterSpec);
218
+ const circles = svg.querySelectorAll('.viz-mark-point');
219
+ for (const circle of circles) {
220
+ const markId = circle.getAttribute('data-mark-id');
221
+ expect(markId).not.toBeNull();
222
+ expect(markId).toMatch(/^point-/);
223
+ }
224
+ });
225
+ });
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Pie chart marks
229
+ // ---------------------------------------------------------------------------
230
+
231
+ describe('pie chart SVG rendering', () => {
232
+ it('renders <path> arc segments for each slice', () => {
233
+ const { svg } = renderSpec(pieSpec);
234
+ const arcGroups = svg.querySelectorAll('.viz-mark-arc');
235
+ // pieSpec has 3 categories
236
+ expect(arcGroups.length).toBe(3);
237
+ });
238
+
239
+ it('arc paths have valid d attribute with arc commands', () => {
240
+ const { svg } = renderSpec(pieSpec);
241
+ const paths = svg.querySelectorAll('.viz-mark-arc path');
242
+ for (const path of paths) {
243
+ const d = path.getAttribute('d');
244
+ expect(d).not.toBeNull();
245
+ // Arc paths should contain A (arc) commands
246
+ expect(d).toMatch(/[AaLl]/);
247
+ expect(d!.length).toBeGreaterThan(5);
248
+ }
249
+ });
250
+
251
+ it('arc groups are translated to the pie center', () => {
252
+ const { svg } = renderSpec(pieSpec);
253
+ const arcGroups = svg.querySelectorAll('.viz-mark-arc');
254
+ for (const group of arcGroups) {
255
+ const transform = group.getAttribute('transform');
256
+ expect(transform).not.toBeNull();
257
+ expect(transform).toMatch(/translate\(\d+/);
258
+ }
259
+ });
260
+
261
+ it('arc marks have fill colors', () => {
262
+ const { svg } = renderSpec(pieSpec);
263
+ const paths = svg.querySelectorAll('.viz-mark-arc path');
264
+ for (const path of paths) {
265
+ const fill = path.getAttribute('fill');
266
+ expect(fill).not.toBeNull();
267
+ expect(fill).not.toBe('none');
268
+ }
269
+ });
270
+ });
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Multi-series: correct grouping and distinct colors
274
+ // ---------------------------------------------------------------------------
275
+
276
+ describe('multi-series rendering', () => {
277
+ it('multi-series line chart has distinct stroke colors per series', () => {
278
+ const { svg } = renderSpec(lineSpec);
279
+ const paths = svg.querySelectorAll('.viz-mark-line path');
280
+ const strokes = new Set<string>();
281
+ for (const path of paths) {
282
+ const stroke = path.getAttribute('stroke');
283
+ if (stroke) strokes.add(stroke);
284
+ }
285
+ // US and UK should have different colors
286
+ expect(strokes.size).toBe(2);
287
+ });
288
+
289
+ it('multi-series scatter chart has distinct fill colors per group', () => {
290
+ const { svg } = renderSpec(scatterSpec);
291
+ const circles = svg.querySelectorAll('.viz-mark-point');
292
+ const fills = new Set<string>();
293
+ for (const circle of circles) {
294
+ const fill = circle.getAttribute('fill');
295
+ if (fill) fills.add(fill);
296
+ }
297
+ // group A and B should have different fill colors
298
+ expect(fills.size).toBe(2);
299
+ });
300
+
301
+ it('multi-series bar chart renders data-series attributes on rect marks', () => {
302
+ const { svg } = renderSpec(multiSeriesBarSpec);
303
+ const marks = svg.querySelectorAll('.viz-mark-rect[data-series]');
304
+ const seriesNames = new Set<string>();
305
+ for (const mark of marks) {
306
+ const s = mark.getAttribute('data-series');
307
+ if (s) seriesNames.add(s);
308
+ }
309
+ // Should have marks with series info
310
+ expect(seriesNames.size).toBeGreaterThan(0);
311
+ });
312
+ });
313
+
314
+ // ---------------------------------------------------------------------------
315
+ // Chrome elements (title, subtitle, source)
316
+ // ---------------------------------------------------------------------------
317
+
318
+ describe('chart chrome rendering', () => {
319
+ it('renders title text with correct content', () => {
320
+ const { svg } = renderSpec(lineSpec);
321
+ const title = svg.querySelector('.viz-title');
322
+ expect(title).not.toBeNull();
323
+ expect(title!.textContent).toBe('GDP Growth');
324
+ });
325
+
326
+ it('renders subtitle text with correct content', () => {
327
+ const { svg } = renderSpec(lineSpec);
328
+ const subtitle = svg.querySelector('.viz-subtitle');
329
+ expect(subtitle).not.toBeNull();
330
+ expect(subtitle!.textContent).toBe('US vs UK over time');
331
+ });
332
+
333
+ it('renders source text with correct content', () => {
334
+ const { svg } = renderSpec(lineSpec);
335
+ const source = svg.querySelector('.viz-source');
336
+ expect(source).not.toBeNull();
337
+ expect(source!.textContent).toBe('World Bank');
338
+ });
339
+
340
+ it('chrome elements are inside a .viz-chrome group', () => {
341
+ const { svg } = renderSpec(lineSpec);
342
+ const chromeGroup = svg.querySelector('.viz-chrome');
343
+ expect(chromeGroup).not.toBeNull();
344
+ expect(chromeGroup!.querySelector('.viz-title')).not.toBeNull();
345
+ });
346
+
347
+ it('title has font styling applied', () => {
348
+ const { svg } = renderSpec(lineSpec);
349
+ const title = svg.querySelector('.viz-title');
350
+ expect(title).not.toBeNull();
351
+ const fontFamily = title!.getAttribute('font-family');
352
+ const fontSize = Number(title!.getAttribute('font-size'));
353
+ expect(fontFamily).not.toBeNull();
354
+ expect(fontSize).toBeGreaterThan(0);
355
+ });
356
+
357
+ it('chart with no chrome specified renders no chrome text elements', () => {
358
+ const noChrome: ChartSpec = {
359
+ type: 'bar',
360
+ data: [{ name: 'A', value: 10 }],
361
+ encoding: {
362
+ x: { field: 'value', type: 'quantitative' },
363
+ y: { field: 'name', type: 'nominal' },
364
+ },
365
+ };
366
+ const { svg } = renderSpec(noChrome);
367
+ const chromeGroup = svg.querySelector('.viz-chrome');
368
+ expect(chromeGroup).not.toBeNull();
369
+ // No title/subtitle/source should be in the chrome group
370
+ expect(chromeGroup!.querySelector('.viz-title')).toBeNull();
371
+ expect(chromeGroup!.querySelector('.viz-subtitle')).toBeNull();
372
+ expect(chromeGroup!.querySelector('.viz-source')).toBeNull();
373
+ });
374
+ });
375
+
376
+ // ---------------------------------------------------------------------------
377
+ // Axes and tick labels
378
+ // ---------------------------------------------------------------------------
379
+
380
+ describe('axis rendering', () => {
381
+ it('renders x-axis and y-axis groups', () => {
382
+ const { svg } = renderSpec(lineSpec);
383
+ const xAxis = svg.querySelector('.viz-axis-x');
384
+ const yAxis = svg.querySelector('.viz-axis-y');
385
+ expect(xAxis).not.toBeNull();
386
+ expect(yAxis).not.toBeNull();
387
+ });
388
+
389
+ it('x-axis has tick labels as text elements', () => {
390
+ const { svg } = renderSpec(lineSpec);
391
+ const xAxis = svg.querySelector('.viz-axis-x');
392
+ const labels = xAxis!.querySelectorAll('text');
393
+ expect(labels.length).toBeGreaterThan(0);
394
+ // Each label should have text content
395
+ for (const label of labels) {
396
+ expect(label.textContent!.length).toBeGreaterThan(0);
397
+ }
398
+ });
399
+
400
+ it('y-axis has tick labels as text elements', () => {
401
+ const { svg } = renderSpec(barSpec);
402
+ const yAxis = svg.querySelector('.viz-axis-y');
403
+ const labels = yAxis!.querySelectorAll('text');
404
+ expect(labels.length).toBeGreaterThan(0);
405
+ });
406
+
407
+ it('x-axis has a baseline line element', () => {
408
+ const { svg } = renderSpec(lineSpec);
409
+ const xAxis = svg.querySelector('.viz-axis-x');
410
+ const line = xAxis!.querySelector('line');
411
+ // The renderer draws an axis line for x-axis
412
+ expect(line).not.toBeNull();
413
+ });
414
+ });
415
+
416
+ // ---------------------------------------------------------------------------
417
+ // Gridlines
418
+ // ---------------------------------------------------------------------------
419
+
420
+ describe('gridline rendering', () => {
421
+ it('renders gridlines as line elements within axis groups', () => {
422
+ const { svg } = renderSpec(lineSpec);
423
+ // y-axis gridlines are horizontal lines
424
+ const yAxis = svg.querySelector('.viz-axis-y');
425
+ const gridlines = yAxis!.querySelectorAll('line');
426
+ expect(gridlines.length).toBeGreaterThan(0);
427
+ });
428
+
429
+ it('gridlines have stroke-opacity for subtlety', () => {
430
+ const { svg } = renderSpec(lineSpec);
431
+ const yAxis = svg.querySelector('.viz-axis-y');
432
+ const gridlines = yAxis!.querySelectorAll('line');
433
+ for (const gl of gridlines) {
434
+ const opacity = gl.getAttribute('stroke-opacity');
435
+ if (opacity) {
436
+ // Gridlines should be subtle (less than 1.0 opacity)
437
+ expect(Number(opacity)).toBeLessThan(1);
438
+ }
439
+ }
440
+ });
441
+ });
442
+
443
+ // ---------------------------------------------------------------------------
444
+ // Legend
445
+ // ---------------------------------------------------------------------------
446
+
447
+ describe('legend rendering', () => {
448
+ it('multi-series chart renders legend entries', () => {
449
+ const { svg } = renderSpec(lineSpec);
450
+ const legend = svg.querySelector('.viz-legend');
451
+ expect(legend).not.toBeNull();
452
+ const entries = legend!.querySelectorAll('.viz-legend-entry');
453
+ // lineSpec has US and UK series
454
+ expect(entries.length).toBe(2);
455
+ });
456
+
457
+ it('legend entries have labels with series names', () => {
458
+ const { svg } = renderSpec(lineSpec);
459
+ const entries = svg.querySelectorAll('.viz-legend-entry');
460
+ const labels: string[] = [];
461
+ for (const entry of entries) {
462
+ const text = entry.querySelector('text');
463
+ if (text?.textContent) labels.push(text.textContent);
464
+ }
465
+ expect(labels).toContain('US');
466
+ expect(labels).toContain('UK');
467
+ });
468
+
469
+ it('legend entries have data-legend-label attribute', () => {
470
+ const { svg } = renderSpec(lineSpec);
471
+ const entries = svg.querySelectorAll('.viz-legend-entry');
472
+ for (const entry of entries) {
473
+ expect(entry.getAttribute('data-legend-label')).not.toBeNull();
474
+ }
475
+ });
476
+
477
+ it('legend has ARIA attributes for accessibility', () => {
478
+ const { svg } = renderSpec(lineSpec);
479
+ const legend = svg.querySelector('.viz-legend');
480
+ expect(legend!.getAttribute('role')).toBe('list');
481
+ expect(legend!.getAttribute('aria-label')).toBe('Chart legend');
482
+ const entries = legend!.querySelectorAll('.viz-legend-entry');
483
+ for (const entry of entries) {
484
+ expect(entry.getAttribute('role')).toBe('listitem');
485
+ }
486
+ });
487
+
488
+ it('single-series chart has no legend entries', () => {
489
+ const { svg } = renderSpec(singleSeriesLineSpec);
490
+ const entries = svg.querySelectorAll('.viz-legend-entry');
491
+ expect(entries.length).toBe(0);
492
+ });
493
+
494
+ it('pie chart renders legend entries for each slice', () => {
495
+ const { svg } = renderSpec(pieSpec);
496
+ const entries = svg.querySelectorAll('.viz-legend-entry');
497
+ // pieSpec has 3 slices
498
+ expect(entries.length).toBe(3);
499
+ });
500
+ });
501
+
502
+ // ---------------------------------------------------------------------------
503
+ // SVG root structure
504
+ // ---------------------------------------------------------------------------
505
+
506
+ describe('SVG root structure', () => {
507
+ it('SVG has correct viewBox matching dimensions', () => {
508
+ const { svg } = renderSpec(lineSpec);
509
+ const viewBox = svg.getAttribute('viewBox');
510
+ expect(viewBox).toBe('0 0 600 400');
511
+ });
512
+
513
+ it('SVG has accessibility role and aria-label', () => {
514
+ const { svg } = renderSpec(lineSpec);
515
+ expect(svg.getAttribute('role')).toBe('img');
516
+ const ariaLabel = svg.getAttribute('aria-label');
517
+ expect(ariaLabel).not.toBeNull();
518
+ expect(ariaLabel!.length).toBeGreaterThan(0);
519
+ });
520
+
521
+ it('SVG has viz-chart class', () => {
522
+ const { svg } = renderSpec(lineSpec);
523
+ expect(svg.getAttribute('class')).toBe('viz-chart');
524
+ });
525
+
526
+ it('SVG has a background rect as first child', () => {
527
+ const { svg } = renderSpec(lineSpec);
528
+ const firstChild = svg.children[0];
529
+ expect(firstChild.tagName.toLowerCase()).toBe('rect');
530
+ expect(Number(firstChild.getAttribute('width'))).toBe(600);
531
+ expect(Number(firstChild.getAttribute('height'))).toBe(400);
532
+ });
533
+
534
+ it('SVG has a defs element with clip path', () => {
535
+ const { svg } = renderSpec(lineSpec);
536
+ const defs = svg.querySelector('defs');
537
+ expect(defs).not.toBeNull();
538
+ const clipPath = defs!.querySelector('clipPath');
539
+ expect(clipPath).not.toBeNull();
540
+ expect(clipPath!.getAttribute('id')).toMatch(/^viz-clip-/);
541
+ });
542
+
543
+ it('marks group is clipped via clip-path attribute', () => {
544
+ const { svg } = renderSpec(lineSpec);
545
+ const clippedGroup = svg.querySelector('[clip-path]');
546
+ expect(clippedGroup).not.toBeNull();
547
+ expect(clippedGroup!.getAttribute('clip-path')).toMatch(/url\(#viz-clip-/);
548
+ });
549
+ });
550
+
551
+ // ---------------------------------------------------------------------------
552
+ // Inline snapshot for a critical mark element
553
+ // ---------------------------------------------------------------------------
554
+
555
+ describe('targeted mark snapshots', () => {
556
+ it('line mark group has expected structure', () => {
557
+ const { svg } = renderSpec(singleSeriesLineSpec);
558
+ const lineGroup = svg.querySelector('.viz-mark-line');
559
+ expect(lineGroup).not.toBeNull();
560
+ expect(lineGroup!.getAttribute('class')).toBe('viz-mark viz-mark-line');
561
+ expect(lineGroup!.getAttribute('data-mark-id')).toMatch(/^line-/);
562
+
563
+ const path = lineGroup!.querySelector('path');
564
+ expect(path).not.toBeNull();
565
+ expect(path!.getAttribute('fill')).toBe('none');
566
+ expect(path!.getAttribute('stroke')).not.toBeNull();
567
+ expect(Number(path!.getAttribute('stroke-width'))).toBeGreaterThan(0);
568
+ expect(path!.getAttribute('d')).toMatch(/^M/);
569
+ });
570
+
571
+ it('rect mark group has expected structure', () => {
572
+ const { svg } = renderSpec(barSpec);
573
+ const rectGroup = svg.querySelector('.viz-mark-rect');
574
+ expect(rectGroup).not.toBeNull();
575
+ expect(rectGroup!.getAttribute('class')).toBe('viz-mark viz-mark-rect');
576
+ expect(rectGroup!.getAttribute('data-mark-id')).toMatch(/^rect-/);
577
+
578
+ const rect = rectGroup!.querySelector('rect');
579
+ expect(rect).not.toBeNull();
580
+ expect(Number(rect!.getAttribute('width'))).toBeGreaterThan(0);
581
+ expect(Number(rect!.getAttribute('height'))).toBeGreaterThan(0);
582
+ expect(rect!.getAttribute('fill')).not.toBeNull();
583
+ });
584
+
585
+ it('point mark has expected attributes', () => {
586
+ const { svg } = renderSpec(scatterSpec);
587
+ const point = svg.querySelector('.viz-mark-point');
588
+ expect(point).not.toBeNull();
589
+ expect(point!.tagName.toLowerCase()).toBe('circle');
590
+ expect(point!.getAttribute('class')).toBe('viz-mark viz-mark-point');
591
+ expect(point!.getAttribute('data-mark-id')).toMatch(/^point-/);
592
+ expect(Number(point!.getAttribute('r'))).toBeGreaterThan(0);
593
+ expect(point!.getAttribute('fill')).not.toBeNull();
594
+ });
595
+
596
+ it('arc mark group has expected structure', () => {
597
+ const { svg } = renderSpec(pieSpec);
598
+ const arcGroup = svg.querySelector('.viz-mark-arc');
599
+ expect(arcGroup).not.toBeNull();
600
+ expect(arcGroup!.getAttribute('class')).toBe('viz-mark viz-mark-arc');
601
+ expect(arcGroup!.getAttribute('data-mark-id')).toMatch(/^arc-/);
602
+ expect(arcGroup!.getAttribute('transform')).toMatch(/translate\(/);
603
+
604
+ const path = arcGroup!.querySelector('path');
605
+ expect(path).not.toBeNull();
606
+ expect(path!.getAttribute('fill')).not.toBeNull();
607
+ expect(path!.getAttribute('d')).not.toBeNull();
608
+ });
609
+ });