@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.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/components/accordion/accordion-content.constela.json +20 -0
  3. package/components/accordion/accordion-item.constela.json +20 -0
  4. package/components/accordion/accordion-trigger.constela.json +21 -0
  5. package/components/accordion/accordion.constela.json +18 -0
  6. package/components/accordion/accordion.styles.json +54 -0
  7. package/components/accordion/accordion.test.ts +608 -0
  8. package/components/calendar/calendar.constela.json +195 -0
  9. package/components/calendar/calendar.styles.json +33 -0
  10. package/components/calendar/calendar.test.ts +458 -0
  11. package/components/chart/area-chart.constela.json +482 -0
  12. package/components/chart/bar-chart.constela.json +342 -0
  13. package/components/chart/chart-axis.constela.json +224 -0
  14. package/components/chart/chart-legend.constela.json +82 -0
  15. package/components/chart/chart-tooltip.constela.json +61 -0
  16. package/components/chart/chart.styles.json +183 -0
  17. package/components/chart/chart.test.ts +3260 -0
  18. package/components/chart/donut-chart.constela.json +369 -0
  19. package/components/chart/line-chart.constela.json +380 -0
  20. package/components/chart/pie-chart.constela.json +259 -0
  21. package/components/chart/radar-chart.constela.json +297 -0
  22. package/components/chart/scatter-chart.constela.json +300 -0
  23. package/components/data-table/data-table-cell.constela.json +22 -0
  24. package/components/data-table/data-table-header.constela.json +30 -0
  25. package/components/data-table/data-table-pagination.constela.json +19 -0
  26. package/components/data-table/data-table-row.constela.json +30 -0
  27. package/components/data-table/data-table.constela.json +32 -0
  28. package/components/data-table/data-table.styles.json +84 -0
  29. package/components/data-table/data-table.test.ts +873 -0
  30. package/components/datepicker/datepicker.constela.json +128 -0
  31. package/components/datepicker/datepicker.styles.json +47 -0
  32. package/components/datepicker/datepicker.test.ts +540 -0
  33. package/components/tree/tree-node.constela.json +26 -0
  34. package/components/tree/tree.constela.json +24 -0
  35. package/components/tree/tree.styles.json +50 -0
  36. package/components/tree/tree.test.ts +542 -0
  37. package/components/virtual-scroll/virtual-scroll.constela.json +27 -0
  38. package/components/virtual-scroll/virtual-scroll.styles.json +17 -0
  39. package/components/virtual-scroll/virtual-scroll.test.ts +345 -0
  40. package/dist/index.d.ts +1 -2
  41. 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
- * A shadcn/ui style component library providing pre-built,
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.2.0",
4
- "description": "Copy-paste UI components for Constela - shadcn/ui style component library",
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.15.2"
19
+ "@constela/core": "0.17.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^20.10.0",