@opendata-ai/openchart-vanilla 8.0.0-rc.2 → 8.0.0-rc.21

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 (145) hide show
  1. package/dist/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
  2. package/dist/chunk-DCODCLWI.js +234 -0
  3. package/dist/chunk-DCODCLWI.js.map +1 -0
  4. package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
  5. package/dist/chunk-EKAUQAAX.js.map +1 -0
  6. package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
  7. package/dist/chunk-KNT5AZ2O.js.map +1 -0
  8. package/dist/chunk-RM5XS6NR.js +113 -0
  9. package/dist/chunk-RM5XS6NR.js.map +1 -0
  10. package/dist/chunk-T32UHMZA.js +282 -0
  11. package/dist/chunk-T32UHMZA.js.map +1 -0
  12. package/dist/export-gif.d.ts +113 -0
  13. package/dist/export-gif.js +21 -0
  14. package/dist/export-gif.js.map +1 -0
  15. package/dist/index.d.ts +283 -24
  16. package/dist/index.js +2012 -763
  17. package/dist/index.js.map +1 -1
  18. package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
  19. package/dist/simulation-worker.js +83 -2
  20. package/dist/static.js +7 -69
  21. package/dist/static.js.map +1 -1
  22. package/dist/story/index.d.ts +3 -2
  23. package/dist/story/index.js +5 -3
  24. package/dist/story/index.js.map +1 -1
  25. package/dist/styles.css +1 -1
  26. package/dist/styles.css.map +1 -1
  27. package/package.json +14 -6
  28. package/dist/chunk-5EOV5JQZ.js.map +0 -1
  29. package/dist/chunk-ENXSUXC5.js.map +0 -1
  30. package/src/__test-fixtures__/dom.ts +0 -70
  31. package/src/__test-fixtures__/specs.ts +0 -214
  32. package/src/__tests__/animation.test.ts +0 -358
  33. package/src/__tests__/annotations-render.test.ts +0 -201
  34. package/src/__tests__/compound-labels.test.ts +0 -122
  35. package/src/__tests__/continuous-legend.test.ts +0 -91
  36. package/src/__tests__/crosshair.test.ts +0 -130
  37. package/src/__tests__/edit-events.test.ts +0 -902
  38. package/src/__tests__/events.test.ts +0 -387
  39. package/src/__tests__/export.test.ts +0 -241
  40. package/src/__tests__/font-race.test.ts +0 -199
  41. package/src/__tests__/gradient-ids.test.ts +0 -159
  42. package/src/__tests__/gradient-utils.test.ts +0 -125
  43. package/src/__tests__/known-bugs-render.test.ts +0 -183
  44. package/src/__tests__/map.test.ts +0 -293
  45. package/src/__tests__/mount.test.ts +0 -357
  46. package/src/__tests__/pattern-fills.test.ts +0 -105
  47. package/src/__tests__/resize-timing.test.ts +0 -218
  48. package/src/__tests__/sankey.test.ts +0 -133
  49. package/src/__tests__/selection-events.test.ts +0 -869
  50. package/src/__tests__/series-search.test.ts +0 -311
  51. package/src/__tests__/static.test.ts +0 -141
  52. package/src/__tests__/svg-renderer.test.ts +0 -895
  53. package/src/__tests__/table-keyboard.test.ts +0 -361
  54. package/src/__tests__/table-mount.test.ts +0 -639
  55. package/src/__tests__/tilemap.test.ts +0 -158
  56. package/src/__tests__/tooltip.test.ts +0 -337
  57. package/src/__tests__/transition.test.ts +0 -1777
  58. package/src/__tests__/you-draw-it.test.ts +0 -273
  59. package/src/animation-vars.ts +0 -42
  60. package/src/animation.ts +0 -76
  61. package/src/barlist-mount.ts +0 -383
  62. package/src/barlist-renderer.ts +0 -267
  63. package/src/dom-helpers.ts +0 -17
  64. package/src/export.ts +0 -482
  65. package/src/gradient-utils.ts +0 -138
  66. package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
  67. package/src/graph/__tests__/graph-mount.test.ts +0 -213
  68. package/src/graph/__tests__/interaction.test.ts +0 -205
  69. package/src/graph/__tests__/keyboard.test.ts +0 -653
  70. package/src/graph/__tests__/search.test.ts +0 -88
  71. package/src/graph/__tests__/simulation.test.ts +0 -233
  72. package/src/graph/__tests__/spatial-index.test.ts +0 -142
  73. package/src/graph/__tests__/zoom.test.ts +0 -203
  74. package/src/graph/canvas-renderer.ts +0 -747
  75. package/src/graph/interaction.ts +0 -367
  76. package/src/graph/keyboard.ts +0 -208
  77. package/src/graph/search.ts +0 -50
  78. package/src/graph/simulation-worker-url.ts +0 -12
  79. package/src/graph/simulation-worker.ts +0 -278
  80. package/src/graph/simulation.ts +0 -422
  81. package/src/graph/spatial-index.ts +0 -121
  82. package/src/graph/types.ts +0 -47
  83. package/src/graph/worker-protocol.ts +0 -70
  84. package/src/graph/zoom.ts +0 -134
  85. package/src/graph-mount.ts +0 -888
  86. package/src/index.ts +0 -76
  87. package/src/interactions/chart-events.ts +0 -139
  88. package/src/interactions/crosshair.ts +0 -233
  89. package/src/interactions/drag-handler.ts +0 -175
  90. package/src/interactions/editing-drags.ts +0 -515
  91. package/src/interactions/index.ts +0 -25
  92. package/src/interactions/keyboard-nav.ts +0 -111
  93. package/src/interactions/legend-interaction.ts +0 -46
  94. package/src/interactions/selection.ts +0 -280
  95. package/src/interactions/tooltip-events.ts +0 -72
  96. package/src/map-camera.ts +0 -88
  97. package/src/map-mount.ts +0 -701
  98. package/src/map-renderer.ts +0 -338
  99. package/src/map-transition.ts +0 -83
  100. package/src/measure-text.ts +0 -134
  101. package/src/mount.ts +0 -1382
  102. package/src/pattern-utils.ts +0 -110
  103. package/src/renderers/annotations.ts +0 -388
  104. package/src/renderers/axes.ts +0 -351
  105. package/src/renderers/brand.ts +0 -87
  106. package/src/renderers/chrome.ts +0 -196
  107. package/src/renderers/endpoint-labels.ts +0 -150
  108. package/src/renderers/legend.ts +0 -311
  109. package/src/renderers/marks.ts +0 -561
  110. package/src/renderers/metrics.ts +0 -63
  111. package/src/renderers/svg-dom.ts +0 -40
  112. package/src/renderers/table-cells.ts +0 -446
  113. package/src/resize-observer.ts +0 -46
  114. package/src/sankey-mount.ts +0 -622
  115. package/src/sankey-renderer.ts +0 -614
  116. package/src/series-search.ts +0 -392
  117. package/src/static.ts +0 -251
  118. package/src/story/__tests__/camera-math.test.ts +0 -145
  119. package/src/story/__tests__/create-chart-story.test.ts +0 -317
  120. package/src/story/__tests__/progress-math.test.ts +0 -95
  121. package/src/story/__tests__/scroll-driver.test.ts +0 -130
  122. package/src/story/__tests__/tween.test.ts +0 -159
  123. package/src/story/camera-math.ts +0 -125
  124. package/src/story/create-chart-story.ts +0 -353
  125. package/src/story/crossfade.ts +0 -70
  126. package/src/story/index.ts +0 -39
  127. package/src/story/progress-math.ts +0 -85
  128. package/src/story/resolve-camera-target.ts +0 -119
  129. package/src/story/scroll-driver.ts +0 -206
  130. package/src/story/story-camera.ts +0 -49
  131. package/src/story/tween.ts +0 -112
  132. package/src/story/types.ts +0 -76
  133. package/src/svg-ids.ts +0 -23
  134. package/src/svg-renderer.ts +0 -383
  135. package/src/table-keyboard.ts +0 -266
  136. package/src/table-mount.ts +0 -567
  137. package/src/table-renderer.ts +0 -414
  138. package/src/text-edit-overlay.ts +0 -255
  139. package/src/theme-tokens.ts +0 -51
  140. package/src/tilemap-mount.ts +0 -511
  141. package/src/tilemap-renderer.ts +0 -396
  142. package/src/tooltip.ts +0 -177
  143. package/src/transition.ts +0 -2816
  144. package/src/you-draw-it.ts +0 -480
  145. /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
