@constela/ui 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -3
- 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/package.json +2 -2
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test suite for DataTable Component Suite
|
|
3
|
+
*
|
|
4
|
+
* @constela/ui DataTable component tests following TDD methodology.
|
|
5
|
+
* These tests verify the DataTable, DataTableHeader, DataTableRow, DataTableCell,
|
|
6
|
+
* and DataTablePagination components structure, params, styles, and accessibility.
|
|
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="table", aria-rowcount, aria-sort, aria-selected, etc.)
|
|
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
|
+
hasSlot,
|
|
32
|
+
findPropInView,
|
|
33
|
+
hasRole,
|
|
34
|
+
hasAriaAttribute,
|
|
35
|
+
} from '../../tests/helpers/test-utils.js';
|
|
36
|
+
|
|
37
|
+
// ==================== Test Utilities ====================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the path to a component file in the data-table directory
|
|
41
|
+
*/
|
|
42
|
+
function getDataTableComponentPath(fileName: string): string {
|
|
43
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
return join(__dirname, fileName);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load a specific data-table sub-component
|
|
49
|
+
*/
|
|
50
|
+
function loadDataTableComponent(componentName: string): ComponentDef {
|
|
51
|
+
const path = getDataTableComponentPath(`${componentName}.constela.json`);
|
|
52
|
+
const content = readFileSync(path, 'utf-8');
|
|
53
|
+
return JSON.parse(content) as ComponentDef;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load data-table styles
|
|
58
|
+
*/
|
|
59
|
+
function loadDataTableStyles(): Record<string, StylePreset> {
|
|
60
|
+
const path = getDataTableComponentPath('data-table.styles.json');
|
|
61
|
+
const content = readFileSync(path, 'utf-8');
|
|
62
|
+
return JSON.parse(content) as Record<string, StylePreset>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if a param is required (required: true or required not specified)
|
|
67
|
+
*/
|
|
68
|
+
function isRequiredParam(component: ComponentDef, paramName: string): boolean {
|
|
69
|
+
if (!component.params || !(paramName in component.params)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const param = component.params[paramName];
|
|
73
|
+
// In Constela, params are required by default unless explicitly set to false
|
|
74
|
+
return param.required !== false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if a component has local state with a specific field
|
|
79
|
+
*/
|
|
80
|
+
function hasLocalState(component: ComponentDef, fieldName: string): boolean {
|
|
81
|
+
if (!component.localState) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return fieldName in component.localState;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if a local state field has a specific type
|
|
89
|
+
*/
|
|
90
|
+
function hasLocalStateType(
|
|
91
|
+
component: ComponentDef,
|
|
92
|
+
fieldName: string,
|
|
93
|
+
expectedType: 'string' | 'number' | 'boolean' | 'list' | 'object'
|
|
94
|
+
): boolean {
|
|
95
|
+
if (!component.localState || !(fieldName in component.localState)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return component.localState[fieldName].type === expectedType;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if a local state field has a specific initial value
|
|
103
|
+
*/
|
|
104
|
+
function hasLocalStateInitial(
|
|
105
|
+
component: ComponentDef,
|
|
106
|
+
fieldName: string,
|
|
107
|
+
expectedInitial: unknown
|
|
108
|
+
): boolean {
|
|
109
|
+
if (!component.localState || !(fieldName in component.localState)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const actualInitial = component.localState[fieldName].initial;
|
|
113
|
+
if (Array.isArray(expectedInitial) && Array.isArray(actualInitial)) {
|
|
114
|
+
return JSON.stringify(expectedInitial) === JSON.stringify(actualInitial);
|
|
115
|
+
}
|
|
116
|
+
return actualInitial === expectedInitial;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ==================== Test Contexts ====================
|
|
120
|
+
|
|
121
|
+
interface DataTableTestContext {
|
|
122
|
+
dataTable: ComponentDef;
|
|
123
|
+
dataTableHeader: ComponentDef;
|
|
124
|
+
dataTableRow: ComponentDef;
|
|
125
|
+
dataTableCell: ComponentDef;
|
|
126
|
+
dataTablePagination: ComponentDef;
|
|
127
|
+
styles: Record<string, StylePreset>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
describe('DataTable Component Suite', () => {
|
|
131
|
+
let ctx: DataTableTestContext;
|
|
132
|
+
|
|
133
|
+
beforeAll(() => {
|
|
134
|
+
ctx = {
|
|
135
|
+
dataTable: loadDataTableComponent('data-table'),
|
|
136
|
+
dataTableHeader: loadDataTableComponent('data-table-header'),
|
|
137
|
+
dataTableRow: loadDataTableComponent('data-table-row'),
|
|
138
|
+
dataTableCell: loadDataTableComponent('data-table-cell'),
|
|
139
|
+
dataTablePagination: loadDataTableComponent('data-table-pagination'),
|
|
140
|
+
styles: loadDataTableStyles(),
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ==================== DataTable (Container) Tests ====================
|
|
145
|
+
|
|
146
|
+
describe('DataTable (Container)', () => {
|
|
147
|
+
// ==================== Component Structure Tests ====================
|
|
148
|
+
|
|
149
|
+
describe('Component Structure', () => {
|
|
150
|
+
it('should have valid component structure', () => {
|
|
151
|
+
assertValidComponent(ctx.dataTable);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should have table as root element', () => {
|
|
155
|
+
const rootTag = getRootTag(ctx.dataTable);
|
|
156
|
+
expect(rootTag).toBe('table');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should have role="table" attribute', () => {
|
|
160
|
+
expect(hasRole(ctx.dataTable.view, 'table')).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should have className using StyleExpr with dataTableContainer preset', () => {
|
|
164
|
+
const className = findPropInView(ctx.dataTable.view, 'className');
|
|
165
|
+
expect(className).not.toBeNull();
|
|
166
|
+
expect(className).toMatchObject({
|
|
167
|
+
expr: 'style',
|
|
168
|
+
preset: 'dataTableContainer',
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ==================== Params Validation Tests ====================
|
|
174
|
+
|
|
175
|
+
describe('Params Validation', () => {
|
|
176
|
+
const expectedParams = [
|
|
177
|
+
'columns',
|
|
178
|
+
'data',
|
|
179
|
+
'pageSize',
|
|
180
|
+
'sortable',
|
|
181
|
+
'filterable',
|
|
182
|
+
'selectable',
|
|
183
|
+
'multiSelect',
|
|
184
|
+
'stickyHeader',
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
it('should have all expected params', () => {
|
|
188
|
+
expect(hasParams(ctx.dataTable, expectedParams)).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('param: columns', () => {
|
|
192
|
+
it('should be required', () => {
|
|
193
|
+
expect(isRequiredParam(ctx.dataTable, 'columns')).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should have type list', () => {
|
|
197
|
+
expect(hasParamType(ctx.dataTable, 'columns', 'list')).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('param: data', () => {
|
|
202
|
+
it('should be required', () => {
|
|
203
|
+
expect(isRequiredParam(ctx.dataTable, 'data')).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should have type list', () => {
|
|
207
|
+
expect(hasParamType(ctx.dataTable, 'data', 'list')).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('param: pageSize', () => {
|
|
212
|
+
it('should be optional', () => {
|
|
213
|
+
expect(isOptionalParam(ctx.dataTable, 'pageSize')).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should have type number', () => {
|
|
217
|
+
expect(hasParamType(ctx.dataTable, 'pageSize', 'number')).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('param: sortable', () => {
|
|
222
|
+
it('should be optional', () => {
|
|
223
|
+
expect(isOptionalParam(ctx.dataTable, 'sortable')).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should have type boolean', () => {
|
|
227
|
+
expect(hasParamType(ctx.dataTable, 'sortable', 'boolean')).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('param: filterable', () => {
|
|
232
|
+
it('should be optional', () => {
|
|
233
|
+
expect(isOptionalParam(ctx.dataTable, 'filterable')).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should have type boolean', () => {
|
|
237
|
+
expect(hasParamType(ctx.dataTable, 'filterable', 'boolean')).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('param: selectable', () => {
|
|
242
|
+
it('should be optional', () => {
|
|
243
|
+
expect(isOptionalParam(ctx.dataTable, 'selectable')).toBe(true);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should have type boolean', () => {
|
|
247
|
+
expect(hasParamType(ctx.dataTable, 'selectable', 'boolean')).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('param: multiSelect', () => {
|
|
252
|
+
it('should be optional', () => {
|
|
253
|
+
expect(isOptionalParam(ctx.dataTable, 'multiSelect')).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should have type boolean', () => {
|
|
257
|
+
expect(hasParamType(ctx.dataTable, 'multiSelect', 'boolean')).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('param: stickyHeader', () => {
|
|
262
|
+
it('should be optional', () => {
|
|
263
|
+
expect(isOptionalParam(ctx.dataTable, 'stickyHeader')).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should have type boolean', () => {
|
|
267
|
+
expect(hasParamType(ctx.dataTable, 'stickyHeader', 'boolean')).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ==================== Local State Tests ====================
|
|
273
|
+
|
|
274
|
+
describe('Local State', () => {
|
|
275
|
+
it('should have currentPage local state', () => {
|
|
276
|
+
expect(hasLocalState(ctx.dataTable, 'currentPage')).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should have currentPage as number type with initial value 0', () => {
|
|
280
|
+
expect(hasLocalStateType(ctx.dataTable, 'currentPage', 'number')).toBe(true);
|
|
281
|
+
expect(hasLocalStateInitial(ctx.dataTable, 'currentPage', 0)).toBe(true);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should have sortColumn local state', () => {
|
|
285
|
+
expect(hasLocalState(ctx.dataTable, 'sortColumn')).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should have sortColumn as string type with initial value ""', () => {
|
|
289
|
+
expect(hasLocalStateType(ctx.dataTable, 'sortColumn', 'string')).toBe(true);
|
|
290
|
+
expect(hasLocalStateInitial(ctx.dataTable, 'sortColumn', '')).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should have sortDirection local state', () => {
|
|
294
|
+
expect(hasLocalState(ctx.dataTable, 'sortDirection')).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should have sortDirection as string type with initial value "asc"', () => {
|
|
298
|
+
expect(hasLocalStateType(ctx.dataTable, 'sortDirection', 'string')).toBe(true);
|
|
299
|
+
expect(hasLocalStateInitial(ctx.dataTable, 'sortDirection', 'asc')).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should have filterText local state', () => {
|
|
303
|
+
expect(hasLocalState(ctx.dataTable, 'filterText')).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should have filterText as string type with initial value ""', () => {
|
|
307
|
+
expect(hasLocalStateType(ctx.dataTable, 'filterText', 'string')).toBe(true);
|
|
308
|
+
expect(hasLocalStateInitial(ctx.dataTable, 'filterText', '')).toBe(true);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should have selectedRows local state', () => {
|
|
312
|
+
expect(hasLocalState(ctx.dataTable, 'selectedRows')).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should have selectedRows as list type with initial value []', () => {
|
|
316
|
+
expect(hasLocalStateType(ctx.dataTable, 'selectedRows', 'list')).toBe(true);
|
|
317
|
+
expect(hasLocalStateInitial(ctx.dataTable, 'selectedRows', [])).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// ==================== Accessibility Tests ====================
|
|
322
|
+
|
|
323
|
+
describe('Accessibility', () => {
|
|
324
|
+
it('should have role="table" for screen readers', () => {
|
|
325
|
+
const role = findPropInView(ctx.dataTable.view, 'role');
|
|
326
|
+
expect(role).not.toBeNull();
|
|
327
|
+
expect(role).toMatchObject({
|
|
328
|
+
expr: 'lit',
|
|
329
|
+
value: 'table',
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should have aria-rowcount attribute', () => {
|
|
334
|
+
expect(hasAriaAttribute(ctx.dataTable.view, 'aria-rowcount')).toBe(true);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ==================== Style Preset Tests ====================
|
|
339
|
+
|
|
340
|
+
describe('Style Preset (dataTableContainer)', () => {
|
|
341
|
+
it('should have valid style preset structure', () => {
|
|
342
|
+
const dataTableContainer = ctx.styles['dataTableContainer'];
|
|
343
|
+
expect(dataTableContainer).toBeDefined();
|
|
344
|
+
assertValidStylePreset(dataTableContainer);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should have base classes for table layout', () => {
|
|
348
|
+
const dataTableContainer = ctx.styles['dataTableContainer'];
|
|
349
|
+
expect(dataTableContainer.base).toBeDefined();
|
|
350
|
+
expect(typeof dataTableContainer.base).toBe('string');
|
|
351
|
+
expect(dataTableContainer.base).toContain('w-full');
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('Style Preset (dataTableToolbar)', () => {
|
|
356
|
+
it('should have valid style preset structure', () => {
|
|
357
|
+
const dataTableToolbar = ctx.styles['dataTableToolbar'];
|
|
358
|
+
expect(dataTableToolbar).toBeDefined();
|
|
359
|
+
assertValidStylePreset(dataTableToolbar);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should have filterable variant', () => {
|
|
363
|
+
const dataTableToolbar = ctx.styles['dataTableToolbar'];
|
|
364
|
+
expect(hasVariants(dataTableToolbar, ['filterable'])).toBe(true);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe('Style Preset (dataTableWrapper)', () => {
|
|
369
|
+
it('should have valid style preset structure', () => {
|
|
370
|
+
const dataTableWrapper = ctx.styles['dataTableWrapper'];
|
|
371
|
+
expect(dataTableWrapper).toBeDefined();
|
|
372
|
+
assertValidStylePreset(dataTableWrapper);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// ==================== DataTableHeader Tests ====================
|
|
378
|
+
|
|
379
|
+
describe('DataTableHeader', () => {
|
|
380
|
+
// ==================== Component Structure Tests ====================
|
|
381
|
+
|
|
382
|
+
describe('Component Structure', () => {
|
|
383
|
+
it('should have valid component structure', () => {
|
|
384
|
+
assertValidComponent(ctx.dataTableHeader);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should have th as root element', () => {
|
|
388
|
+
const rootTag = getRootTag(ctx.dataTableHeader);
|
|
389
|
+
expect(rootTag).toBe('th');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should have role="columnheader" attribute', () => {
|
|
393
|
+
expect(hasRole(ctx.dataTableHeader.view, 'columnheader')).toBe(true);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should have className using StyleExpr with dataTableHeaderCell preset', () => {
|
|
397
|
+
const className = findPropInView(ctx.dataTableHeader.view, 'className');
|
|
398
|
+
expect(className).not.toBeNull();
|
|
399
|
+
expect(className).toMatchObject({
|
|
400
|
+
expr: 'style',
|
|
401
|
+
preset: 'dataTableHeaderCell',
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ==================== Params Validation Tests ====================
|
|
407
|
+
|
|
408
|
+
describe('Params Validation', () => {
|
|
409
|
+
const expectedParams = ['column', 'sortable', 'sorted', 'sortDirection'];
|
|
410
|
+
|
|
411
|
+
it('should have all expected params', () => {
|
|
412
|
+
expect(hasParams(ctx.dataTableHeader, expectedParams)).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe('param: column', () => {
|
|
416
|
+
it('should be required', () => {
|
|
417
|
+
expect(isRequiredParam(ctx.dataTableHeader, 'column')).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('should have type object', () => {
|
|
421
|
+
expect(hasParamType(ctx.dataTableHeader, 'column', 'object')).toBe(true);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe('param: sortable', () => {
|
|
426
|
+
it('should be optional', () => {
|
|
427
|
+
expect(isOptionalParam(ctx.dataTableHeader, 'sortable')).toBe(true);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should have type boolean', () => {
|
|
431
|
+
expect(hasParamType(ctx.dataTableHeader, 'sortable', 'boolean')).toBe(true);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe('param: sorted', () => {
|
|
436
|
+
it('should be optional', () => {
|
|
437
|
+
expect(isOptionalParam(ctx.dataTableHeader, 'sorted')).toBe(true);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('should have type boolean', () => {
|
|
441
|
+
expect(hasParamType(ctx.dataTableHeader, 'sorted', 'boolean')).toBe(true);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('param: sortDirection', () => {
|
|
446
|
+
it('should be optional', () => {
|
|
447
|
+
expect(isOptionalParam(ctx.dataTableHeader, 'sortDirection')).toBe(true);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should have type string', () => {
|
|
451
|
+
expect(hasParamType(ctx.dataTableHeader, 'sortDirection', 'string')).toBe(true);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// ==================== Accessibility Tests ====================
|
|
457
|
+
|
|
458
|
+
describe('Accessibility', () => {
|
|
459
|
+
it('should have role="columnheader" for screen readers', () => {
|
|
460
|
+
const role = findPropInView(ctx.dataTableHeader.view, 'role');
|
|
461
|
+
expect(role).not.toBeNull();
|
|
462
|
+
expect(role).toMatchObject({
|
|
463
|
+
expr: 'lit',
|
|
464
|
+
value: 'columnheader',
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('should have aria-sort attribute', () => {
|
|
469
|
+
expect(hasAriaAttribute(ctx.dataTableHeader.view, 'aria-sort')).toBe(true);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// ==================== Style Preset Tests ====================
|
|
474
|
+
|
|
475
|
+
describe('Style Preset (dataTableHeader)', () => {
|
|
476
|
+
it('should have valid style preset structure', () => {
|
|
477
|
+
const dataTableHeader = ctx.styles['dataTableHeader'];
|
|
478
|
+
expect(dataTableHeader).toBeDefined();
|
|
479
|
+
assertValidStylePreset(dataTableHeader);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should have sticky variant', () => {
|
|
483
|
+
const dataTableHeader = ctx.styles['dataTableHeader'];
|
|
484
|
+
expect(hasVariants(dataTableHeader, ['sticky'])).toBe(true);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe('variant options', () => {
|
|
488
|
+
it('should have true sticky option with position sticky', () => {
|
|
489
|
+
const dataTableHeader = ctx.styles['dataTableHeader'];
|
|
490
|
+
expect(hasVariantOptions(dataTableHeader, 'sticky', ['true'])).toBe(true);
|
|
491
|
+
expect(dataTableHeader.variants?.sticky?.['true']).toContain('sticky');
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe('Style Preset (dataTableHeaderRow)', () => {
|
|
497
|
+
it('should have valid style preset structure', () => {
|
|
498
|
+
const dataTableHeaderRow = ctx.styles['dataTableHeaderRow'];
|
|
499
|
+
expect(dataTableHeaderRow).toBeDefined();
|
|
500
|
+
assertValidStylePreset(dataTableHeaderRow);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe('Style Preset (dataTableHeaderCell)', () => {
|
|
505
|
+
it('should have valid style preset structure', () => {
|
|
506
|
+
const dataTableHeaderCell = ctx.styles['dataTableHeaderCell'];
|
|
507
|
+
expect(dataTableHeaderCell).toBeDefined();
|
|
508
|
+
assertValidStylePreset(dataTableHeaderCell);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('should have sortable variant', () => {
|
|
512
|
+
const dataTableHeaderCell = ctx.styles['dataTableHeaderCell'];
|
|
513
|
+
expect(hasVariants(dataTableHeaderCell, ['sortable'])).toBe(true);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('should have align variant with left, center, right options', () => {
|
|
517
|
+
const dataTableHeaderCell = ctx.styles['dataTableHeaderCell'];
|
|
518
|
+
expect(hasVariants(dataTableHeaderCell, ['align'])).toBe(true);
|
|
519
|
+
expect(hasVariantOptions(dataTableHeaderCell, 'align', ['left', 'center', 'right'])).toBe(true);
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// ==================== DataTableRow Tests ====================
|
|
525
|
+
|
|
526
|
+
describe('DataTableRow', () => {
|
|
527
|
+
// ==================== Component Structure Tests ====================
|
|
528
|
+
|
|
529
|
+
describe('Component Structure', () => {
|
|
530
|
+
it('should have valid component structure', () => {
|
|
531
|
+
assertValidComponent(ctx.dataTableRow);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('should have tr as root element', () => {
|
|
535
|
+
const rootTag = getRootTag(ctx.dataTableRow);
|
|
536
|
+
expect(rootTag).toBe('tr');
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('should have role="row" attribute', () => {
|
|
540
|
+
expect(hasRole(ctx.dataTableRow.view, 'row')).toBe(true);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('should have className using StyleExpr with dataTableRow preset', () => {
|
|
544
|
+
const className = findPropInView(ctx.dataTableRow.view, 'className');
|
|
545
|
+
expect(className).not.toBeNull();
|
|
546
|
+
expect(className).toMatchObject({
|
|
547
|
+
expr: 'style',
|
|
548
|
+
preset: 'dataTableRow',
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// ==================== Params Validation Tests ====================
|
|
554
|
+
|
|
555
|
+
describe('Params Validation', () => {
|
|
556
|
+
const expectedParams = ['row', 'rowIndex', 'selected', 'selectable'];
|
|
557
|
+
|
|
558
|
+
it('should have all expected params', () => {
|
|
559
|
+
expect(hasParams(ctx.dataTableRow, expectedParams)).toBe(true);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
describe('param: row', () => {
|
|
563
|
+
it('should be required', () => {
|
|
564
|
+
expect(isRequiredParam(ctx.dataTableRow, 'row')).toBe(true);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('should have type object', () => {
|
|
568
|
+
expect(hasParamType(ctx.dataTableRow, 'row', 'object')).toBe(true);
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
describe('param: rowIndex', () => {
|
|
573
|
+
it('should be required', () => {
|
|
574
|
+
expect(isRequiredParam(ctx.dataTableRow, 'rowIndex')).toBe(true);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should have type number', () => {
|
|
578
|
+
expect(hasParamType(ctx.dataTableRow, 'rowIndex', 'number')).toBe(true);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
describe('param: selected', () => {
|
|
583
|
+
it('should be optional', () => {
|
|
584
|
+
expect(isOptionalParam(ctx.dataTableRow, 'selected')).toBe(true);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('should have type boolean', () => {
|
|
588
|
+
expect(hasParamType(ctx.dataTableRow, 'selected', 'boolean')).toBe(true);
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
describe('param: selectable', () => {
|
|
593
|
+
it('should be optional', () => {
|
|
594
|
+
expect(isOptionalParam(ctx.dataTableRow, 'selectable')).toBe(true);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('should have type boolean', () => {
|
|
598
|
+
expect(hasParamType(ctx.dataTableRow, 'selectable', 'boolean')).toBe(true);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// ==================== Accessibility Tests ====================
|
|
604
|
+
|
|
605
|
+
describe('Accessibility', () => {
|
|
606
|
+
it('should have role="row" for screen readers', () => {
|
|
607
|
+
const role = findPropInView(ctx.dataTableRow.view, 'role');
|
|
608
|
+
expect(role).not.toBeNull();
|
|
609
|
+
expect(role).toMatchObject({
|
|
610
|
+
expr: 'lit',
|
|
611
|
+
value: 'row',
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('should have aria-rowindex attribute', () => {
|
|
616
|
+
expect(hasAriaAttribute(ctx.dataTableRow.view, 'aria-rowindex')).toBe(true);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('should have aria-selected attribute', () => {
|
|
620
|
+
expect(hasAriaAttribute(ctx.dataTableRow.view, 'aria-selected')).toBe(true);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// ==================== Style Preset Tests ====================
|
|
625
|
+
|
|
626
|
+
describe('Style Preset (dataTableBody)', () => {
|
|
627
|
+
it('should have valid style preset structure', () => {
|
|
628
|
+
const dataTableBody = ctx.styles['dataTableBody'];
|
|
629
|
+
expect(dataTableBody).toBeDefined();
|
|
630
|
+
assertValidStylePreset(dataTableBody);
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
describe('Style Preset (dataTableRow)', () => {
|
|
635
|
+
it('should have valid style preset structure', () => {
|
|
636
|
+
const dataTableRow = ctx.styles['dataTableRow'];
|
|
637
|
+
expect(dataTableRow).toBeDefined();
|
|
638
|
+
assertValidStylePreset(dataTableRow);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('should have selected variant', () => {
|
|
642
|
+
const dataTableRow = ctx.styles['dataTableRow'];
|
|
643
|
+
expect(hasVariants(dataTableRow, ['selected'])).toBe(true);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
describe('variant options', () => {
|
|
647
|
+
it('should have true selected option with highlight styling', () => {
|
|
648
|
+
const dataTableRow = ctx.styles['dataTableRow'];
|
|
649
|
+
expect(hasVariantOptions(dataTableRow, 'selected', ['true', 'false'])).toBe(true);
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// ==================== DataTableCell Tests ====================
|
|
656
|
+
|
|
657
|
+
describe('DataTableCell', () => {
|
|
658
|
+
// ==================== Component Structure Tests ====================
|
|
659
|
+
|
|
660
|
+
describe('Component Structure', () => {
|
|
661
|
+
it('should have valid component structure', () => {
|
|
662
|
+
assertValidComponent(ctx.dataTableCell);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('should have td as root element', () => {
|
|
666
|
+
const rootTag = getRootTag(ctx.dataTableCell);
|
|
667
|
+
expect(rootTag).toBe('td');
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('should have role="cell" attribute', () => {
|
|
671
|
+
expect(hasRole(ctx.dataTableCell.view, 'cell')).toBe(true);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('should have className using StyleExpr with dataTableCell preset', () => {
|
|
675
|
+
const className = findPropInView(ctx.dataTableCell.view, 'className');
|
|
676
|
+
expect(className).not.toBeNull();
|
|
677
|
+
expect(className).toMatchObject({
|
|
678
|
+
expr: 'style',
|
|
679
|
+
preset: 'dataTableCell',
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// ==================== Params Validation Tests ====================
|
|
685
|
+
|
|
686
|
+
describe('Params Validation', () => {
|
|
687
|
+
const expectedParams = ['value', 'column', 'align'];
|
|
688
|
+
|
|
689
|
+
it('should have all expected params', () => {
|
|
690
|
+
expect(hasParams(ctx.dataTableCell, expectedParams)).toBe(true);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
describe('param: value', () => {
|
|
694
|
+
it('should be required', () => {
|
|
695
|
+
expect(isRequiredParam(ctx.dataTableCell, 'value')).toBe(true);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('should have type any', () => {
|
|
699
|
+
expect(hasParamType(ctx.dataTableCell, 'value', 'any')).toBe(true);
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
describe('param: column', () => {
|
|
704
|
+
it('should be required', () => {
|
|
705
|
+
expect(isRequiredParam(ctx.dataTableCell, 'column')).toBe(true);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('should have type object', () => {
|
|
709
|
+
expect(hasParamType(ctx.dataTableCell, 'column', 'object')).toBe(true);
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
describe('param: align', () => {
|
|
714
|
+
it('should be optional', () => {
|
|
715
|
+
expect(isOptionalParam(ctx.dataTableCell, 'align')).toBe(true);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('should have type string', () => {
|
|
719
|
+
expect(hasParamType(ctx.dataTableCell, 'align', 'string')).toBe(true);
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// ==================== Accessibility Tests ====================
|
|
725
|
+
|
|
726
|
+
describe('Accessibility', () => {
|
|
727
|
+
it('should have role="cell" for screen readers', () => {
|
|
728
|
+
const role = findPropInView(ctx.dataTableCell.view, 'role');
|
|
729
|
+
expect(role).not.toBeNull();
|
|
730
|
+
expect(role).toMatchObject({
|
|
731
|
+
expr: 'lit',
|
|
732
|
+
value: 'cell',
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// ==================== Style Preset Tests ====================
|
|
738
|
+
|
|
739
|
+
describe('Style Preset (dataTableCell)', () => {
|
|
740
|
+
it('should have valid style preset structure', () => {
|
|
741
|
+
const dataTableCell = ctx.styles['dataTableCell'];
|
|
742
|
+
expect(dataTableCell).toBeDefined();
|
|
743
|
+
assertValidStylePreset(dataTableCell);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it('should have align variant with left, center, right options', () => {
|
|
747
|
+
const dataTableCell = ctx.styles['dataTableCell'];
|
|
748
|
+
expect(hasVariants(dataTableCell, ['align'])).toBe(true);
|
|
749
|
+
expect(hasVariantOptions(dataTableCell, 'align', ['left', 'center', 'right'])).toBe(true);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
describe('variant options', () => {
|
|
753
|
+
it('should have left align option with text-left', () => {
|
|
754
|
+
const dataTableCell = ctx.styles['dataTableCell'];
|
|
755
|
+
expect(dataTableCell.variants?.align?.['left']).toContain('text-left');
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('should have center align option with text-center', () => {
|
|
759
|
+
const dataTableCell = ctx.styles['dataTableCell'];
|
|
760
|
+
expect(dataTableCell.variants?.align?.['center']).toContain('text-center');
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('should have right align option with text-right', () => {
|
|
764
|
+
const dataTableCell = ctx.styles['dataTableCell'];
|
|
765
|
+
expect(dataTableCell.variants?.align?.['right']).toContain('text-right');
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
describe('default variants', () => {
|
|
770
|
+
it('should have default align set to left', () => {
|
|
771
|
+
const dataTableCell = ctx.styles['dataTableCell'];
|
|
772
|
+
expect(hasDefaultVariants(dataTableCell, { align: 'left' })).toBe(true);
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// ==================== DataTablePagination Tests ====================
|
|
779
|
+
|
|
780
|
+
describe('DataTablePagination', () => {
|
|
781
|
+
// ==================== Component Structure Tests ====================
|
|
782
|
+
|
|
783
|
+
describe('Component Structure', () => {
|
|
784
|
+
it('should have valid component structure', () => {
|
|
785
|
+
assertValidComponent(ctx.dataTablePagination);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('should have nav as root element', () => {
|
|
789
|
+
const rootTag = getRootTag(ctx.dataTablePagination);
|
|
790
|
+
expect(rootTag).toBe('nav');
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('should have className using StyleExpr with dataTablePagination preset', () => {
|
|
794
|
+
const className = findPropInView(ctx.dataTablePagination.view, 'className');
|
|
795
|
+
expect(className).not.toBeNull();
|
|
796
|
+
expect(className).toMatchObject({
|
|
797
|
+
expr: 'style',
|
|
798
|
+
preset: 'dataTablePagination',
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
// ==================== Params Validation Tests ====================
|
|
804
|
+
|
|
805
|
+
describe('Params Validation', () => {
|
|
806
|
+
const expectedParams = ['currentPage', 'totalPages', 'maxVisible'];
|
|
807
|
+
|
|
808
|
+
it('should have all expected params', () => {
|
|
809
|
+
expect(hasParams(ctx.dataTablePagination, expectedParams)).toBe(true);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
describe('param: currentPage', () => {
|
|
813
|
+
it('should be required', () => {
|
|
814
|
+
expect(isRequiredParam(ctx.dataTablePagination, 'currentPage')).toBe(true);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('should have type number', () => {
|
|
818
|
+
expect(hasParamType(ctx.dataTablePagination, 'currentPage', 'number')).toBe(true);
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
describe('param: totalPages', () => {
|
|
823
|
+
it('should be required', () => {
|
|
824
|
+
expect(isRequiredParam(ctx.dataTablePagination, 'totalPages')).toBe(true);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('should have type number', () => {
|
|
828
|
+
expect(hasParamType(ctx.dataTablePagination, 'totalPages', 'number')).toBe(true);
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
describe('param: maxVisible', () => {
|
|
833
|
+
it('should be optional', () => {
|
|
834
|
+
expect(isOptionalParam(ctx.dataTablePagination, 'maxVisible')).toBe(true);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it('should have type number', () => {
|
|
838
|
+
expect(hasParamType(ctx.dataTablePagination, 'maxVisible', 'number')).toBe(true);
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
// ==================== Accessibility Tests ====================
|
|
844
|
+
|
|
845
|
+
describe('Accessibility', () => {
|
|
846
|
+
it('should have aria-label="Pagination" attribute', () => {
|
|
847
|
+
const ariaLabel = findPropInView(ctx.dataTablePagination.view, 'aria-label');
|
|
848
|
+
expect(ariaLabel).not.toBeNull();
|
|
849
|
+
expect(ariaLabel).toMatchObject({
|
|
850
|
+
expr: 'lit',
|
|
851
|
+
value: 'Pagination',
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
// ==================== Style Preset Tests ====================
|
|
857
|
+
|
|
858
|
+
describe('Style Preset (dataTablePagination)', () => {
|
|
859
|
+
it('should have valid style preset structure', () => {
|
|
860
|
+
const dataTablePagination = ctx.styles['dataTablePagination'];
|
|
861
|
+
expect(dataTablePagination).toBeDefined();
|
|
862
|
+
assertValidStylePreset(dataTablePagination);
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it('should have base classes for flex layout', () => {
|
|
866
|
+
const dataTablePagination = ctx.styles['dataTablePagination'];
|
|
867
|
+
expect(dataTablePagination.base).toBeDefined();
|
|
868
|
+
expect(typeof dataTablePagination.base).toBe('string');
|
|
869
|
+
expect(dataTablePagination.base).toContain('flex');
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
});
|