@opendata-ai/openchart-vanilla 6.4.1 → 6.5.1
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/dist/index.d.ts +9 -2
- package/dist/index.js +327 -174
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -764
- package/package.json +3 -3
- package/src/__tests__/animation.test.ts +358 -0
- package/src/__tests__/edit-events.test.ts +35 -35
- package/src/__tests__/events.test.ts +7 -7
- package/src/__tests__/export.test.ts +1 -1
- package/src/__tests__/mount.test.ts +10 -10
- package/src/__tests__/selection-events.test.ts +14 -14
- package/src/__tests__/svg-renderer.test.ts +67 -67
- package/src/__tests__/table-keyboard.test.ts +18 -18
- package/src/__tests__/table-mount.test.ts +138 -17
- package/src/__tests__/tooltip.test.ts +12 -12
- package/src/animation.ts +75 -0
- package/src/graph/__tests__/graph-mount.test.ts +16 -16
- package/src/graph-mount.ts +18 -18
- package/src/mount.ts +71 -37
- package/src/renderers/table-cells.ts +11 -9
- package/src/svg-renderer.ts +161 -54
- package/src/table-keyboard.ts +5 -5
- package/src/table-mount.ts +34 -11
- package/src/table-renderer.ts +70 -39
- package/src/tooltip.ts +8 -8
|
@@ -53,7 +53,7 @@ afterEach(() => {
|
|
|
53
53
|
describe('line chart SVG rendering', () => {
|
|
54
54
|
it('renders <path> elements with valid d attribute for each series', () => {
|
|
55
55
|
const { svg } = renderSpec(lineSpec);
|
|
56
|
-
const paths = svg.querySelectorAll('.
|
|
56
|
+
const paths = svg.querySelectorAll('.oc-mark-line path');
|
|
57
57
|
expect(paths.length).toBeGreaterThan(0);
|
|
58
58
|
|
|
59
59
|
for (const path of paths) {
|
|
@@ -67,14 +67,14 @@ describe('line chart SVG rendering', () => {
|
|
|
67
67
|
|
|
68
68
|
it('creates a mark group per series in multi-series line chart', () => {
|
|
69
69
|
const { svg } = renderSpec(lineSpec);
|
|
70
|
-
const lineGroups = svg.querySelectorAll('.
|
|
70
|
+
const lineGroups = svg.querySelectorAll('.oc-mark-line');
|
|
71
71
|
// lineSpec has 2 series (US and UK)
|
|
72
72
|
expect(lineGroups.length).toBe(2);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
it('each line mark group has a data-mark-id attribute', () => {
|
|
76
76
|
const { svg } = renderSpec(lineSpec);
|
|
77
|
-
const lineGroups = svg.querySelectorAll('.
|
|
77
|
+
const lineGroups = svg.querySelectorAll('.oc-mark-line');
|
|
78
78
|
for (const group of lineGroups) {
|
|
79
79
|
const markId = group.getAttribute('data-mark-id');
|
|
80
80
|
expect(markId).not.toBeNull();
|
|
@@ -84,7 +84,7 @@ describe('line chart SVG rendering', () => {
|
|
|
84
84
|
|
|
85
85
|
it('each line mark group has a data-series attribute', () => {
|
|
86
86
|
const { svg } = renderSpec(lineSpec);
|
|
87
|
-
const lineGroups = svg.querySelectorAll('.
|
|
87
|
+
const lineGroups = svg.querySelectorAll('.oc-mark-line');
|
|
88
88
|
const seriesNames = new Set<string>();
|
|
89
89
|
for (const group of lineGroups) {
|
|
90
90
|
const series = group.getAttribute('data-series');
|
|
@@ -97,7 +97,7 @@ describe('line chart SVG rendering', () => {
|
|
|
97
97
|
|
|
98
98
|
it('paths have stroke color and non-zero stroke width', () => {
|
|
99
99
|
const { svg } = renderSpec(lineSpec);
|
|
100
|
-
const paths = svg.querySelectorAll('.
|
|
100
|
+
const paths = svg.querySelectorAll('.oc-mark-line path');
|
|
101
101
|
for (const path of paths) {
|
|
102
102
|
const stroke = path.getAttribute('stroke');
|
|
103
103
|
expect(stroke).not.toBeNull();
|
|
@@ -115,14 +115,14 @@ describe('line chart SVG rendering', () => {
|
|
|
115
115
|
describe('bar chart SVG rendering', () => {
|
|
116
116
|
it('renders <rect> elements for each data point', () => {
|
|
117
117
|
const { svg } = renderSpec(barSpec);
|
|
118
|
-
const rects = svg.querySelectorAll('.
|
|
118
|
+
const rects = svg.querySelectorAll('.oc-mark-rect rect');
|
|
119
119
|
// barSpec has 3 data points
|
|
120
120
|
expect(rects.length).toBe(3);
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
it('rect elements have width and height > 0', () => {
|
|
124
124
|
const { svg } = renderSpec(barSpec);
|
|
125
|
-
const rects = svg.querySelectorAll('.
|
|
125
|
+
const rects = svg.querySelectorAll('.oc-mark-rect rect');
|
|
126
126
|
for (const rect of rects) {
|
|
127
127
|
const width = Number(rect.getAttribute('width'));
|
|
128
128
|
const height = Number(rect.getAttribute('height'));
|
|
@@ -133,7 +133,7 @@ describe('bar chart SVG rendering', () => {
|
|
|
133
133
|
|
|
134
134
|
it('rect marks have data-mark-id attributes', () => {
|
|
135
135
|
const { svg } = renderSpec(barSpec);
|
|
136
|
-
const markGroups = svg.querySelectorAll('.
|
|
136
|
+
const markGroups = svg.querySelectorAll('.oc-mark-rect');
|
|
137
137
|
for (const group of markGroups) {
|
|
138
138
|
const markId = group.getAttribute('data-mark-id');
|
|
139
139
|
expect(markId).not.toBeNull();
|
|
@@ -143,7 +143,7 @@ describe('bar chart SVG rendering', () => {
|
|
|
143
143
|
|
|
144
144
|
it('bar rects are oriented horizontally (width varies, y is categorical)', () => {
|
|
145
145
|
const { svg } = renderSpec(barSpec);
|
|
146
|
-
const rects = svg.querySelectorAll('.
|
|
146
|
+
const rects = svg.querySelectorAll('.oc-mark-rect rect');
|
|
147
147
|
const widths = Array.from(rects).map((r) => Number(r.getAttribute('width')));
|
|
148
148
|
// Different data values should produce different widths
|
|
149
149
|
const uniqueWidths = new Set(widths);
|
|
@@ -158,13 +158,13 @@ describe('bar chart SVG rendering', () => {
|
|
|
158
158
|
describe('column chart SVG rendering', () => {
|
|
159
159
|
it('renders <rect> elements oriented vertically', () => {
|
|
160
160
|
const { svg } = renderSpec(columnSpec);
|
|
161
|
-
const rects = svg.querySelectorAll('.
|
|
161
|
+
const rects = svg.querySelectorAll('.oc-mark-rect rect');
|
|
162
162
|
expect(rects.length).toBe(3);
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
it('column rects have varying heights (vertical orientation)', () => {
|
|
166
166
|
const { svg } = renderSpec(columnSpec);
|
|
167
|
-
const rects = svg.querySelectorAll('.
|
|
167
|
+
const rects = svg.querySelectorAll('.oc-mark-rect rect');
|
|
168
168
|
const heights = Array.from(rects).map((r) => Number(r.getAttribute('height')));
|
|
169
169
|
// Different revenue values should produce different heights
|
|
170
170
|
const uniqueHeights = new Set(heights);
|
|
@@ -177,7 +177,7 @@ describe('column chart SVG rendering', () => {
|
|
|
177
177
|
|
|
178
178
|
it('column rects have positive width', () => {
|
|
179
179
|
const { svg } = renderSpec(columnSpec);
|
|
180
|
-
const rects = svg.querySelectorAll('.
|
|
180
|
+
const rects = svg.querySelectorAll('.oc-mark-rect rect');
|
|
181
181
|
for (const rect of rects) {
|
|
182
182
|
const width = Number(rect.getAttribute('width'));
|
|
183
183
|
expect(width).toBeGreaterThan(0);
|
|
@@ -192,14 +192,14 @@ describe('column chart SVG rendering', () => {
|
|
|
192
192
|
describe('scatter chart SVG rendering', () => {
|
|
193
193
|
it('renders <circle> elements for each data point', () => {
|
|
194
194
|
const { svg } = renderSpec(scatterSpec);
|
|
195
|
-
const circles = svg.querySelectorAll('.
|
|
195
|
+
const circles = svg.querySelectorAll('.oc-mark-point');
|
|
196
196
|
// scatterSpec has 4 data points
|
|
197
197
|
expect(circles.length).toBe(4);
|
|
198
198
|
});
|
|
199
199
|
|
|
200
200
|
it('circles have valid cx, cy, and r attributes', () => {
|
|
201
201
|
const { svg } = renderSpec(scatterSpec);
|
|
202
|
-
const circles = svg.querySelectorAll('.
|
|
202
|
+
const circles = svg.querySelectorAll('.oc-mark-point');
|
|
203
203
|
for (const circle of circles) {
|
|
204
204
|
const cx = Number(circle.getAttribute('cx'));
|
|
205
205
|
const cy = Number(circle.getAttribute('cy'));
|
|
@@ -215,7 +215,7 @@ describe('scatter chart SVG rendering', () => {
|
|
|
215
215
|
|
|
216
216
|
it('scatter marks have data-mark-id attributes', () => {
|
|
217
217
|
const { svg } = renderSpec(scatterSpec);
|
|
218
|
-
const circles = svg.querySelectorAll('.
|
|
218
|
+
const circles = svg.querySelectorAll('.oc-mark-point');
|
|
219
219
|
for (const circle of circles) {
|
|
220
220
|
const markId = circle.getAttribute('data-mark-id');
|
|
221
221
|
expect(markId).not.toBeNull();
|
|
@@ -231,14 +231,14 @@ describe('scatter chart SVG rendering', () => {
|
|
|
231
231
|
describe('pie chart SVG rendering', () => {
|
|
232
232
|
it('renders <path> arc segments for each slice', () => {
|
|
233
233
|
const { svg } = renderSpec(pieSpec);
|
|
234
|
-
const arcGroups = svg.querySelectorAll('.
|
|
234
|
+
const arcGroups = svg.querySelectorAll('.oc-mark-arc');
|
|
235
235
|
// pieSpec has 3 categories
|
|
236
236
|
expect(arcGroups.length).toBe(3);
|
|
237
237
|
});
|
|
238
238
|
|
|
239
239
|
it('arc paths have valid d attribute with arc commands', () => {
|
|
240
240
|
const { svg } = renderSpec(pieSpec);
|
|
241
|
-
const paths = svg.querySelectorAll('.
|
|
241
|
+
const paths = svg.querySelectorAll('.oc-mark-arc path');
|
|
242
242
|
for (const path of paths) {
|
|
243
243
|
const d = path.getAttribute('d');
|
|
244
244
|
expect(d).not.toBeNull();
|
|
@@ -250,7 +250,7 @@ describe('pie chart SVG rendering', () => {
|
|
|
250
250
|
|
|
251
251
|
it('arc groups are translated to the pie center', () => {
|
|
252
252
|
const { svg } = renderSpec(pieSpec);
|
|
253
|
-
const arcGroups = svg.querySelectorAll('.
|
|
253
|
+
const arcGroups = svg.querySelectorAll('.oc-mark-arc');
|
|
254
254
|
for (const group of arcGroups) {
|
|
255
255
|
const transform = group.getAttribute('transform');
|
|
256
256
|
expect(transform).not.toBeNull();
|
|
@@ -260,7 +260,7 @@ describe('pie chart SVG rendering', () => {
|
|
|
260
260
|
|
|
261
261
|
it('arc marks have fill colors', () => {
|
|
262
262
|
const { svg } = renderSpec(pieSpec);
|
|
263
|
-
const paths = svg.querySelectorAll('.
|
|
263
|
+
const paths = svg.querySelectorAll('.oc-mark-arc path');
|
|
264
264
|
for (const path of paths) {
|
|
265
265
|
const fill = path.getAttribute('fill');
|
|
266
266
|
expect(fill).not.toBeNull();
|
|
@@ -276,7 +276,7 @@ describe('pie chart SVG rendering', () => {
|
|
|
276
276
|
describe('multi-series rendering', () => {
|
|
277
277
|
it('multi-series line chart has distinct stroke colors per series', () => {
|
|
278
278
|
const { svg } = renderSpec(lineSpec);
|
|
279
|
-
const paths = svg.querySelectorAll('.
|
|
279
|
+
const paths = svg.querySelectorAll('.oc-mark-line path');
|
|
280
280
|
const strokes = new Set<string>();
|
|
281
281
|
for (const path of paths) {
|
|
282
282
|
const stroke = path.getAttribute('stroke');
|
|
@@ -288,7 +288,7 @@ describe('multi-series rendering', () => {
|
|
|
288
288
|
|
|
289
289
|
it('multi-series scatter chart has distinct fill colors per group', () => {
|
|
290
290
|
const { svg } = renderSpec(scatterSpec);
|
|
291
|
-
const circles = svg.querySelectorAll('.
|
|
291
|
+
const circles = svg.querySelectorAll('.oc-mark-point');
|
|
292
292
|
const fills = new Set<string>();
|
|
293
293
|
for (const circle of circles) {
|
|
294
294
|
const fill = circle.getAttribute('fill');
|
|
@@ -300,7 +300,7 @@ describe('multi-series rendering', () => {
|
|
|
300
300
|
|
|
301
301
|
it('multi-series bar chart renders data-series attributes on rect marks', () => {
|
|
302
302
|
const { svg } = renderSpec(multiSeriesBarSpec);
|
|
303
|
-
const marks = svg.querySelectorAll('.
|
|
303
|
+
const marks = svg.querySelectorAll('.oc-mark-rect[data-series]');
|
|
304
304
|
const seriesNames = new Set<string>();
|
|
305
305
|
for (const mark of marks) {
|
|
306
306
|
const s = mark.getAttribute('data-series');
|
|
@@ -318,35 +318,35 @@ describe('multi-series rendering', () => {
|
|
|
318
318
|
describe('chart chrome rendering', () => {
|
|
319
319
|
it('renders title text with correct content', () => {
|
|
320
320
|
const { svg } = renderSpec(lineSpec);
|
|
321
|
-
const title = svg.querySelector('.
|
|
321
|
+
const title = svg.querySelector('.oc-title');
|
|
322
322
|
expect(title).not.toBeNull();
|
|
323
323
|
expect(title!.textContent).toBe('GDP Growth');
|
|
324
324
|
});
|
|
325
325
|
|
|
326
326
|
it('renders subtitle text with correct content', () => {
|
|
327
327
|
const { svg } = renderSpec(lineSpec);
|
|
328
|
-
const subtitle = svg.querySelector('.
|
|
328
|
+
const subtitle = svg.querySelector('.oc-subtitle');
|
|
329
329
|
expect(subtitle).not.toBeNull();
|
|
330
330
|
expect(subtitle!.textContent).toBe('US vs UK over time');
|
|
331
331
|
});
|
|
332
332
|
|
|
333
333
|
it('renders source text with correct content', () => {
|
|
334
334
|
const { svg } = renderSpec(lineSpec);
|
|
335
|
-
const source = svg.querySelector('.
|
|
335
|
+
const source = svg.querySelector('.oc-source');
|
|
336
336
|
expect(source).not.toBeNull();
|
|
337
337
|
expect(source!.textContent).toBe('World Bank');
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
-
it('chrome elements are inside a .
|
|
340
|
+
it('chrome elements are inside a .oc-chrome group', () => {
|
|
341
341
|
const { svg } = renderSpec(lineSpec);
|
|
342
|
-
const chromeGroup = svg.querySelector('.
|
|
342
|
+
const chromeGroup = svg.querySelector('.oc-chrome');
|
|
343
343
|
expect(chromeGroup).not.toBeNull();
|
|
344
|
-
expect(chromeGroup!.querySelector('.
|
|
344
|
+
expect(chromeGroup!.querySelector('.oc-title')).not.toBeNull();
|
|
345
345
|
});
|
|
346
346
|
|
|
347
347
|
it('title has font styling applied', () => {
|
|
348
348
|
const { svg } = renderSpec(lineSpec);
|
|
349
|
-
const title = svg.querySelector('.
|
|
349
|
+
const title = svg.querySelector('.oc-title');
|
|
350
350
|
expect(title).not.toBeNull();
|
|
351
351
|
const fontFamily = title!.getAttribute('font-family');
|
|
352
352
|
const fontSize = Number(title!.getAttribute('font-size'));
|
|
@@ -371,7 +371,7 @@ describe('chart chrome rendering', () => {
|
|
|
371
371
|
};
|
|
372
372
|
// Render at a very narrow width to force wrapping
|
|
373
373
|
const { svg } = renderSpec(longTitleSpec, { width: 250, height: 300 });
|
|
374
|
-
const title = svg.querySelector('.
|
|
374
|
+
const title = svg.querySelector('.oc-title');
|
|
375
375
|
expect(title).not.toBeNull();
|
|
376
376
|
const tspans = title!.querySelectorAll('tspan');
|
|
377
377
|
expect(tspans.length).toBeGreaterThan(1);
|
|
@@ -386,7 +386,7 @@ describe('chart chrome rendering', () => {
|
|
|
386
386
|
|
|
387
387
|
it('does not wrap short title text', () => {
|
|
388
388
|
const { svg } = renderSpec(lineSpec);
|
|
389
|
-
const title = svg.querySelector('.
|
|
389
|
+
const title = svg.querySelector('.oc-title');
|
|
390
390
|
expect(title).not.toBeNull();
|
|
391
391
|
// Short title should have no tspan children, just direct textContent
|
|
392
392
|
const tspans = title!.querySelectorAll('tspan');
|
|
@@ -404,12 +404,12 @@ describe('chart chrome rendering', () => {
|
|
|
404
404
|
},
|
|
405
405
|
};
|
|
406
406
|
const { svg } = renderSpec(noChrome);
|
|
407
|
-
const chromeGroup = svg.querySelector('.
|
|
407
|
+
const chromeGroup = svg.querySelector('.oc-chrome');
|
|
408
408
|
expect(chromeGroup).not.toBeNull();
|
|
409
409
|
// No title/subtitle/source should be in the chrome group
|
|
410
|
-
expect(chromeGroup!.querySelector('.
|
|
411
|
-
expect(chromeGroup!.querySelector('.
|
|
412
|
-
expect(chromeGroup!.querySelector('.
|
|
410
|
+
expect(chromeGroup!.querySelector('.oc-title')).toBeNull();
|
|
411
|
+
expect(chromeGroup!.querySelector('.oc-subtitle')).toBeNull();
|
|
412
|
+
expect(chromeGroup!.querySelector('.oc-source')).toBeNull();
|
|
413
413
|
});
|
|
414
414
|
});
|
|
415
415
|
|
|
@@ -420,15 +420,15 @@ describe('chart chrome rendering', () => {
|
|
|
420
420
|
describe('axis rendering', () => {
|
|
421
421
|
it('renders x-axis and y-axis groups', () => {
|
|
422
422
|
const { svg } = renderSpec(lineSpec);
|
|
423
|
-
const xAxis = svg.querySelector('.
|
|
424
|
-
const yAxis = svg.querySelector('.
|
|
423
|
+
const xAxis = svg.querySelector('.oc-axis-x');
|
|
424
|
+
const yAxis = svg.querySelector('.oc-axis-y');
|
|
425
425
|
expect(xAxis).not.toBeNull();
|
|
426
426
|
expect(yAxis).not.toBeNull();
|
|
427
427
|
});
|
|
428
428
|
|
|
429
429
|
it('x-axis has tick labels as text elements', () => {
|
|
430
430
|
const { svg } = renderSpec(lineSpec);
|
|
431
|
-
const xAxis = svg.querySelector('.
|
|
431
|
+
const xAxis = svg.querySelector('.oc-axis-x');
|
|
432
432
|
const labels = xAxis!.querySelectorAll('text');
|
|
433
433
|
expect(labels.length).toBeGreaterThan(0);
|
|
434
434
|
// Each label should have text content
|
|
@@ -439,14 +439,14 @@ describe('axis rendering', () => {
|
|
|
439
439
|
|
|
440
440
|
it('y-axis has tick labels as text elements', () => {
|
|
441
441
|
const { svg } = renderSpec(barSpec);
|
|
442
|
-
const yAxis = svg.querySelector('.
|
|
442
|
+
const yAxis = svg.querySelector('.oc-axis-y');
|
|
443
443
|
const labels = yAxis!.querySelectorAll('text');
|
|
444
444
|
expect(labels.length).toBeGreaterThan(0);
|
|
445
445
|
});
|
|
446
446
|
|
|
447
447
|
it('x-axis has a baseline line element', () => {
|
|
448
448
|
const { svg } = renderSpec(lineSpec);
|
|
449
|
-
const xAxis = svg.querySelector('.
|
|
449
|
+
const xAxis = svg.querySelector('.oc-axis-x');
|
|
450
450
|
const line = xAxis!.querySelector('line');
|
|
451
451
|
// The renderer draws an axis line for x-axis
|
|
452
452
|
expect(line).not.toBeNull();
|
|
@@ -461,14 +461,14 @@ describe('gridline rendering', () => {
|
|
|
461
461
|
it('renders gridlines as line elements within axis groups', () => {
|
|
462
462
|
const { svg } = renderSpec(lineSpec);
|
|
463
463
|
// y-axis gridlines are horizontal lines
|
|
464
|
-
const yAxis = svg.querySelector('.
|
|
464
|
+
const yAxis = svg.querySelector('.oc-axis-y');
|
|
465
465
|
const gridlines = yAxis!.querySelectorAll('line');
|
|
466
466
|
expect(gridlines.length).toBeGreaterThan(0);
|
|
467
467
|
});
|
|
468
468
|
|
|
469
469
|
it('gridlines have stroke-opacity for subtlety', () => {
|
|
470
470
|
const { svg } = renderSpec(lineSpec);
|
|
471
|
-
const yAxis = svg.querySelector('.
|
|
471
|
+
const yAxis = svg.querySelector('.oc-axis-y');
|
|
472
472
|
const gridlines = yAxis!.querySelectorAll('line');
|
|
473
473
|
for (const gl of gridlines) {
|
|
474
474
|
const opacity = gl.getAttribute('stroke-opacity');
|
|
@@ -487,16 +487,16 @@ describe('gridline rendering', () => {
|
|
|
487
487
|
describe('legend rendering', () => {
|
|
488
488
|
it('multi-series chart renders legend entries', () => {
|
|
489
489
|
const { svg } = renderSpec(lineSpec);
|
|
490
|
-
const legend = svg.querySelector('.
|
|
490
|
+
const legend = svg.querySelector('.oc-legend');
|
|
491
491
|
expect(legend).not.toBeNull();
|
|
492
|
-
const entries = legend!.querySelectorAll('.
|
|
492
|
+
const entries = legend!.querySelectorAll('.oc-legend-entry');
|
|
493
493
|
// lineSpec has US and UK series
|
|
494
494
|
expect(entries.length).toBe(2);
|
|
495
495
|
});
|
|
496
496
|
|
|
497
497
|
it('legend entries have labels with series names', () => {
|
|
498
498
|
const { svg } = renderSpec(lineSpec);
|
|
499
|
-
const entries = svg.querySelectorAll('.
|
|
499
|
+
const entries = svg.querySelectorAll('.oc-legend-entry');
|
|
500
500
|
const labels: string[] = [];
|
|
501
501
|
for (const entry of entries) {
|
|
502
502
|
const text = entry.querySelector('text');
|
|
@@ -508,7 +508,7 @@ describe('legend rendering', () => {
|
|
|
508
508
|
|
|
509
509
|
it('legend entries have data-legend-label attribute', () => {
|
|
510
510
|
const { svg } = renderSpec(lineSpec);
|
|
511
|
-
const entries = svg.querySelectorAll('.
|
|
511
|
+
const entries = svg.querySelectorAll('.oc-legend-entry');
|
|
512
512
|
for (const entry of entries) {
|
|
513
513
|
expect(entry.getAttribute('data-legend-label')).not.toBeNull();
|
|
514
514
|
}
|
|
@@ -516,10 +516,10 @@ describe('legend rendering', () => {
|
|
|
516
516
|
|
|
517
517
|
it('legend has ARIA attributes for accessibility', () => {
|
|
518
518
|
const { svg } = renderSpec(lineSpec);
|
|
519
|
-
const legend = svg.querySelector('.
|
|
519
|
+
const legend = svg.querySelector('.oc-legend');
|
|
520
520
|
expect(legend!.getAttribute('role')).toBe('list');
|
|
521
521
|
expect(legend!.getAttribute('aria-label')).toBe('Chart legend');
|
|
522
|
-
const entries = legend!.querySelectorAll('.
|
|
522
|
+
const entries = legend!.querySelectorAll('.oc-legend-entry');
|
|
523
523
|
for (const entry of entries) {
|
|
524
524
|
expect(entry.getAttribute('role')).toBe('listitem');
|
|
525
525
|
}
|
|
@@ -527,13 +527,13 @@ describe('legend rendering', () => {
|
|
|
527
527
|
|
|
528
528
|
it('single-series chart has no legend entries', () => {
|
|
529
529
|
const { svg } = renderSpec(singleSeriesLineSpec);
|
|
530
|
-
const entries = svg.querySelectorAll('.
|
|
530
|
+
const entries = svg.querySelectorAll('.oc-legend-entry');
|
|
531
531
|
expect(entries.length).toBe(0);
|
|
532
532
|
});
|
|
533
533
|
|
|
534
534
|
it('pie chart renders legend entries for each slice', () => {
|
|
535
535
|
const { svg } = renderSpec(pieSpec);
|
|
536
|
-
const entries = svg.querySelectorAll('.
|
|
536
|
+
const entries = svg.querySelectorAll('.oc-legend-entry');
|
|
537
537
|
// pieSpec has 3 slices
|
|
538
538
|
expect(entries.length).toBe(3);
|
|
539
539
|
});
|
|
@@ -558,9 +558,9 @@ describe('SVG root structure', () => {
|
|
|
558
558
|
expect(ariaLabel!.length).toBeGreaterThan(0);
|
|
559
559
|
});
|
|
560
560
|
|
|
561
|
-
it('SVG has
|
|
561
|
+
it('SVG has oc-chart class', () => {
|
|
562
562
|
const { svg } = renderSpec(lineSpec);
|
|
563
|
-
expect(svg.getAttribute('class')).toBe('
|
|
563
|
+
expect(svg.getAttribute('class')).toBe('oc-chart');
|
|
564
564
|
});
|
|
565
565
|
|
|
566
566
|
it('SVG has a background rect as first child', () => {
|
|
@@ -577,14 +577,14 @@ describe('SVG root structure', () => {
|
|
|
577
577
|
expect(defs).not.toBeNull();
|
|
578
578
|
const clipPath = defs!.querySelector('clipPath');
|
|
579
579
|
expect(clipPath).not.toBeNull();
|
|
580
|
-
expect(clipPath!.getAttribute('id')).toMatch(/^
|
|
580
|
+
expect(clipPath!.getAttribute('id')).toMatch(/^oc-clip-/);
|
|
581
581
|
});
|
|
582
582
|
|
|
583
583
|
it('marks group is clipped via clip-path attribute', () => {
|
|
584
584
|
const { svg } = renderSpec(lineSpec);
|
|
585
585
|
const clippedGroup = svg.querySelector('[clip-path]');
|
|
586
586
|
expect(clippedGroup).not.toBeNull();
|
|
587
|
-
expect(clippedGroup!.getAttribute('clip-path')).toMatch(/url\(#
|
|
587
|
+
expect(clippedGroup!.getAttribute('clip-path')).toMatch(/url\(#oc-clip-/);
|
|
588
588
|
});
|
|
589
589
|
});
|
|
590
590
|
|
|
@@ -595,9 +595,9 @@ describe('SVG root structure', () => {
|
|
|
595
595
|
describe('targeted mark snapshots', () => {
|
|
596
596
|
it('line mark group has expected structure', () => {
|
|
597
597
|
const { svg } = renderSpec(singleSeriesLineSpec);
|
|
598
|
-
const lineGroup = svg.querySelector('.
|
|
598
|
+
const lineGroup = svg.querySelector('.oc-mark-line');
|
|
599
599
|
expect(lineGroup).not.toBeNull();
|
|
600
|
-
expect(lineGroup!.getAttribute('class')).toBe('
|
|
600
|
+
expect(lineGroup!.getAttribute('class')).toBe('oc-mark oc-mark-line');
|
|
601
601
|
expect(lineGroup!.getAttribute('data-mark-id')).toMatch(/^line-/);
|
|
602
602
|
|
|
603
603
|
const path = lineGroup!.querySelector('path');
|
|
@@ -610,9 +610,9 @@ describe('targeted mark snapshots', () => {
|
|
|
610
610
|
|
|
611
611
|
it('rect mark group has expected structure', () => {
|
|
612
612
|
const { svg } = renderSpec(barSpec);
|
|
613
|
-
const rectGroup = svg.querySelector('.
|
|
613
|
+
const rectGroup = svg.querySelector('.oc-mark-rect');
|
|
614
614
|
expect(rectGroup).not.toBeNull();
|
|
615
|
-
expect(rectGroup!.getAttribute('class')).toBe('
|
|
615
|
+
expect(rectGroup!.getAttribute('class')).toBe('oc-mark oc-mark-rect');
|
|
616
616
|
expect(rectGroup!.getAttribute('data-mark-id')).toMatch(/^rect-/);
|
|
617
617
|
|
|
618
618
|
const rect = rectGroup!.querySelector('rect');
|
|
@@ -624,10 +624,10 @@ describe('targeted mark snapshots', () => {
|
|
|
624
624
|
|
|
625
625
|
it('point mark has expected attributes', () => {
|
|
626
626
|
const { svg } = renderSpec(scatterSpec);
|
|
627
|
-
const point = svg.querySelector('.
|
|
627
|
+
const point = svg.querySelector('.oc-mark-point');
|
|
628
628
|
expect(point).not.toBeNull();
|
|
629
629
|
expect(point!.tagName.toLowerCase()).toBe('circle');
|
|
630
|
-
expect(point!.getAttribute('class')).toBe('
|
|
630
|
+
expect(point!.getAttribute('class')).toBe('oc-mark oc-mark-point');
|
|
631
631
|
expect(point!.getAttribute('data-mark-id')).toMatch(/^point-/);
|
|
632
632
|
expect(Number(point!.getAttribute('r'))).toBeGreaterThan(0);
|
|
633
633
|
expect(point!.getAttribute('fill')).not.toBeNull();
|
|
@@ -635,9 +635,9 @@ describe('targeted mark snapshots', () => {
|
|
|
635
635
|
|
|
636
636
|
it('arc mark group has expected structure', () => {
|
|
637
637
|
const { svg } = renderSpec(pieSpec);
|
|
638
|
-
const arcGroup = svg.querySelector('.
|
|
638
|
+
const arcGroup = svg.querySelector('.oc-mark-arc');
|
|
639
639
|
expect(arcGroup).not.toBeNull();
|
|
640
|
-
expect(arcGroup!.getAttribute('class')).toBe('
|
|
640
|
+
expect(arcGroup!.getAttribute('class')).toBe('oc-mark oc-mark-arc');
|
|
641
641
|
expect(arcGroup!.getAttribute('data-mark-id')).toMatch(/^arc-/);
|
|
642
642
|
expect(arcGroup!.getAttribute('transform')).toMatch(/translate\(/);
|
|
643
643
|
|
|
@@ -655,7 +655,7 @@ describe('targeted mark snapshots', () => {
|
|
|
655
655
|
describe('brand watermark', () => {
|
|
656
656
|
it('renders "OpenData" as a single text element with two tspans', () => {
|
|
657
657
|
const { svg } = renderSpec(lineSpec);
|
|
658
|
-
const brandLink = svg.querySelector('.
|
|
658
|
+
const brandLink = svg.querySelector('.oc-chrome-ref');
|
|
659
659
|
expect(brandLink).not.toBeNull();
|
|
660
660
|
const text = brandLink!.querySelector('text')!;
|
|
661
661
|
expect(text.textContent).toBe('OpenData');
|
|
@@ -673,21 +673,21 @@ describe('brand watermark', () => {
|
|
|
673
673
|
|
|
674
674
|
it('is a direct child of SVG root', () => {
|
|
675
675
|
const { svg } = renderSpec(lineSpec);
|
|
676
|
-
const brandLink = svg.querySelector('.
|
|
676
|
+
const brandLink = svg.querySelector('.oc-chrome-ref');
|
|
677
677
|
expect(brandLink!.parentElement).toBe(svg);
|
|
678
678
|
});
|
|
679
679
|
|
|
680
680
|
it('renders after chrome (in the footer row)', () => {
|
|
681
681
|
const { svg } = renderSpec(lineSpec);
|
|
682
682
|
const children = Array.from(svg.children);
|
|
683
|
-
const chromeIdx = children.findIndex((el) => el.classList.contains('
|
|
684
|
-
const brandIdx = children.findIndex((el) => el.classList.contains('
|
|
683
|
+
const chromeIdx = children.findIndex((el) => el.classList.contains('oc-chrome'));
|
|
684
|
+
const brandIdx = children.findIndex((el) => el.classList.contains('oc-chrome-ref'));
|
|
685
685
|
expect(brandIdx).toBeGreaterThan(chromeIdx);
|
|
686
686
|
});
|
|
687
687
|
|
|
688
688
|
it('skips watermark on very small charts', () => {
|
|
689
689
|
const { svg } = renderSpec(lineSpec, { width: 100, height: 80 });
|
|
690
|
-
const brandLink = svg.querySelector('.
|
|
690
|
+
const brandLink = svg.querySelector('.oc-chrome-ref');
|
|
691
691
|
expect(brandLink).toBeNull();
|
|
692
692
|
});
|
|
693
693
|
});
|
|
@@ -41,7 +41,7 @@ function createTableDOM(rows: number, cols: number, opts?: { search?: boolean })
|
|
|
41
41
|
// Search input
|
|
42
42
|
if (opts?.search) {
|
|
43
43
|
const searchDiv = document.createElement('div');
|
|
44
|
-
searchDiv.className = '
|
|
44
|
+
searchDiv.className = 'oc-table-search';
|
|
45
45
|
const input = document.createElement('input');
|
|
46
46
|
searchDiv.appendChild(input);
|
|
47
47
|
wrapper.appendChild(searchDiv);
|
|
@@ -147,8 +147,8 @@ describe('tbody arrow key navigation', () => {
|
|
|
147
147
|
|
|
148
148
|
it('focus highlights first cell and sets aria-activedescendant', () => {
|
|
149
149
|
const tbody = focusTbody(wrapper);
|
|
150
|
-
expect(tbody.getAttribute('aria-activedescendant')).toBe('
|
|
151
|
-
const cell = wrapper.querySelector('.
|
|
150
|
+
expect(tbody.getAttribute('aria-activedescendant')).toBe('oc-cell-0-0');
|
|
151
|
+
const cell = wrapper.querySelector('.oc-table-cell-focus');
|
|
152
152
|
expect(cell).not.toBeNull();
|
|
153
153
|
expect(cell?.textContent).toBe('R0C0');
|
|
154
154
|
});
|
|
@@ -156,25 +156,25 @@ describe('tbody arrow key navigation', () => {
|
|
|
156
156
|
it('ArrowDown moves focus down one row', () => {
|
|
157
157
|
const tbody = focusTbody(wrapper);
|
|
158
158
|
keydown(tbody, 'ArrowDown');
|
|
159
|
-
expect(tbody.getAttribute('aria-activedescendant')).toBe('
|
|
159
|
+
expect(tbody.getAttribute('aria-activedescendant')).toBe('oc-cell-1-0');
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
it('ArrowDown does not go past last row', () => {
|
|
163
163
|
const tbody = focusTbody(wrapper);
|
|
164
164
|
for (let i = 0; i < 10; i++) keydown(tbody, 'ArrowDown');
|
|
165
|
-
expect(tbody.getAttribute('aria-activedescendant')).toBe('
|
|
165
|
+
expect(tbody.getAttribute('aria-activedescendant')).toBe('oc-cell-4-0');
|
|
166
166
|
});
|
|
167
167
|
|
|
168
168
|
it('ArrowRight moves focus right one column', () => {
|
|
169
169
|
const tbody = focusTbody(wrapper);
|
|
170
170
|
keydown(tbody, 'ArrowRight');
|
|
171
|
-
expect(tbody.getAttribute('aria-activedescendant')).toBe('
|
|
171
|
+
expect(tbody.getAttribute('aria-activedescendant')).toBe('oc-cell-0-1');
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
it('ArrowRight does not go past last column', () => {
|
|
175
175
|
const tbody = focusTbody(wrapper);
|
|
176
176
|
for (let i = 0; i < 10; i++) keydown(tbody, 'ArrowRight');
|
|
177
|
-
expect(tbody.getAttribute('aria-activedescendant')).toBe('
|
|
177
|
+
expect(tbody.getAttribute('aria-activedescendant')).toBe('oc-cell-0-2');
|
|
178
178
|
});
|
|
179
179
|
|
|
180
180
|
it('ArrowLeft moves focus left', () => {
|
|
@@ -182,13 +182,13 @@ describe('tbody arrow key navigation', () => {
|
|
|
182
182
|
keydown(tbody, 'ArrowRight');
|
|
183
183
|
keydown(tbody, 'ArrowRight');
|
|
184
184
|
keydown(tbody, 'ArrowLeft');
|
|
185
|
-
expect(tbody.getAttribute('aria-activedescendant')).toBe('
|
|
185
|
+
expect(tbody.getAttribute('aria-activedescendant')).toBe('oc-cell-0-1');
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
it('ArrowLeft does not go past first column', () => {
|
|
189
189
|
const tbody = focusTbody(wrapper);
|
|
190
190
|
keydown(tbody, 'ArrowLeft');
|
|
191
|
-
expect(tbody.getAttribute('aria-activedescendant')).toBe('
|
|
191
|
+
expect(tbody.getAttribute('aria-activedescendant')).toBe('oc-cell-0-0');
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
it('Home moves to first column in current row', () => {
|
|
@@ -196,19 +196,19 @@ describe('tbody arrow key navigation', () => {
|
|
|
196
196
|
keydown(tbody, 'ArrowRight');
|
|
197
197
|
keydown(tbody, 'ArrowRight');
|
|
198
198
|
keydown(tbody, 'Home');
|
|
199
|
-
expect(tbody.getAttribute('aria-activedescendant')).toBe('
|
|
199
|
+
expect(tbody.getAttribute('aria-activedescendant')).toBe('oc-cell-0-0');
|
|
200
200
|
});
|
|
201
201
|
|
|
202
202
|
it('End moves to last column in current row', () => {
|
|
203
203
|
const tbody = focusTbody(wrapper);
|
|
204
204
|
keydown(tbody, 'End');
|
|
205
|
-
expect(tbody.getAttribute('aria-activedescendant')).toBe('
|
|
205
|
+
expect(tbody.getAttribute('aria-activedescendant')).toBe('oc-cell-0-2');
|
|
206
206
|
});
|
|
207
207
|
|
|
208
208
|
it('navigation clears previous focus highlight', () => {
|
|
209
209
|
const tbody = focusTbody(wrapper);
|
|
210
210
|
keydown(tbody, 'ArrowDown');
|
|
211
|
-
const focused = wrapper.querySelectorAll('.
|
|
211
|
+
const focused = wrapper.querySelectorAll('.oc-table-cell-focus');
|
|
212
212
|
expect(focused.length).toBe(1);
|
|
213
213
|
expect(focused[0].textContent).toBe('R1C0');
|
|
214
214
|
});
|
|
@@ -274,7 +274,7 @@ describe('header keyboard navigation', () => {
|
|
|
274
274
|
keydown(headers[1] as HTMLElement, 'ArrowDown');
|
|
275
275
|
const tbody = wrapper.querySelector('tbody')!;
|
|
276
276
|
// The tbody should have activedescendant set to first row, column 1
|
|
277
|
-
expect(tbody.getAttribute('aria-activedescendant')).toBe('
|
|
277
|
+
expect(tbody.getAttribute('aria-activedescendant')).toBe('oc-cell-0-1');
|
|
278
278
|
});
|
|
279
279
|
});
|
|
280
280
|
|
|
@@ -305,19 +305,19 @@ describe('search escape handling', () => {
|
|
|
305
305
|
});
|
|
306
306
|
|
|
307
307
|
it('Escape in search input calls onClearSearch', () => {
|
|
308
|
-
const input = wrapper.querySelector('.
|
|
308
|
+
const input = wrapper.querySelector('.oc-table-search input')!;
|
|
309
309
|
keydown(input as HTMLElement, 'Escape');
|
|
310
310
|
expect(onClearSearch).toHaveBeenCalledTimes(1);
|
|
311
311
|
});
|
|
312
312
|
|
|
313
313
|
it('Escape in search announces "Search cleared"', () => {
|
|
314
|
-
const input = wrapper.querySelector('.
|
|
314
|
+
const input = wrapper.querySelector('.oc-table-search input')!;
|
|
315
315
|
keydown(input as HTMLElement, 'Escape');
|
|
316
316
|
expect(onAnnounce).toHaveBeenCalledWith('Search cleared');
|
|
317
317
|
});
|
|
318
318
|
|
|
319
319
|
it('non-Escape keys in search do not trigger clear', () => {
|
|
320
|
-
const input = wrapper.querySelector('.
|
|
320
|
+
const input = wrapper.querySelector('.oc-table-search input')!;
|
|
321
321
|
keydown(input as HTMLElement, 'a');
|
|
322
322
|
keydown(input as HTMLElement, 'Enter');
|
|
323
323
|
expect(onClearSearch).not.toHaveBeenCalled();
|
|
@@ -347,13 +347,13 @@ describe('cleanup', () => {
|
|
|
347
347
|
tbody.dispatchEvent(new Event('focus'));
|
|
348
348
|
keydown(tbody, 'ArrowDown');
|
|
349
349
|
|
|
350
|
-
const input = wrapper.querySelector('.
|
|
350
|
+
const input = wrapper.querySelector('.oc-table-search input')!;
|
|
351
351
|
keydown(input as HTMLElement, 'Escape');
|
|
352
352
|
|
|
353
353
|
expect(onClearSearch).not.toHaveBeenCalled();
|
|
354
354
|
|
|
355
355
|
// Focus highlight should be cleared
|
|
356
|
-
const focused = wrapper.querySelectorAll('.
|
|
356
|
+
const focused = wrapper.querySelectorAll('.oc-table-cell-focus');
|
|
357
357
|
expect(focused.length).toBe(0);
|
|
358
358
|
|
|
359
359
|
document.body.innerHTML = '';
|