@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.
- package/dist/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
- package/dist/chunk-DCODCLWI.js +234 -0
- package/dist/chunk-DCODCLWI.js.map +1 -0
- package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
- package/dist/chunk-EKAUQAAX.js.map +1 -0
- package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
- package/dist/chunk-KNT5AZ2O.js.map +1 -0
- package/dist/chunk-RM5XS6NR.js +113 -0
- package/dist/chunk-RM5XS6NR.js.map +1 -0
- package/dist/chunk-T32UHMZA.js +282 -0
- package/dist/chunk-T32UHMZA.js.map +1 -0
- package/dist/export-gif.d.ts +113 -0
- package/dist/export-gif.js +21 -0
- package/dist/export-gif.js.map +1 -0
- package/dist/index.d.ts +283 -24
- package/dist/index.js +2012 -763
- package/dist/index.js.map +1 -1
- package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
- package/dist/simulation-worker.js +83 -2
- package/dist/static.js +7 -69
- package/dist/static.js.map +1 -1
- package/dist/story/index.d.ts +3 -2
- package/dist/story/index.js +5 -3
- package/dist/story/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +14 -6
- package/dist/chunk-5EOV5JQZ.js.map +0 -1
- package/dist/chunk-ENXSUXC5.js.map +0 -1
- package/src/__test-fixtures__/dom.ts +0 -70
- package/src/__test-fixtures__/specs.ts +0 -214
- package/src/__tests__/animation.test.ts +0 -358
- package/src/__tests__/annotations-render.test.ts +0 -201
- package/src/__tests__/compound-labels.test.ts +0 -122
- package/src/__tests__/continuous-legend.test.ts +0 -91
- package/src/__tests__/crosshair.test.ts +0 -130
- package/src/__tests__/edit-events.test.ts +0 -902
- package/src/__tests__/events.test.ts +0 -387
- package/src/__tests__/export.test.ts +0 -241
- package/src/__tests__/font-race.test.ts +0 -199
- package/src/__tests__/gradient-ids.test.ts +0 -159
- package/src/__tests__/gradient-utils.test.ts +0 -125
- package/src/__tests__/known-bugs-render.test.ts +0 -183
- package/src/__tests__/map.test.ts +0 -293
- package/src/__tests__/mount.test.ts +0 -357
- package/src/__tests__/pattern-fills.test.ts +0 -105
- package/src/__tests__/resize-timing.test.ts +0 -218
- package/src/__tests__/sankey.test.ts +0 -133
- package/src/__tests__/selection-events.test.ts +0 -869
- package/src/__tests__/series-search.test.ts +0 -311
- package/src/__tests__/static.test.ts +0 -141
- package/src/__tests__/svg-renderer.test.ts +0 -895
- package/src/__tests__/table-keyboard.test.ts +0 -361
- package/src/__tests__/table-mount.test.ts +0 -639
- package/src/__tests__/tilemap.test.ts +0 -158
- package/src/__tests__/tooltip.test.ts +0 -337
- package/src/__tests__/transition.test.ts +0 -1777
- package/src/__tests__/you-draw-it.test.ts +0 -273
- package/src/animation-vars.ts +0 -42
- package/src/animation.ts +0 -76
- package/src/barlist-mount.ts +0 -383
- package/src/barlist-renderer.ts +0 -267
- package/src/dom-helpers.ts +0 -17
- package/src/export.ts +0 -482
- package/src/gradient-utils.ts +0 -138
- package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
- package/src/graph/__tests__/graph-mount.test.ts +0 -213
- package/src/graph/__tests__/interaction.test.ts +0 -205
- package/src/graph/__tests__/keyboard.test.ts +0 -653
- package/src/graph/__tests__/search.test.ts +0 -88
- package/src/graph/__tests__/simulation.test.ts +0 -233
- package/src/graph/__tests__/spatial-index.test.ts +0 -142
- package/src/graph/__tests__/zoom.test.ts +0 -203
- package/src/graph/canvas-renderer.ts +0 -747
- package/src/graph/interaction.ts +0 -367
- package/src/graph/keyboard.ts +0 -208
- package/src/graph/search.ts +0 -50
- package/src/graph/simulation-worker-url.ts +0 -12
- package/src/graph/simulation-worker.ts +0 -278
- package/src/graph/simulation.ts +0 -422
- package/src/graph/spatial-index.ts +0 -121
- package/src/graph/types.ts +0 -47
- package/src/graph/worker-protocol.ts +0 -70
- package/src/graph/zoom.ts +0 -134
- package/src/graph-mount.ts +0 -888
- package/src/index.ts +0 -76
- package/src/interactions/chart-events.ts +0 -139
- package/src/interactions/crosshair.ts +0 -233
- package/src/interactions/drag-handler.ts +0 -175
- package/src/interactions/editing-drags.ts +0 -515
- package/src/interactions/index.ts +0 -25
- package/src/interactions/keyboard-nav.ts +0 -111
- package/src/interactions/legend-interaction.ts +0 -46
- package/src/interactions/selection.ts +0 -280
- package/src/interactions/tooltip-events.ts +0 -72
- package/src/map-camera.ts +0 -88
- package/src/map-mount.ts +0 -701
- package/src/map-renderer.ts +0 -338
- package/src/map-transition.ts +0 -83
- package/src/measure-text.ts +0 -134
- package/src/mount.ts +0 -1382
- package/src/pattern-utils.ts +0 -110
- package/src/renderers/annotations.ts +0 -388
- package/src/renderers/axes.ts +0 -351
- package/src/renderers/brand.ts +0 -87
- package/src/renderers/chrome.ts +0 -196
- package/src/renderers/endpoint-labels.ts +0 -150
- package/src/renderers/legend.ts +0 -311
- package/src/renderers/marks.ts +0 -561
- package/src/renderers/metrics.ts +0 -63
- package/src/renderers/svg-dom.ts +0 -40
- package/src/renderers/table-cells.ts +0 -446
- package/src/resize-observer.ts +0 -46
- package/src/sankey-mount.ts +0 -622
- package/src/sankey-renderer.ts +0 -614
- package/src/series-search.ts +0 -392
- package/src/static.ts +0 -251
- package/src/story/__tests__/camera-math.test.ts +0 -145
- package/src/story/__tests__/create-chart-story.test.ts +0 -317
- package/src/story/__tests__/progress-math.test.ts +0 -95
- package/src/story/__tests__/scroll-driver.test.ts +0 -130
- package/src/story/__tests__/tween.test.ts +0 -159
- package/src/story/camera-math.ts +0 -125
- package/src/story/create-chart-story.ts +0 -353
- package/src/story/crossfade.ts +0 -70
- package/src/story/index.ts +0 -39
- package/src/story/progress-math.ts +0 -85
- package/src/story/resolve-camera-target.ts +0 -119
- package/src/story/scroll-driver.ts +0 -206
- package/src/story/story-camera.ts +0 -49
- package/src/story/tween.ts +0 -112
- package/src/story/types.ts +0 -76
- package/src/svg-ids.ts +0 -23
- package/src/svg-renderer.ts +0 -383
- package/src/table-keyboard.ts +0 -266
- package/src/table-mount.ts +0 -567
- package/src/table-renderer.ts +0 -414
- package/src/text-edit-overlay.ts +0 -255
- package/src/theme-tokens.ts +0 -51
- package/src/tilemap-mount.ts +0 -511
- package/src/tilemap-renderer.ts +0 -396
- package/src/tooltip.ts +0 -177
- package/src/transition.ts +0 -2816
- package/src/you-draw-it.ts +0 -480
- /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
|
@@ -1,639 +0,0 @@
|
|
|
1
|
-
import type { TableSpec } from '@opendata-ai/openchart-engine';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { createTable } from '../table-mount';
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Test data
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
function makeSpec(overrides?: Partial<TableSpec>): TableSpec {
|
|
10
|
-
return {
|
|
11
|
-
type: 'table',
|
|
12
|
-
data: [
|
|
13
|
-
{ name: 'Alice', age: 30, city: 'Portland' },
|
|
14
|
-
{ name: 'Bob', age: 25, city: 'Seattle' },
|
|
15
|
-
{ name: 'Charlie', age: 35, city: 'Portland' },
|
|
16
|
-
{ name: 'Diana', age: 28, city: 'Denver' },
|
|
17
|
-
{ name: 'Eve', age: 22, city: 'Seattle' },
|
|
18
|
-
],
|
|
19
|
-
columns: [
|
|
20
|
-
{ key: 'name', label: 'Name' },
|
|
21
|
-
{ key: 'age', label: 'Age' },
|
|
22
|
-
{ key: 'city', label: 'City' },
|
|
23
|
-
],
|
|
24
|
-
chrome: { title: 'People' },
|
|
25
|
-
...overrides,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const paginatedSpec: TableSpec = {
|
|
30
|
-
type: 'table',
|
|
31
|
-
data: Array.from({ length: 50 }, (_, i) => ({
|
|
32
|
-
id: i + 1,
|
|
33
|
-
name: `Person ${i + 1}`,
|
|
34
|
-
value: (i * 17 + 3) % 100,
|
|
35
|
-
})),
|
|
36
|
-
columns: [
|
|
37
|
-
{ key: 'id', label: 'ID' },
|
|
38
|
-
{ key: 'name', label: 'Name' },
|
|
39
|
-
{ key: 'value', label: 'Value' },
|
|
40
|
-
],
|
|
41
|
-
pagination: { pageSize: 10 },
|
|
42
|
-
search: true,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const heatmapSpec: TableSpec = {
|
|
46
|
-
type: 'table',
|
|
47
|
-
data: [
|
|
48
|
-
{ name: 'A', score: 10 },
|
|
49
|
-
{ name: 'B', score: 50 },
|
|
50
|
-
{ name: 'C', score: 90 },
|
|
51
|
-
],
|
|
52
|
-
columns: [
|
|
53
|
-
{ key: 'name', label: 'Name' },
|
|
54
|
-
{ key: 'score', label: 'Score', heatmap: {} },
|
|
55
|
-
],
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const barSpec: TableSpec = {
|
|
59
|
-
type: 'table',
|
|
60
|
-
data: [
|
|
61
|
-
{ name: 'A', value: 250 },
|
|
62
|
-
{ name: 'B', value: 750 },
|
|
63
|
-
{ name: 'C', value: 500 },
|
|
64
|
-
],
|
|
65
|
-
columns: [
|
|
66
|
-
{ key: 'name', label: 'Name' },
|
|
67
|
-
{ key: 'value', label: 'Value', bar: {} },
|
|
68
|
-
],
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const sparklineSpec: TableSpec = {
|
|
72
|
-
type: 'table',
|
|
73
|
-
data: [
|
|
74
|
-
{ name: 'A', trend: [1, 3, 2, 5, 4] },
|
|
75
|
-
{ name: 'B', trend: [5, 4, 3, 2, 1] },
|
|
76
|
-
],
|
|
77
|
-
columns: [
|
|
78
|
-
{ key: 'name', label: 'Name' },
|
|
79
|
-
{ key: 'trend', label: 'Trend', sparkline: { type: 'line' } },
|
|
80
|
-
],
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const stickySpec: TableSpec = {
|
|
84
|
-
type: 'table',
|
|
85
|
-
data: [{ name: 'A', v1: 1, v2: 2, v3: 3 }],
|
|
86
|
-
columns: [
|
|
87
|
-
{ key: 'name', label: 'Name' },
|
|
88
|
-
{ key: 'v1', label: 'V1' },
|
|
89
|
-
{ key: 'v2', label: 'V2' },
|
|
90
|
-
{ key: 'v3', label: 'V3' },
|
|
91
|
-
],
|
|
92
|
-
stickyFirstColumn: true,
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
// Tests
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
describe('createTable', () => {
|
|
100
|
-
let container: HTMLDivElement;
|
|
101
|
-
|
|
102
|
-
beforeEach(() => {
|
|
103
|
-
container = document.createElement('div');
|
|
104
|
-
Object.defineProperty(container, 'getBoundingClientRect', {
|
|
105
|
-
value: () => ({
|
|
106
|
-
width: 800,
|
|
107
|
-
height: 600,
|
|
108
|
-
top: 0,
|
|
109
|
-
left: 0,
|
|
110
|
-
right: 800,
|
|
111
|
-
bottom: 600,
|
|
112
|
-
x: 0,
|
|
113
|
-
y: 0,
|
|
114
|
-
toJSON: () => ({}),
|
|
115
|
-
}),
|
|
116
|
-
});
|
|
117
|
-
document.body.appendChild(container);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
afterEach(() => {
|
|
121
|
-
document.body.innerHTML = '';
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('renders a <table> element', () => {
|
|
125
|
-
const table = createTable(container, makeSpec());
|
|
126
|
-
const tableEl = container.querySelector('table');
|
|
127
|
-
expect(tableEl).not.toBeNull();
|
|
128
|
-
table.destroy();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('renders correct number of rows and columns', () => {
|
|
132
|
-
const spec = makeSpec();
|
|
133
|
-
const table = createTable(container, spec);
|
|
134
|
-
|
|
135
|
-
const headerCells = container.querySelectorAll('thead th');
|
|
136
|
-
expect(headerCells.length).toBe(3);
|
|
137
|
-
|
|
138
|
-
const bodyRows = container.querySelectorAll('tbody tr');
|
|
139
|
-
expect(bodyRows.length).toBe(5);
|
|
140
|
-
|
|
141
|
-
table.destroy();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('renders column headers with correct labels', () => {
|
|
145
|
-
const table = createTable(container, makeSpec());
|
|
146
|
-
|
|
147
|
-
const headers = container.querySelectorAll('thead th');
|
|
148
|
-
expect(headers[0]?.textContent).toContain('Name');
|
|
149
|
-
expect(headers[1]?.textContent).toContain('Age');
|
|
150
|
-
expect(headers[2]?.textContent).toContain('City');
|
|
151
|
-
|
|
152
|
-
table.destroy();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('sort click re-renders sorted data', () => {
|
|
156
|
-
const spec = makeSpec();
|
|
157
|
-
const table = createTable(container, spec);
|
|
158
|
-
|
|
159
|
-
// Click sort button for "Age"
|
|
160
|
-
const sortBtns = container.querySelectorAll('[data-sort-column]');
|
|
161
|
-
const ageBtn = Array.from(sortBtns).find(
|
|
162
|
-
(btn) => btn.getAttribute('data-sort-column') === 'age',
|
|
163
|
-
);
|
|
164
|
-
expect(ageBtn).not.toBeNull();
|
|
165
|
-
ageBtn!.dispatchEvent(new Event('click', { bubbles: true }));
|
|
166
|
-
|
|
167
|
-
// After sorting by age ascending, first row should be youngest
|
|
168
|
-
const firstRowCells = container.querySelectorAll('tbody tr:first-child td');
|
|
169
|
-
// Eve (22) should be first
|
|
170
|
-
expect(firstRowCells[0]?.textContent).toBe('Eve');
|
|
171
|
-
|
|
172
|
-
table.destroy();
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('sort cycling: none -> asc -> desc -> none', () => {
|
|
176
|
-
const spec = makeSpec();
|
|
177
|
-
const table = createTable(container, spec);
|
|
178
|
-
|
|
179
|
-
const sortBtn = container.querySelector('[data-sort-column="age"]')!;
|
|
180
|
-
|
|
181
|
-
// Click 1: asc
|
|
182
|
-
sortBtn.dispatchEvent(new Event('click', { bubbles: true }));
|
|
183
|
-
expect(table.getState().sort).toEqual({ column: 'age', direction: 'asc' });
|
|
184
|
-
|
|
185
|
-
// Click 2: desc
|
|
186
|
-
sortBtn.dispatchEvent(new Event('click', { bubbles: true }));
|
|
187
|
-
expect(table.getState().sort).toEqual({ column: 'age', direction: 'desc' });
|
|
188
|
-
|
|
189
|
-
// Click 3: none
|
|
190
|
-
sortBtn.dispatchEvent(new Event('click', { bubbles: true }));
|
|
191
|
-
expect(table.getState().sort).toBeNull();
|
|
192
|
-
|
|
193
|
-
table.destroy();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('search input filters rows', () => {
|
|
197
|
-
vi.useFakeTimers();
|
|
198
|
-
try {
|
|
199
|
-
const spec = makeSpec({ search: true });
|
|
200
|
-
const table = createTable(container, spec);
|
|
201
|
-
|
|
202
|
-
const input = container.querySelector('.oc-table-search input') as HTMLInputElement;
|
|
203
|
-
expect(input).not.toBeNull();
|
|
204
|
-
|
|
205
|
-
// Type in a search query
|
|
206
|
-
input.value = 'Portland';
|
|
207
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
208
|
-
|
|
209
|
-
// Advance past the 200ms debounce
|
|
210
|
-
vi.advanceTimersByTime(200);
|
|
211
|
-
|
|
212
|
-
// Should show only Portland rows (Alice and Charlie)
|
|
213
|
-
const rows = container.querySelectorAll('tbody tr');
|
|
214
|
-
expect(rows.length).toBe(2);
|
|
215
|
-
|
|
216
|
-
table.destroy();
|
|
217
|
-
} finally {
|
|
218
|
-
vi.useRealTimers();
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('pagination controls navigate pages', () => {
|
|
223
|
-
const table = createTable(container, paginatedSpec);
|
|
224
|
-
|
|
225
|
-
// Should show page 1 of 5 (10 per page, 50 total)
|
|
226
|
-
const info = container.querySelector('.oc-table-pagination-info');
|
|
227
|
-
expect(info?.textContent).toContain('Showing 1-10 of 50');
|
|
228
|
-
|
|
229
|
-
const rows = container.querySelectorAll('tbody tr');
|
|
230
|
-
expect(rows.length).toBe(10);
|
|
231
|
-
|
|
232
|
-
// Click next page
|
|
233
|
-
const nextBtn = container.querySelector('[data-page-action="next"]') as HTMLButtonElement;
|
|
234
|
-
expect(nextBtn).not.toBeNull();
|
|
235
|
-
nextBtn.dispatchEvent(new Event('click', { bubbles: true }));
|
|
236
|
-
|
|
237
|
-
const infoAfter = container.querySelector('.oc-table-pagination-info');
|
|
238
|
-
expect(infoAfter?.textContent).toContain('Showing 11-20 of 50');
|
|
239
|
-
|
|
240
|
-
// Previous button should be enabled
|
|
241
|
-
const prevBtn = container.querySelector('[data-page-action="prev"]') as HTMLButtonElement;
|
|
242
|
-
expect(prevBtn.disabled).toBe(false);
|
|
243
|
-
|
|
244
|
-
table.destroy();
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('sticky first column CSS applied', () => {
|
|
248
|
-
const table = createTable(container, stickySpec);
|
|
249
|
-
|
|
250
|
-
const tableEl = container.querySelector('table');
|
|
251
|
-
expect(tableEl?.classList.contains('oc-table--sticky')).toBe(true);
|
|
252
|
-
|
|
253
|
-
table.destroy();
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('heatmap cells have colored backgrounds', () => {
|
|
257
|
-
const table = createTable(container, heatmapSpec);
|
|
258
|
-
|
|
259
|
-
// The heatmap cells should have background styles
|
|
260
|
-
const bodyRows = container.querySelectorAll('tbody tr');
|
|
261
|
-
const cells = bodyRows[2]?.querySelectorAll('td');
|
|
262
|
-
// Score column (index 1) should have background color
|
|
263
|
-
const scoreCellHigh = cells?.[1];
|
|
264
|
-
expect(
|
|
265
|
-
scoreCellHigh?.style.background !== '' || scoreCellHigh?.style.backgroundColor !== '',
|
|
266
|
-
).toBe(true);
|
|
267
|
-
|
|
268
|
-
table.destroy();
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('sparkline cells contain inline SVG', () => {
|
|
272
|
-
const table = createTable(container, sparklineSpec);
|
|
273
|
-
|
|
274
|
-
// Sparkline cells should be rendered with the sparkline class
|
|
275
|
-
const sparklineCells = container.querySelectorAll('.oc-table-sparkline');
|
|
276
|
-
expect(sparklineCells.length).toBeGreaterThan(0);
|
|
277
|
-
|
|
278
|
-
const svg = sparklineCells[0]?.querySelector('svg');
|
|
279
|
-
expect(svg).not.toBeNull();
|
|
280
|
-
|
|
281
|
-
// Line sparkline should have a polyline
|
|
282
|
-
const polyline = svg?.querySelector('polyline');
|
|
283
|
-
expect(polyline).not.toBeNull();
|
|
284
|
-
|
|
285
|
-
table.destroy();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('bar cells have proportional fill div', () => {
|
|
289
|
-
const table = createTable(container, barSpec);
|
|
290
|
-
|
|
291
|
-
const barFills = container.querySelectorAll('.oc-table-bar-fill');
|
|
292
|
-
expect(barFills.length).toBe(3);
|
|
293
|
-
|
|
294
|
-
// The bar values should have width proportional to their data
|
|
295
|
-
const barValues = container.querySelectorAll('.oc-table-bar-value');
|
|
296
|
-
expect(barValues.length).toBe(3);
|
|
297
|
-
|
|
298
|
-
table.destroy();
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('destroy() removes DOM', () => {
|
|
302
|
-
const table = createTable(container, makeSpec());
|
|
303
|
-
|
|
304
|
-
const tableBefore = container.querySelector('table');
|
|
305
|
-
expect(tableBefore).not.toBeNull();
|
|
306
|
-
|
|
307
|
-
table.destroy();
|
|
308
|
-
|
|
309
|
-
const tableAfter = container.querySelector('table');
|
|
310
|
-
expect(tableAfter).toBeNull();
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('export("csv") returns valid CSV', () => {
|
|
314
|
-
const spec = makeSpec();
|
|
315
|
-
const table = createTable(container, spec);
|
|
316
|
-
|
|
317
|
-
const csv = table.export('csv');
|
|
318
|
-
const lines = csv.split('\n');
|
|
319
|
-
expect(lines.length).toBe(6); // 1 header + 5 data rows
|
|
320
|
-
expect(lines[0]).toContain('Name');
|
|
321
|
-
expect(lines[0]).toContain('Age');
|
|
322
|
-
expect(lines[0]).toContain('City');
|
|
323
|
-
|
|
324
|
-
table.destroy();
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('search with no results renders "No results found" message', () => {
|
|
328
|
-
vi.useFakeTimers();
|
|
329
|
-
try {
|
|
330
|
-
const spec = makeSpec({ search: true });
|
|
331
|
-
const table = createTable(container, spec);
|
|
332
|
-
|
|
333
|
-
// Search for something that doesn't match any data
|
|
334
|
-
const input = container.querySelector('.oc-table-search input') as HTMLInputElement;
|
|
335
|
-
input.value = 'zzzznonexistent';
|
|
336
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
337
|
-
|
|
338
|
-
// Advance past the 200ms debounce
|
|
339
|
-
vi.advanceTimersByTime(200);
|
|
340
|
-
|
|
341
|
-
const empty = container.querySelector('.oc-table-empty');
|
|
342
|
-
expect(empty).not.toBeNull();
|
|
343
|
-
expect(empty?.textContent).toBe('No results found');
|
|
344
|
-
|
|
345
|
-
table.destroy();
|
|
346
|
-
} finally {
|
|
347
|
-
vi.useRealTimers();
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it('controlled mode: externalState used, onStateChange fires', () => {
|
|
352
|
-
const onStateChange = vi.fn();
|
|
353
|
-
const spec = makeSpec();
|
|
354
|
-
|
|
355
|
-
const table = createTable(container, spec, {
|
|
356
|
-
externalState: { sort: null, search: '', page: 0 },
|
|
357
|
-
onStateChange,
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// Click sort
|
|
361
|
-
const sortBtn = container.querySelector('[data-sort-column="age"]')!;
|
|
362
|
-
sortBtn.dispatchEvent(new Event('click', { bubbles: true }));
|
|
363
|
-
|
|
364
|
-
expect(onStateChange).toHaveBeenCalledWith(
|
|
365
|
-
expect.objectContaining({
|
|
366
|
-
sort: { column: 'age', direction: 'asc' },
|
|
367
|
-
}),
|
|
368
|
-
);
|
|
369
|
-
|
|
370
|
-
table.destroy();
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('controlled mode: setState re-renders sorted rows', () => {
|
|
374
|
-
const externalState = { sort: null, search: '', page: 0 };
|
|
375
|
-
const table = createTable(container, makeSpec(), { externalState });
|
|
376
|
-
|
|
377
|
-
// Default order: Alice, Bob, Charlie...
|
|
378
|
-
const firstBefore = container.querySelector('tbody tr td')?.textContent;
|
|
379
|
-
expect(firstBefore).toBe('Alice');
|
|
380
|
-
|
|
381
|
-
// Drive a sort through the controlled setState path.
|
|
382
|
-
table.setState({ sort: { column: 'age', direction: 'asc' } });
|
|
383
|
-
|
|
384
|
-
// Youngest (Eve, 22) should now be first.
|
|
385
|
-
const firstAfter = container.querySelector('tbody tr td')?.textContent;
|
|
386
|
-
expect(firstAfter).toBe('Eve');
|
|
387
|
-
|
|
388
|
-
table.destroy();
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it('controlled mode: setState re-renders filtered rows', () => {
|
|
392
|
-
const externalState = { sort: null, search: '', page: 0 };
|
|
393
|
-
const table = createTable(container, makeSpec({ search: true }), { externalState });
|
|
394
|
-
|
|
395
|
-
expect(container.querySelectorAll('tbody tr').length).toBe(5);
|
|
396
|
-
|
|
397
|
-
// Filter to a single match through the controlled setState path.
|
|
398
|
-
table.setState({ search: 'Denver' });
|
|
399
|
-
|
|
400
|
-
const rows = container.querySelectorAll('tbody tr');
|
|
401
|
-
expect(rows.length).toBe(1);
|
|
402
|
-
expect(rows[0].querySelector('td')?.textContent).toBe('Diana');
|
|
403
|
-
|
|
404
|
-
table.destroy();
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('update() re-renders with new spec', () => {
|
|
408
|
-
const spec = makeSpec();
|
|
409
|
-
const table = createTable(container, spec);
|
|
410
|
-
|
|
411
|
-
const headersBefore = container.querySelectorAll('thead th');
|
|
412
|
-
expect(headersBefore.length).toBe(3);
|
|
413
|
-
|
|
414
|
-
// Update with a spec that has 2 columns
|
|
415
|
-
table.update({
|
|
416
|
-
type: 'table',
|
|
417
|
-
data: [{ x: 1, y: 2 }],
|
|
418
|
-
columns: [
|
|
419
|
-
{ key: 'x', label: 'X' },
|
|
420
|
-
{ key: 'y', label: 'Y' },
|
|
421
|
-
],
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
const headersAfter = container.querySelectorAll('thead th');
|
|
425
|
-
expect(headersAfter.length).toBe(2);
|
|
426
|
-
|
|
427
|
-
table.destroy();
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
it('getState returns current state', () => {
|
|
431
|
-
const spec = makeSpec();
|
|
432
|
-
const table = createTable(container, spec);
|
|
433
|
-
|
|
434
|
-
const state = table.getState();
|
|
435
|
-
expect(state.sort).toBeNull();
|
|
436
|
-
expect(state.search).toBe('');
|
|
437
|
-
expect(state.page).toBe(0);
|
|
438
|
-
|
|
439
|
-
table.destroy();
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
it('setState updates table', () => {
|
|
443
|
-
const spec = makeSpec();
|
|
444
|
-
const table = createTable(container, spec);
|
|
445
|
-
|
|
446
|
-
table.setState({ sort: { column: 'age', direction: 'desc' } });
|
|
447
|
-
|
|
448
|
-
const state = table.getState();
|
|
449
|
-
expect(state.sort).toEqual({ column: 'age', direction: 'desc' });
|
|
450
|
-
|
|
451
|
-
// First row should be the oldest (Charlie, 35)
|
|
452
|
-
const firstRowCells = container.querySelectorAll('tbody tr:first-child td');
|
|
453
|
-
expect(firstRowCells[0]?.textContent).toBe('Charlie');
|
|
454
|
-
|
|
455
|
-
table.destroy();
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it('compact mode applies oc-table--compact class', () => {
|
|
459
|
-
const spec = makeSpec({ compact: true });
|
|
460
|
-
const table = createTable(container, spec);
|
|
461
|
-
|
|
462
|
-
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
463
|
-
expect(wrapper?.classList.contains('oc-table--compact')).toBe(true);
|
|
464
|
-
|
|
465
|
-
table.destroy();
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it('dark mode applies oc-dark class', () => {
|
|
469
|
-
const spec = makeSpec();
|
|
470
|
-
const table = createTable(container, spec, { darkMode: 'force' });
|
|
471
|
-
|
|
472
|
-
expect(container.classList.contains('oc-dark')).toBe(true);
|
|
473
|
-
|
|
474
|
-
table.destroy();
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it('onRowClick makes rows clickable', () => {
|
|
478
|
-
const onClick = vi.fn();
|
|
479
|
-
const spec = makeSpec();
|
|
480
|
-
const table = createTable(container, spec, { onRowClick: onClick });
|
|
481
|
-
|
|
482
|
-
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
483
|
-
expect(wrapper?.classList.contains('oc-table--clickable')).toBe(true);
|
|
484
|
-
|
|
485
|
-
// Click first row
|
|
486
|
-
const firstRow = container.querySelector('tbody tr');
|
|
487
|
-
firstRow?.dispatchEvent(new Event('click', { bubbles: true }));
|
|
488
|
-
|
|
489
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
490
|
-
expect(onClick).toHaveBeenCalledWith(expect.objectContaining({ name: expect.any(String) }));
|
|
491
|
-
|
|
492
|
-
table.destroy();
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it('renders title chrome', () => {
|
|
496
|
-
const spec = makeSpec();
|
|
497
|
-
const table = createTable(container, spec);
|
|
498
|
-
|
|
499
|
-
const title = container.querySelector('.oc-table-title');
|
|
500
|
-
expect(title).not.toBeNull();
|
|
501
|
-
expect(title?.textContent).toBe('People');
|
|
502
|
-
|
|
503
|
-
table.destroy();
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
it('resize() re-renders the table', () => {
|
|
507
|
-
const spec = makeSpec();
|
|
508
|
-
const table = createTable(container, spec);
|
|
509
|
-
|
|
510
|
-
// Should not throw
|
|
511
|
-
table.resize();
|
|
512
|
-
|
|
513
|
-
const tableEl = container.querySelector('table');
|
|
514
|
-
expect(tableEl).not.toBeNull();
|
|
515
|
-
|
|
516
|
-
table.destroy();
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// -------------------------------------------------------------------------
|
|
520
|
-
// Animation lifecycle
|
|
521
|
-
// -------------------------------------------------------------------------
|
|
522
|
-
|
|
523
|
-
describe('animation', () => {
|
|
524
|
-
it('adds oc-animate class on first render when animation is enabled', () => {
|
|
525
|
-
const spec = makeSpec({ animation: true });
|
|
526
|
-
const table = createTable(container, spec);
|
|
527
|
-
|
|
528
|
-
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
529
|
-
expect(wrapper?.classList.contains('oc-animate')).toBe(true);
|
|
530
|
-
|
|
531
|
-
table.destroy();
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
it('does not add oc-animate when animation is not set', () => {
|
|
535
|
-
const table = createTable(container, makeSpec());
|
|
536
|
-
|
|
537
|
-
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
538
|
-
expect(wrapper?.classList.contains('oc-animate')).toBe(false);
|
|
539
|
-
|
|
540
|
-
table.destroy();
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
it('sets --oc-animation-duration CSS custom property', () => {
|
|
544
|
-
const spec = makeSpec({ animation: { enter: { duration: 800 } } });
|
|
545
|
-
const table = createTable(container, spec);
|
|
546
|
-
|
|
547
|
-
const wrapper = container.querySelector('.oc-table-wrapper') as HTMLElement;
|
|
548
|
-
expect(wrapper.style.getPropertyValue('--oc-animation-duration')).toBe('800ms');
|
|
549
|
-
|
|
550
|
-
table.destroy();
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
it('sets --oc-animation-stagger CSS custom property', () => {
|
|
554
|
-
const spec = makeSpec({ animation: true });
|
|
555
|
-
const table = createTable(container, spec);
|
|
556
|
-
|
|
557
|
-
const wrapper = container.querySelector('.oc-table-wrapper') as HTMLElement;
|
|
558
|
-
const stagger = wrapper.style.getPropertyValue('--oc-animation-stagger');
|
|
559
|
-
expect(stagger).toMatch(/^\d+ms$/);
|
|
560
|
-
|
|
561
|
-
table.destroy();
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
it('stamps --oc-row-index on each tbody tr', () => {
|
|
565
|
-
const spec = makeSpec({ animation: true });
|
|
566
|
-
const table = createTable(container, spec);
|
|
567
|
-
|
|
568
|
-
const rows = container.querySelectorAll('tbody tr');
|
|
569
|
-
for (let i = 0; i < rows.length; i++) {
|
|
570
|
-
const row = rows[i] as HTMLElement;
|
|
571
|
-
expect(row.style.getPropertyValue('--oc-row-index')).toBe(String(i));
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
table.destroy();
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
it('does not re-animate on sort (only first render)', () => {
|
|
578
|
-
const spec = makeSpec({ animation: true });
|
|
579
|
-
const table = createTable(container, spec);
|
|
580
|
-
|
|
581
|
-
// Trigger sort via click
|
|
582
|
-
const sortBtn = container.querySelector('[data-sort-column]');
|
|
583
|
-
if (sortBtn) {
|
|
584
|
-
sortBtn.dispatchEvent(new Event('click', { bubbles: true }));
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
588
|
-
expect(wrapper?.classList.contains('oc-animate')).toBe(false);
|
|
589
|
-
|
|
590
|
-
table.destroy();
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
it('does not re-animate on update()', () => {
|
|
594
|
-
const spec = makeSpec({ animation: true });
|
|
595
|
-
const table = createTable(container, spec);
|
|
596
|
-
|
|
597
|
-
table.update(makeSpec({ animation: true }));
|
|
598
|
-
|
|
599
|
-
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
600
|
-
expect(wrapper?.classList.contains('oc-animate')).toBe(false);
|
|
601
|
-
|
|
602
|
-
table.destroy();
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
it('removes oc-animate after cleanup timeout', () => {
|
|
606
|
-
vi.useFakeTimers();
|
|
607
|
-
try {
|
|
608
|
-
const spec = makeSpec({ animation: true });
|
|
609
|
-
const table = createTable(container, spec);
|
|
610
|
-
|
|
611
|
-
const wrapper = container.querySelector('.oc-table-wrapper');
|
|
612
|
-
expect(wrapper?.classList.contains('oc-animate')).toBe(true);
|
|
613
|
-
|
|
614
|
-
// Advance past total animation time
|
|
615
|
-
vi.advanceTimersByTime(5000);
|
|
616
|
-
|
|
617
|
-
expect(wrapper?.classList.contains('oc-animate')).toBe(false);
|
|
618
|
-
|
|
619
|
-
table.destroy();
|
|
620
|
-
} finally {
|
|
621
|
-
vi.useRealTimers();
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
it('destroy cancels animation cleanup without error', () => {
|
|
626
|
-
vi.useFakeTimers();
|
|
627
|
-
try {
|
|
628
|
-
const spec = makeSpec({ animation: true });
|
|
629
|
-
const table = createTable(container, spec);
|
|
630
|
-
table.destroy();
|
|
631
|
-
|
|
632
|
-
// Should not throw after timer fires
|
|
633
|
-
vi.advanceTimersByTime(5000);
|
|
634
|
-
} finally {
|
|
635
|
-
vi.useRealTimers();
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
});
|
|
639
|
-
});
|