@constela/ui 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/components/accordion/accordion-content.constela.json +20 -0
- package/components/accordion/accordion-item.constela.json +20 -0
- package/components/accordion/accordion-trigger.constela.json +21 -0
- package/components/accordion/accordion.constela.json +18 -0
- package/components/accordion/accordion.styles.json +54 -0
- package/components/accordion/accordion.test.ts +608 -0
- package/components/calendar/calendar.constela.json +195 -0
- package/components/calendar/calendar.styles.json +33 -0
- package/components/calendar/calendar.test.ts +458 -0
- package/components/chart/area-chart.constela.json +482 -0
- package/components/chart/bar-chart.constela.json +342 -0
- package/components/chart/chart-axis.constela.json +224 -0
- package/components/chart/chart-legend.constela.json +82 -0
- package/components/chart/chart-tooltip.constela.json +61 -0
- package/components/chart/chart.styles.json +183 -0
- package/components/chart/chart.test.ts +3260 -0
- package/components/chart/donut-chart.constela.json +369 -0
- package/components/chart/line-chart.constela.json +380 -0
- package/components/chart/pie-chart.constela.json +259 -0
- package/components/chart/radar-chart.constela.json +297 -0
- package/components/chart/scatter-chart.constela.json +300 -0
- package/components/data-table/data-table-cell.constela.json +22 -0
- package/components/data-table/data-table-header.constela.json +30 -0
- package/components/data-table/data-table-pagination.constela.json +19 -0
- package/components/data-table/data-table-row.constela.json +30 -0
- package/components/data-table/data-table.constela.json +32 -0
- package/components/data-table/data-table.styles.json +84 -0
- package/components/data-table/data-table.test.ts +873 -0
- package/components/datepicker/datepicker.constela.json +128 -0
- package/components/datepicker/datepicker.styles.json +47 -0
- package/components/datepicker/datepicker.test.ts +540 -0
- package/components/tree/tree-node.constela.json +26 -0
- package/components/tree/tree.constela.json +24 -0
- package/components/tree/tree.styles.json +50 -0
- package/components/tree/tree.test.ts +542 -0
- package/components/virtual-scroll/virtual-scroll.constela.json +27 -0
- package/components/virtual-scroll/virtual-scroll.styles.json +17 -0
- package/components/virtual-scroll/virtual-scroll.test.ts +345 -0
- package/dist/index.d.ts +1 -2
- package/package.json +3 -3
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test suite for VirtualScroll component
|
|
3
|
+
*
|
|
4
|
+
* @constela/ui VirtualScroll component tests following TDD methodology.
|
|
5
|
+
* These tests verify the VirtualScroll component structure, params, local state,
|
|
6
|
+
* styles, and accessibility for efficient rendering of large lists.
|
|
7
|
+
*
|
|
8
|
+
* Coverage:
|
|
9
|
+
* - Component structure validation
|
|
10
|
+
* - Params definition validation (including required params)
|
|
11
|
+
* - Local state validation
|
|
12
|
+
* - Style preset validation
|
|
13
|
+
* - Accessibility attributes (role="listbox", aria-setsize, aria-live, tabindex)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { join, dirname } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import type { ComponentDef, StylePreset } from '@constela/core';
|
|
21
|
+
import {
|
|
22
|
+
assertValidComponent,
|
|
23
|
+
assertValidStylePreset,
|
|
24
|
+
hasParams,
|
|
25
|
+
isOptionalParam,
|
|
26
|
+
hasParamType,
|
|
27
|
+
getRootTag,
|
|
28
|
+
hasVariants,
|
|
29
|
+
hasVariantOptions,
|
|
30
|
+
hasDefaultVariants,
|
|
31
|
+
findPropInView,
|
|
32
|
+
hasRole,
|
|
33
|
+
hasAriaAttribute,
|
|
34
|
+
} from '../../tests/helpers/test-utils.js';
|
|
35
|
+
|
|
36
|
+
// ==================== Test Utilities ====================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the path to a component file in the virtual-scroll directory
|
|
40
|
+
*/
|
|
41
|
+
function getVirtualScrollComponentPath(fileName: string): string {
|
|
42
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
return join(__dirname, fileName);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load virtual-scroll component definition
|
|
48
|
+
*/
|
|
49
|
+
function loadVirtualScrollComponent(): ComponentDef {
|
|
50
|
+
const path = getVirtualScrollComponentPath('virtual-scroll.constela.json');
|
|
51
|
+
const content = readFileSync(path, 'utf-8');
|
|
52
|
+
return JSON.parse(content) as ComponentDef;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load virtual-scroll styles
|
|
57
|
+
*/
|
|
58
|
+
function loadVirtualScrollStyles(): Record<string, StylePreset> {
|
|
59
|
+
const path = getVirtualScrollComponentPath('virtual-scroll.styles.json');
|
|
60
|
+
const content = readFileSync(path, 'utf-8');
|
|
61
|
+
return JSON.parse(content) as Record<string, StylePreset>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if a param is required (required: true or required not specified)
|
|
66
|
+
*/
|
|
67
|
+
function isRequiredParam(component: ComponentDef, paramName: string): boolean {
|
|
68
|
+
if (!component.params || !(paramName in component.params)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const param = component.params[paramName];
|
|
72
|
+
// In Constela, params are required by default unless explicitly set to false
|
|
73
|
+
return param.required !== false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if a component has local state with a specific field
|
|
78
|
+
*/
|
|
79
|
+
function hasLocalState(component: ComponentDef, fieldName: string): boolean {
|
|
80
|
+
if (!component.localState) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return fieldName in component.localState;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if a local state field has a specific type
|
|
88
|
+
*/
|
|
89
|
+
function hasLocalStateType(
|
|
90
|
+
component: ComponentDef,
|
|
91
|
+
fieldName: string,
|
|
92
|
+
expectedType: 'string' | 'number' | 'boolean' | 'list' | 'object'
|
|
93
|
+
): boolean {
|
|
94
|
+
if (!component.localState || !(fieldName in component.localState)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return component.localState[fieldName].type === expectedType;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a local state field has a specific initial value
|
|
102
|
+
*/
|
|
103
|
+
function hasLocalStateInitial(
|
|
104
|
+
component: ComponentDef,
|
|
105
|
+
fieldName: string,
|
|
106
|
+
expectedInitial: unknown
|
|
107
|
+
): boolean {
|
|
108
|
+
if (!component.localState || !(fieldName in component.localState)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const actualInitial = component.localState[fieldName].initial;
|
|
112
|
+
if (Array.isArray(expectedInitial) && Array.isArray(actualInitial)) {
|
|
113
|
+
return JSON.stringify(expectedInitial) === JSON.stringify(actualInitial);
|
|
114
|
+
}
|
|
115
|
+
return actualInitial === expectedInitial;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ==================== Test Context ====================
|
|
119
|
+
|
|
120
|
+
interface VirtualScrollTestContext {
|
|
121
|
+
component: ComponentDef;
|
|
122
|
+
styles: Record<string, StylePreset>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe('VirtualScroll Component', () => {
|
|
126
|
+
let ctx: VirtualScrollTestContext;
|
|
127
|
+
|
|
128
|
+
beforeAll(() => {
|
|
129
|
+
ctx = {
|
|
130
|
+
component: loadVirtualScrollComponent(),
|
|
131
|
+
styles: loadVirtualScrollStyles(),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ==================== Component Structure Tests ====================
|
|
136
|
+
|
|
137
|
+
describe('Component Structure', () => {
|
|
138
|
+
it('should have valid component structure', () => {
|
|
139
|
+
assertValidComponent(ctx.component);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should have div as root element', () => {
|
|
143
|
+
const rootTag = getRootTag(ctx.component);
|
|
144
|
+
expect(rootTag).toBe('div');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should have role="listbox" or role="feed" attribute', () => {
|
|
148
|
+
const role = findPropInView(ctx.component.view, 'role');
|
|
149
|
+
expect(role).not.toBeNull();
|
|
150
|
+
expect(role).toMatchObject({
|
|
151
|
+
expr: 'lit',
|
|
152
|
+
});
|
|
153
|
+
// Accept either listbox or feed role
|
|
154
|
+
const roleValue = (role as { value: string }).value;
|
|
155
|
+
expect(['listbox', 'feed']).toContain(roleValue);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should have className using StyleExpr with virtualScrollContainer preset', () => {
|
|
159
|
+
const className = findPropInView(ctx.component.view, 'className');
|
|
160
|
+
expect(className).not.toBeNull();
|
|
161
|
+
expect(className).toMatchObject({
|
|
162
|
+
expr: 'style',
|
|
163
|
+
preset: 'virtualScrollContainer',
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ==================== Params Validation Tests ====================
|
|
169
|
+
|
|
170
|
+
describe('Params Validation', () => {
|
|
171
|
+
const expectedParams = ['items', 'itemHeight', 'containerHeight', 'overscan', 'keyField'];
|
|
172
|
+
|
|
173
|
+
it('should have all expected params', () => {
|
|
174
|
+
expect(hasParams(ctx.component, expectedParams)).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('param: items', () => {
|
|
178
|
+
it('should be required', () => {
|
|
179
|
+
expect(isRequiredParam(ctx.component, 'items')).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should have type list', () => {
|
|
183
|
+
expect(hasParamType(ctx.component, 'items', 'list')).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('param: itemHeight', () => {
|
|
188
|
+
it('should be required', () => {
|
|
189
|
+
expect(isRequiredParam(ctx.component, 'itemHeight')).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should have type number', () => {
|
|
193
|
+
expect(hasParamType(ctx.component, 'itemHeight', 'number')).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('param: containerHeight', () => {
|
|
198
|
+
it('should be required', () => {
|
|
199
|
+
expect(isRequiredParam(ctx.component, 'containerHeight')).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should have type number', () => {
|
|
203
|
+
expect(hasParamType(ctx.component, 'containerHeight', 'number')).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('param: overscan', () => {
|
|
208
|
+
it('should be optional', () => {
|
|
209
|
+
expect(isOptionalParam(ctx.component, 'overscan')).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should have type number', () => {
|
|
213
|
+
expect(hasParamType(ctx.component, 'overscan', 'number')).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('param: keyField', () => {
|
|
218
|
+
it('should be optional', () => {
|
|
219
|
+
expect(isOptionalParam(ctx.component, 'keyField')).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should have type string', () => {
|
|
223
|
+
expect(hasParamType(ctx.component, 'keyField', 'string')).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ==================== Local State Tests ====================
|
|
229
|
+
|
|
230
|
+
describe('Local State', () => {
|
|
231
|
+
it('should have scrollTop local state', () => {
|
|
232
|
+
expect(hasLocalState(ctx.component, 'scrollTop')).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should have scrollTop as number type', () => {
|
|
236
|
+
expect(hasLocalStateType(ctx.component, 'scrollTop', 'number')).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should have scrollTop with initial value 0', () => {
|
|
240
|
+
expect(hasLocalStateInitial(ctx.component, 'scrollTop', 0)).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ==================== Accessibility Tests ====================
|
|
245
|
+
|
|
246
|
+
describe('Accessibility', () => {
|
|
247
|
+
it('should have aria-setsize attribute for total number of items', () => {
|
|
248
|
+
expect(hasAriaAttribute(ctx.component.view, 'aria-setsize')).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should have aria-live="polite" for screen reader announcements', () => {
|
|
252
|
+
const ariaLive = findPropInView(ctx.component.view, 'aria-live');
|
|
253
|
+
expect(ariaLive).not.toBeNull();
|
|
254
|
+
expect(ariaLive).toMatchObject({
|
|
255
|
+
expr: 'lit',
|
|
256
|
+
value: 'polite',
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should have tabindex="0" for keyboard navigation', () => {
|
|
261
|
+
const tabindex = findPropInView(ctx.component.view, 'tabindex');
|
|
262
|
+
expect(tabindex).not.toBeNull();
|
|
263
|
+
expect(tabindex).toMatchObject({
|
|
264
|
+
expr: 'lit',
|
|
265
|
+
value: '0',
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ==================== Style Preset Tests ====================
|
|
271
|
+
|
|
272
|
+
describe('Style Preset (virtualScrollContainer)', () => {
|
|
273
|
+
it('should have valid style preset structure', () => {
|
|
274
|
+
const virtualScrollContainer = ctx.styles['virtualScrollContainer'];
|
|
275
|
+
expect(virtualScrollContainer).toBeDefined();
|
|
276
|
+
assertValidStylePreset(virtualScrollContainer);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should have base classes containing "relative overflow-hidden"', () => {
|
|
280
|
+
const virtualScrollContainer = ctx.styles['virtualScrollContainer'];
|
|
281
|
+
expect(virtualScrollContainer.base).toBeDefined();
|
|
282
|
+
expect(typeof virtualScrollContainer.base).toBe('string');
|
|
283
|
+
expect(virtualScrollContainer.base).toContain('relative');
|
|
284
|
+
expect(virtualScrollContainer.base).toContain('overflow-hidden');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('Style Preset (virtualScrollViewport)', () => {
|
|
289
|
+
it('should have valid style preset structure', () => {
|
|
290
|
+
const virtualScrollViewport = ctx.styles['virtualScrollViewport'];
|
|
291
|
+
expect(virtualScrollViewport).toBeDefined();
|
|
292
|
+
assertValidStylePreset(virtualScrollViewport);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should have base classes containing "overflow-auto"', () => {
|
|
296
|
+
const virtualScrollViewport = ctx.styles['virtualScrollViewport'];
|
|
297
|
+
expect(virtualScrollViewport.base).toBeDefined();
|
|
298
|
+
expect(typeof virtualScrollViewport.base).toBe('string');
|
|
299
|
+
expect(virtualScrollViewport.base).toContain('overflow-auto');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('Style Preset (virtualScrollSpacer)', () => {
|
|
304
|
+
it('should have valid style preset structure', () => {
|
|
305
|
+
const virtualScrollSpacer = ctx.styles['virtualScrollSpacer'];
|
|
306
|
+
expect(virtualScrollSpacer).toBeDefined();
|
|
307
|
+
assertValidStylePreset(virtualScrollSpacer);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should have base classes for spacer element', () => {
|
|
311
|
+
const virtualScrollSpacer = ctx.styles['virtualScrollSpacer'];
|
|
312
|
+
expect(virtualScrollSpacer.base).toBeDefined();
|
|
313
|
+
expect(typeof virtualScrollSpacer.base).toBe('string');
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe('Style Preset (virtualScrollContent)', () => {
|
|
318
|
+
it('should have valid style preset structure', () => {
|
|
319
|
+
const virtualScrollContent = ctx.styles['virtualScrollContent'];
|
|
320
|
+
expect(virtualScrollContent).toBeDefined();
|
|
321
|
+
assertValidStylePreset(virtualScrollContent);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should have base classes containing "absolute"', () => {
|
|
325
|
+
const virtualScrollContent = ctx.styles['virtualScrollContent'];
|
|
326
|
+
expect(virtualScrollContent.base).toBeDefined();
|
|
327
|
+
expect(typeof virtualScrollContent.base).toBe('string');
|
|
328
|
+
expect(virtualScrollContent.base).toContain('absolute');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('Style Preset (virtualScrollItem)', () => {
|
|
333
|
+
it('should have valid style preset structure', () => {
|
|
334
|
+
const virtualScrollItem = ctx.styles['virtualScrollItem'];
|
|
335
|
+
expect(virtualScrollItem).toBeDefined();
|
|
336
|
+
assertValidStylePreset(virtualScrollItem);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should have base classes for item wrapper', () => {
|
|
340
|
+
const virtualScrollItem = ctx.styles['virtualScrollItem'];
|
|
341
|
+
expect(virtualScrollItem.base).toBeDefined();
|
|
342
|
+
expect(typeof virtualScrollItem.base).toBe('string');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,7 @@ export { ComponentDef, Expression, ParamDef, StylePreset, ViewNode, validateAst
|
|
|
3
3
|
/**
|
|
4
4
|
* @constela/ui - Copy-paste UI components for Constela
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* accessible UI components written in Constela JSON DSL.
|
|
6
|
+
* Pre-built, accessible UI components written in Constela JSON DSL.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constela/ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Copy-paste UI components for Constela
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Copy-paste UI components for Constela",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"components"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@constela/core": "0.
|
|
19
|
+
"@constela/core": "0.17.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^20.10.0",
|