@@ -1,387 +0,0 @@
1
- import type { ChartSpec } from '@opendata-ai/openchart-engine';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { createContainer, createMouseEvent } from '../__test-fixtures__/dom';
4
- import { barSpec, lineSpec } from '../__test-fixtures__/specs';
5
- import { createChart } from '../mount';
6
-
7
- // ---------------------------------------------------------------------------
8
- // Test data
9
- // ---------------------------------------------------------------------------
10
-
11
- const annotatedSpec: ChartSpec = {
12
- ...barSpec,
13
- annotations: [{ type: 'refline', y: 0, label: 'Zero' }],
14
- };
15
-
16
- const textAnnotatedSpec: ChartSpec = {
17
- ...barSpec,
18
- annotations: [{ type: 'text', x: 10, y: 'A', text: 'Peak', offset: { dx: 10, dy: -20 } }],
19
- };
20
-
21
- // ---------------------------------------------------------------------------
22
- // Tests
23
- // ---------------------------------------------------------------------------
24
-
25
- describe('chart event handlers', () => {
26
- let container: HTMLDivElement;
27
-
28
- beforeEach(() => {
29
- container = createContainer();
30
- });
31
-
32
- afterEach(() => {
33
- document.body.innerHTML = '';
34
- });
35
-
36
- describe('onMarkClick', () => {
37
- it('fires with correct data when a mark element is clicked', () => {
38
- const onMarkClick = vi.fn();
39
- const chart = createChart(container, barSpec, { onMarkClick });
40
-
41
- // Find a rect mark element (bar chart renders rect marks)
42
- const mark = container.querySelector('[data-mark-id]');
43
- expect(mark).not.toBeNull();
44
-
45
- mark!.dispatchEvent(createMouseEvent('click', 150, 200));
46
-
47
- expect(onMarkClick).toHaveBeenCalledTimes(1);
48
- const event = onMarkClick.mock.calls[0][0];
49
- expect(event.datum).toBeDefined();
50
- expect(event.position).toBeDefined();
51
- expect(event.position.x).toBeTypeOf('number');
52
- expect(event.position.y).toBeTypeOf('number');
53
- expect(event.event).toBeInstanceOf(MouseEvent);
54
-
55
- chart.destroy();
56
- });
57
-
58
- it('includes series info for multi-series charts', () => {
59
- const onMarkClick = vi.fn();
60
- const chart = createChart(container, lineSpec, { onMarkClick });
61
-
62
- // Line charts have marks with data-series attribute
63
- const marks = container.querySelectorAll('[data-mark-id]');
64
- expect(marks.length).toBeGreaterThan(0);
65
-
66
- marks[0].dispatchEvent(createMouseEvent('click'));
67
-
68
- expect(onMarkClick).toHaveBeenCalledTimes(1);
69
- const event = onMarkClick.mock.calls[0][0];
70
- // Line marks should have series info
71
- expect(event.series).toBeDefined();
72
-
73
- chart.destroy();
74
- });
75
- });
76
-
77
- describe('onMarkHover', () => {
78
- it('fires on mouseenter of a mark element', () => {
79
- const onMarkHover = vi.fn();
80
- const chart = createChart(container, barSpec, { onMarkHover });
81
-
82
- const mark = container.querySelector('[data-mark-id]');
83
- expect(mark).not.toBeNull();
84
-
85
- mark!.dispatchEvent(createMouseEvent('mouseenter', 120, 180));
86
-
87
- expect(onMarkHover).toHaveBeenCalledTimes(1);
88
- const event = onMarkHover.mock.calls[0][0];
89
- expect(event.datum).toBeDefined();
90
- expect(event.position).toBeDefined();
91
-
92
- chart.destroy();
93
- });
94
- });
95
-
96
- describe('onMarkLeave', () => {
97
- it('fires on mouseleave of a mark element', () => {
98
- const onMarkLeave = vi.fn();
99
- const chart = createChart(container, barSpec, { onMarkLeave });
100
-
101
- const mark = container.querySelector('[data-mark-id]');
102
- expect(mark).not.toBeNull();
103
-
104
- mark!.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
105
-
106
- expect(onMarkLeave).toHaveBeenCalledTimes(1);
107
- // onMarkLeave receives no arguments
108
- expect(onMarkLeave.mock.calls[0]).toHaveLength(0);
109
-
110
- chart.destroy();
111
- });
112
- });
113
-
114
- describe('onLegendToggle', () => {
115
- it('fires when a legend entry is clicked, and toggling back restores the same series', () => {
116
- const onLegendToggle = vi.fn();
117
- const chart = createChart(
118
- container,
119
- { ...lineSpec, legend: { show: true } },
120
- {
121
- onLegendToggle,
122
- },
123
- );
124
-
125
- const firstEntry = container.querySelector('[data-legend-index]') as HTMLElement | null;
126
- expect(firstEntry).not.toBeNull();
127
- const targetLabel = firstEntry!.getAttribute('data-legend-label')!;
128
- expect(targetLabel).toBeTypeOf('string');
129
-
130
- firstEntry!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
131
-
132
- expect(onLegendToggle).toHaveBeenCalledTimes(1);
133
- const [hiddenSeries, visibleAfterFirst] = onLegendToggle.mock.calls[0];
134
- expect(hiddenSeries).toBe(targetLabel);
135
- expect(visibleAfterFirst).toBe(false);
136
-
137
- // Toggling triggers a recompile (so the y-scale rebalances and any
138
- // endpoint labels for the hidden series clear), which replaces the
139
- // legend DOM. Find the same entry by label and click it again.
140
- const sameEntry = container.querySelector(
141
- `[data-legend-label="${targetLabel}"]`,
142
- ) as HTMLElement | null;
143
- expect(sameEntry).not.toBeNull();
144
- sameEntry!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
145
-
146
- expect(onLegendToggle).toHaveBeenCalledTimes(2);
147
- const [shownSeries, visibleAfterSecond] = onLegendToggle.mock.calls[1];
148
- expect(shownSeries).toBe(targetLabel); // same series, not the other one
149
- expect(visibleAfterSecond).toBe(true);
150
-
151
- chart.destroy();
152
- });
153
-
154
- it('refuses to hide the last visible series', () => {
155
- // Two-series chart: hide one, then attempt to hide the other.
156
- // The second click should be a no-op (no toggle callback for the hide).
157
- const onLegendToggle = vi.fn();
158
- const chart = createChart(
159
- container,
160
- { ...lineSpec, legend: { show: true } },
161
- { onLegendToggle },
162
- );
163
-
164
- const entries = container.querySelectorAll('[data-legend-label]');
165
- expect(entries.length).toBeGreaterThanOrEqual(2);
166
- const firstLabel = (entries[0] as HTMLElement).getAttribute('data-legend-label')!;
167
-
168
- // Hide the first one.
169
- (entries[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }));
170
- expect(onLegendToggle).toHaveBeenCalledTimes(1);
171
-
172
- // Click the *other* (still-visible) series. The guard should refuse.
173
- const remaining = Array.from(container.querySelectorAll('[data-legend-label]')).find(
174
- (el) => (el as HTMLElement).getAttribute('data-legend-label') !== firstLabel,
175
- ) as HTMLElement | undefined;
176
- expect(remaining).toBeTruthy();
177
- remaining!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
178
-
179
- // The handler still fires (the click bubbled and the wired listener ran),
180
- // but the visible flag should report that nothing was actually hidden.
181
- // Implementation detail: we expect the second call to report visible=true
182
- // (the series is still visible because the toggle was refused).
183
- expect(onLegendToggle).toHaveBeenCalledTimes(2);
184
- const [, visibleAfter] = onLegendToggle.mock.calls[1];
185
- expect(visibleAfter).toBe(true);
186
-
187
- chart.destroy();
188
- });
189
- });
190
-
191
- describe('onAnnotationClick', () => {
192
- it('fires when an annotation element is clicked', () => {
193
- const onAnnotationClick = vi.fn();
194
- const chart = createChart(container, annotatedSpec, { onAnnotationClick });
195
-
196
- const annotation = container.querySelector('.oc-annotation');
197
- if (!annotation) {
198
- // Refline at y=0 may resolve outside the chart area in test env
199
- chart.destroy();
200
- return;
201
- }
202
-
203
- annotation.dispatchEvent(createMouseEvent('click'));
204
-
205
- expect(onAnnotationClick).toHaveBeenCalledTimes(1);
206
- const [annotationArg, mouseEvent] = onAnnotationClick.mock.calls[0];
207
- expect(annotationArg).toBeDefined();
208
- expect(annotationArg.type).toBe('refline');
209
- expect(mouseEvent).toBeInstanceOf(MouseEvent);
210
-
211
- chart.destroy();
212
- });
213
- });
214
-
215
- describe('onAnnotationEdit', () => {
216
- it('sets cursor:grab on text annotations when onAnnotationEdit is provided', () => {
217
- const onAnnotationEdit = vi.fn();
218
- const chart = createChart(container, textAnnotatedSpec, { onAnnotationEdit });
219
-
220
- const annotation = container.querySelector('.oc-annotation-text') as SVGGElement | null;
221
- if (!annotation) {
222
- chart.destroy();
223
- return;
224
- }
225
-
226
- expect(annotation.style.cursor).toBe('grab');
227
- chart.destroy();
228
- });
229
-
230
- it('does not set cursor:grab on non-text annotations', () => {
231
- const onAnnotationEdit = vi.fn();
232
- const chart = createChart(container, annotatedSpec, { onAnnotationEdit });
233
-
234
- const annotation = container.querySelector('.oc-annotation-refline') as SVGGElement | null;
235
- if (!annotation) {
236
- chart.destroy();
237
- return;
238
- }
239
-
240
- expect(annotation.style.cursor).not.toBe('grab');
241
- chart.destroy();
242
- });
243
-
244
- it('fires with original annotation and updated offset after drag', () => {
245
- const onAnnotationEdit = vi.fn();
246
- const chart = createChart(container, textAnnotatedSpec, { onAnnotationEdit });
247
-
248
- const annotation = container.querySelector('.oc-annotation-text') as SVGGElement | null;
249
- if (!annotation) {
250
- chart.destroy();
251
- return;
252
- }
253
-
254
- // Simulate drag: mousedown -> mousemove -> mouseup
255
- annotation.dispatchEvent(createMouseEvent('mousedown', 100, 100));
256
- document.dispatchEvent(createMouseEvent('mousemove', 150, 130));
257
- document.dispatchEvent(createMouseEvent('mouseup', 150, 130));
258
-
259
- expect(onAnnotationEdit).toHaveBeenCalledTimes(1);
260
- const [annotationArg, offsetArg] = onAnnotationEdit.mock.calls[0];
261
- expect(annotationArg.type).toBe('text');
262
- expect(annotationArg.text).toBe('Peak');
263
- // Original offset (10, -20) + drag delta (50, 30) = (60, 10)
264
- expect(offsetArg.dx).toBe(60);
265
- expect(offsetArg.dy).toBe(10);
266
-
267
- chart.destroy();
268
- });
269
-
270
- it('does not fire when drag movement is below threshold', () => {
271
- const onAnnotationEdit = vi.fn();
272
- const chart = createChart(container, textAnnotatedSpec, { onAnnotationEdit });
273
-
274
- const annotation = container.querySelector('.oc-annotation-text') as SVGGElement | null;
275
- if (!annotation) {
276
- chart.destroy();
277
- return;
278
- }
279
-
280
- // Simulate drag with minimal movement (< 3px)
281
- annotation.dispatchEvent(createMouseEvent('mousedown', 100, 100));
282
- document.dispatchEvent(createMouseEvent('mousemove', 101, 101));
283
- document.dispatchEvent(createMouseEvent('mouseup', 101, 101));
284
-
285
- expect(onAnnotationEdit).not.toHaveBeenCalled();
286
- chart.destroy();
287
- });
288
-
289
- it('suppresses click after drag', () => {
290
- const onAnnotationClick = vi.fn();
291
- const onAnnotationEdit = vi.fn();
292
- const chart = createChart(container, textAnnotatedSpec, {
293
- onAnnotationClick,
294
- onAnnotationEdit,
295
- });
296
-
297
- const annotation = container.querySelector('.oc-annotation-text') as SVGGElement | null;
298
- if (!annotation) {
299
- chart.destroy();
300
- return;
301
- }
302
-
303
- // Drag > 3px
304
- annotation.dispatchEvent(createMouseEvent('mousedown', 100, 100));
305
- document.dispatchEvent(createMouseEvent('mousemove', 150, 130));
306
- document.dispatchEvent(createMouseEvent('mouseup', 150, 130));
307
-
308
- // Fire a click after the drag
309
- annotation.dispatchEvent(createMouseEvent('click', 150, 130));
310
-
311
- expect(onAnnotationEdit).toHaveBeenCalledTimes(1);
312
- // Click should have been suppressed by the one-time capture handler
313
- expect(onAnnotationClick).not.toHaveBeenCalled();
314
-
315
- chart.destroy();
316
- });
317
-
318
- it('applies transform during drag', () => {
319
- const onAnnotationEdit = vi.fn();
320
- const chart = createChart(container, textAnnotatedSpec, { onAnnotationEdit });
321
-
322
- const annotation = container.querySelector('.oc-annotation-text') as SVGGElement | null;
323
- if (!annotation) {
324
- chart.destroy();
325
- return;
326
- }
327
-
328
- annotation.dispatchEvent(createMouseEvent('mousedown', 100, 100));
329
- document.dispatchEvent(createMouseEvent('mousemove', 140, 120));
330
-
331
- // During drag, the annotation group should have a transform
332
- const transform = annotation.getAttribute('transform');
333
- expect(transform).toContain('translate');
334
-
335
- // Complete the drag to clean up
336
- document.dispatchEvent(createMouseEvent('mouseup', 140, 120));
337
- chart.destroy();
338
- });
339
-
340
- it('cleans up listeners on destroy', () => {
341
- const onAnnotationEdit = vi.fn();
342
- const chart = createChart(container, textAnnotatedSpec, { onAnnotationEdit });
343
-
344
- chart.destroy();
345
-
346
- // After destroy, dispatching events should not cause errors
347
- document.dispatchEvent(createMouseEvent('mousemove', 200, 200));
348
- document.dispatchEvent(createMouseEvent('mouseup', 200, 200));
349
-
350
- expect(onAnnotationEdit).not.toHaveBeenCalled();
351
- });
352
- });
353
-
354
- describe('cleanup', () => {
355
- it('removes event listeners on destroy', () => {
356
- const onMarkClick = vi.fn();
357
- const chart = createChart(container, barSpec, { onMarkClick });
358
-
359
- const mark = container.querySelector('[data-mark-id]');
360
- expect(mark).not.toBeNull();
361
-
362
- chart.destroy();
363
-
364
- // After destroy, clicking should not fire the handler
365
- // (the SVG is removed, but we verify the handler reference is cleaned up)
366
- expect(onMarkClick).not.toHaveBeenCalled();
367
- });
368
-
369
- it('removes event listeners on update', () => {
370
- const onMarkClick = vi.fn();
371
- const chart = createChart(container, barSpec, { onMarkClick });
372
-
373
- // Update triggers a re-render which should clean up old listeners
374
- chart.update(lineSpec);
375
-
376
- // Find mark in the new render
377
- const mark = container.querySelector('[data-mark-id]');
378
- expect(mark).not.toBeNull();
379
-
380
- // Click the new mark - should still fire since new listeners were wired
381
- mark!.dispatchEvent(createMouseEvent('click'));
382
- expect(onMarkClick).toHaveBeenCalledTimes(1);
383
-
384
- chart.destroy();
385
- });
386
- });
387
- });
@@ -1,241 +0,0 @@
1
- /**
2
- * Export utility tests.
3
- *
4
- * Tests exportSVG, exportSVGWithFonts, exportCSV, exportPNG, and exportJPG
5
- * functions directly, verifying SVG string validity, font embedding,
6
- * dimension parsing, CSV formatting, and raster export interfaces.
7
- */
8
-
9
- import type { 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 { barSpec, lineSpec } from '../__test-fixtures__/specs';
14
- import { exportCSV, exportJPG, exportPNG, exportSVG, exportSVGWithFonts } from '../export';
15
- import { renderChartSVG } from '../svg-renderer';
16
-
17
- // ---------------------------------------------------------------------------
18
- // Helpers
19
- // ---------------------------------------------------------------------------
20
-
21
- const COMPILE_OPTS: CompileOptions = { width: 600, height: 400 };
22
-
23
- function renderToSVG(spec = lineSpec) {
24
- const container = createContainer();
25
- const layout = compileChart(spec, COMPILE_OPTS);
26
- return renderChartSVG(layout, container);
27
- }
28
-
29
- afterEach(() => {
30
- document.body.innerHTML = '';
31
- });
32
-
33
- // ---------------------------------------------------------------------------
34
- // exportSVG
35
- // ---------------------------------------------------------------------------
36
-
37
- describe('exportSVG', () => {
38
- it('returns a string starting with <svg', () => {
39
- const svg = renderToSVG();
40
- const result = exportSVG(svg);
41
- expect(result.startsWith('<svg')).toBe(true);
42
- });
43
-
44
- it('returned string contains viewBox attribute', () => {
45
- const svg = renderToSVG();
46
- const result = exportSVG(svg);
47
- expect(result).toContain('viewBox="0 0 600 400"');
48
- });
49
-
50
- it('returned string contains chart chrome text content', () => {
51
- const svg = renderToSVG(lineSpec);
52
- const result = exportSVG(svg);
53
- expect(result).toContain('GDP Growth');
54
- expect(result).toContain('US vs UK over time');
55
- expect(result).toContain('World Bank');
56
- });
57
-
58
- it('returned string contains SVG namespace', () => {
59
- const svg = renderToSVG();
60
- const result = exportSVG(svg);
61
- expect(result).toContain('xmlns');
62
- expect(result).toContain('http://www.w3.org/2000/svg');
63
- });
64
-
65
- it('returned string includes mark elements', () => {
66
- const svg = renderToSVG(barSpec);
67
- const result = exportSVG(svg);
68
- // Bar chart should have rect elements
69
- expect(result).toContain('<rect');
70
- expect(result).toContain('oc-mark-rect');
71
- });
72
-
73
- it('returned string is well-formed XML (ends with closing svg tag)', () => {
74
- const svg = renderToSVG();
75
- const result = exportSVG(svg);
76
- expect(result.endsWith('</svg>')).toBe(true);
77
- });
78
- });
79
-
80
- // ---------------------------------------------------------------------------
81
- // exportSVGWithFonts
82
- // ---------------------------------------------------------------------------
83
-
84
- describe('exportSVGWithFonts', () => {
85
- it('returns a promise', () => {
86
- const svg = renderToSVG();
87
- const result = exportSVGWithFonts(svg);
88
- expect(result).toBeInstanceOf(Promise);
89
- });
90
-
91
- it('resolves to a valid SVG string', async () => {
92
- const svg = renderToSVG();
93
- const result = await exportSVGWithFonts(svg);
94
- expect(result.startsWith('<svg')).toBe(true);
95
- expect(result.endsWith('</svg>')).toBe(true);
96
- });
97
-
98
- it('skips font embedding when embedFonts is false', async () => {
99
- const svg = renderToSVG();
100
- const result = await exportSVGWithFonts(svg, { embedFonts: false });
101
- // Should not contain @font-face (no stylesheets in test env anyway)
102
- expect(result).not.toContain('@font-face');
103
- });
104
-
105
- it('produces valid SVG even without stylesheets in the document', async () => {
106
- const svg = renderToSVG();
107
- // In test env, no Google Fonts stylesheets exist, so font collection
108
- // should gracefully return nothing and the export should still work
109
- const result = await exportSVGWithFonts(svg);
110
- expect(result).toContain('viewBox="0 0 600 400"');
111
- });
112
- });
113
-
114
- // ---------------------------------------------------------------------------
115
- // Dimension parsing (via exportPNG which uses getSVGDimensions internally)
116
- // ---------------------------------------------------------------------------
117
-
118
- describe('dimension parsing', () => {
119
- it('exportPNG reads dimensions from viewBox when width/height are absent', () => {
120
- const svg = renderToSVG();
121
- // Verify the SVG has viewBox but no explicit width/height
122
- expect(svg.getAttribute('viewBox')).toBe('0 0 600 400');
123
- expect(svg.getAttribute('width')).toBeNull();
124
- // exportPNG should still work (not fall back to 600x400 by accident)
125
- const result = exportPNG(svg, { dpi: 1, embedFonts: false });
126
- expect(result).toBeInstanceOf(Promise);
127
- result.catch(() => {}); // happy-dom canvas limitations
128
- });
129
- });
130
-
131
- // ---------------------------------------------------------------------------
132
- // exportCSV
133
- // ---------------------------------------------------------------------------
134
-
135
- describe('exportCSV', () => {
136
- it('returns empty string for empty data', () => {
137
- const result = exportCSV([]);
138
- expect(result).toBe('');
139
- });
140
-
141
- it('first line contains column headers from data keys', () => {
142
- const data = [
143
- { name: 'Alice', age: 30, city: 'Portland' },
144
- { name: 'Bob', age: 25, city: 'Seattle' },
145
- ];
146
- const result = exportCSV(data);
147
- const firstLine = result.split('\n')[0];
148
- expect(firstLine).toBe('name,age,city');
149
- });
150
-
151
- it('data rows follow header line', () => {
152
- const data = [
153
- { name: 'Alice', age: 30 },
154
- { name: 'Bob', age: 25 },
155
- ];
156
- const result = exportCSV(data);
157
- const lines = result.split('\n');
158
- expect(lines.length).toBe(3); // 1 header + 2 data rows
159
- expect(lines[1]).toBe('Alice,30');
160
- expect(lines[2]).toBe('Bob,25');
161
- });
162
-
163
- it('escapes values containing commas', () => {
164
- const data = [{ name: 'Doe, John', value: 42 }];
165
- const result = exportCSV(data);
166
- const lines = result.split('\n');
167
- // Comma in value should be wrapped in double quotes
168
- expect(lines[1]).toBe('"Doe, John",42');
169
- });
170
-
171
- it('escapes values containing double quotes', () => {
172
- const data = [{ name: 'Say "hi"', value: 1 }];
173
- const result = exportCSV(data);
174
- const lines = result.split('\n');
175
- // Double quotes should be doubled and the field wrapped in quotes
176
- expect(lines[1]).toBe('"Say ""hi""",1');
177
- });
178
-
179
- it('escapes values containing newlines', () => {
180
- const data = [{ name: 'Line1\nLine2', value: 1 }];
181
- const result = exportCSV(data);
182
- // The entire field should be quoted since it contains a newline
183
- expect(result).toContain('"Line1\nLine2"');
184
- });
185
-
186
- it('handles undefined and null values as empty strings', () => {
187
- const data = [{ a: undefined, b: null, c: 'ok' }];
188
- const result = exportCSV(data);
189
- const lines = result.split('\n');
190
- // undefined and null should render as empty
191
- expect(lines[1]).toBe(',,ok');
192
- });
193
-
194
- it('uses keys from first row as headers', () => {
195
- const data = [
196
- { x: 1, y: 2, z: 3 },
197
- { x: 4, y: 5, z: 6 },
198
- ];
199
- const result = exportCSV(data);
200
- expect(result.split('\n')[0]).toBe('x,y,z');
201
- });
202
- });
203
-
204
- // ---------------------------------------------------------------------------
205
- // exportPNG
206
- // ---------------------------------------------------------------------------
207
-
208
- describe('exportPNG', () => {
209
- it('returns a Promise when called with a rendered SVG element', () => {
210
- const svg = renderToSVG();
211
- const result = exportPNG(svg, { embedFonts: false });
212
- expect(result).toBeInstanceOf(Promise);
213
- result.catch(() => {});
214
- });
215
- });
216
-
217
- // ---------------------------------------------------------------------------
218
- // exportJPG
219
- // ---------------------------------------------------------------------------
220
-
221
- describe('exportJPG', () => {
222
- it('is a function that accepts an SVG element and options', () => {
223
- expect(typeof exportJPG).toBe('function');
224
- expect(exportJPG.length).toBeGreaterThanOrEqual(1);
225
- });
226
-
227
- it('returns a Promise when called with a rendered SVG element', () => {
228
- const svg = renderToSVG();
229
- const result = exportJPG(svg, { embedFonts: false });
230
- expect(result).toBeInstanceOf(Promise);
231
- // Clean up: catch any rejection from happy-dom canvas limitations
232
- result.catch(() => {});
233
- });
234
-
235
- it('accepts quality option between 0 and 1', () => {
236
- const svg = renderToSVG();
237
- const result = exportJPG(svg, { quality: 0.5, dpi: 1, embedFonts: false });
238
- expect(result).toBeInstanceOf(Promise);
239
- result.catch(() => {});
240
- });
241
- });