@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,3260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test suite for Chart Components (BarChart, LineChart, AreaChart, PieChart, DonutChart, ScatterChart, RadarChart)
|
|
3
|
+
*
|
|
4
|
+
* @constela/ui Chart component tests following TDD methodology.
|
|
5
|
+
* These tests verify the Chart component structure, params, styles, and accessibility.
|
|
6
|
+
*
|
|
7
|
+
* Coverage:
|
|
8
|
+
* - Component structure validation
|
|
9
|
+
* - Params definition validation (including required params)
|
|
10
|
+
* - Style preset validation
|
|
11
|
+
* - Accessibility attributes (role="img", aria-label, title)
|
|
12
|
+
* - Helper function integration
|
|
13
|
+
*
|
|
14
|
+
* TDD Red Phase: These tests verify Chart components that do not yet exist,
|
|
15
|
+
* so all tests are expected to FAIL.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
19
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
20
|
+
import { join, dirname } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import type { ComponentDef, StylePreset } from '@constela/core';
|
|
23
|
+
import {
|
|
24
|
+
assertValidComponent,
|
|
25
|
+
assertValidStylePreset,
|
|
26
|
+
hasParams,
|
|
27
|
+
isOptionalParam,
|
|
28
|
+
hasParamType,
|
|
29
|
+
getRootTag,
|
|
30
|
+
hasVariants,
|
|
31
|
+
hasVariantOptions,
|
|
32
|
+
hasDefaultVariants,
|
|
33
|
+
findPropInView,
|
|
34
|
+
hasRole,
|
|
35
|
+
hasAriaAttribute,
|
|
36
|
+
} from '../../tests/helpers/test-utils.js';
|
|
37
|
+
|
|
38
|
+
// ==================== Test Utilities ====================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the path to a component file in the chart directory
|
|
42
|
+
*/
|
|
43
|
+
function getChartComponentPath(fileName: string): string {
|
|
44
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
return join(__dirname, fileName);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load a specific chart sub-component
|
|
50
|
+
*/
|
|
51
|
+
function loadChartComponent(componentName: string): ComponentDef {
|
|
52
|
+
const path = getChartComponentPath(`${componentName}.constela.json`);
|
|
53
|
+
const content = readFileSync(path, 'utf-8');
|
|
54
|
+
return JSON.parse(content) as ComponentDef;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load chart styles
|
|
59
|
+
*/
|
|
60
|
+
function loadChartStyles(): Record<string, StylePreset> {
|
|
61
|
+
const path = getChartComponentPath('chart.styles.json');
|
|
62
|
+
const content = readFileSync(path, 'utf-8');
|
|
63
|
+
return JSON.parse(content) as Record<string, StylePreset>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a param is required (required: true or required not specified)
|
|
68
|
+
*/
|
|
69
|
+
function isRequiredParam(component: ComponentDef, paramName: string): boolean {
|
|
70
|
+
if (!component.params || !(paramName in component.params)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const param = component.params[paramName];
|
|
74
|
+
// In Constela, params are required by default unless explicitly set to false
|
|
75
|
+
return param.required !== false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a component has local state with a specific field
|
|
80
|
+
*/
|
|
81
|
+
function hasLocalState(component: ComponentDef, fieldName: string): boolean {
|
|
82
|
+
if (!component.localState) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return fieldName in component.localState;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if a local state field has a specific type
|
|
90
|
+
*/
|
|
91
|
+
function hasLocalStateType(
|
|
92
|
+
component: ComponentDef,
|
|
93
|
+
fieldName: string,
|
|
94
|
+
expectedType: 'string' | 'number' | 'boolean' | 'list' | 'object'
|
|
95
|
+
): boolean {
|
|
96
|
+
if (!component.localState || !(fieldName in component.localState)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return component.localState[fieldName].type === expectedType;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if a local state field has an initial value expression with cond pattern
|
|
104
|
+
* (used for default value computation)
|
|
105
|
+
*/
|
|
106
|
+
function hasCondInitialPattern(
|
|
107
|
+
component: ComponentDef,
|
|
108
|
+
fieldName: string,
|
|
109
|
+
paramName: string,
|
|
110
|
+
defaultValue: unknown
|
|
111
|
+
): boolean {
|
|
112
|
+
if (!component.localState || !(fieldName in component.localState)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const initial = component.localState[fieldName].initial;
|
|
116
|
+
if (!initial || typeof initial !== 'object') {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const initExpr = initial as Record<string, unknown>;
|
|
120
|
+
// Check for cond/then/else pattern
|
|
121
|
+
if (initExpr['expr'] !== 'cond') {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
// Check test is param reference
|
|
125
|
+
const test = initExpr['test'] as Record<string, unknown> | undefined;
|
|
126
|
+
if (!test || test['expr'] !== 'param' || test['name'] !== paramName) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
// Check then is param reference
|
|
130
|
+
const then = initExpr['then'] as Record<string, unknown> | undefined;
|
|
131
|
+
if (!then || then['expr'] !== 'param' || then['name'] !== paramName) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
// Check else is literal with default value
|
|
135
|
+
const elseExpr = initExpr['else'] as Record<string, unknown> | undefined;
|
|
136
|
+
if (!elseExpr || elseExpr['expr'] !== 'lit' || elseExpr['value'] !== defaultValue) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Count occurrences of cond/then/else patterns for a specific param in view
|
|
144
|
+
*/
|
|
145
|
+
function countCondPatterns(view: unknown, paramName: string): number {
|
|
146
|
+
if (!view || typeof view !== 'object') return 0;
|
|
147
|
+
const node = view as Record<string, unknown>;
|
|
148
|
+
let count = 0;
|
|
149
|
+
|
|
150
|
+
// Check if this node is a cond pattern for the param
|
|
151
|
+
if (node['expr'] === 'cond') {
|
|
152
|
+
const test = node['test'] as Record<string, unknown> | undefined;
|
|
153
|
+
if (test && test['expr'] === 'param' && test['name'] === paramName) {
|
|
154
|
+
count++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Recursively search all properties
|
|
159
|
+
for (const [key, value] of Object.entries(node)) {
|
|
160
|
+
if (key === 'expr' || key === 'kind') continue;
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
for (const item of value) {
|
|
163
|
+
count += countCondPatterns(item, paramName);
|
|
164
|
+
}
|
|
165
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
166
|
+
count += countCondPatterns(value, paramName);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return count;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if view contains a local expression reference
|
|
175
|
+
*/
|
|
176
|
+
function hasLocalReference(view: unknown, localName: string): boolean {
|
|
177
|
+
if (!view || typeof view !== 'object') return false;
|
|
178
|
+
const node = view as Record<string, unknown>;
|
|
179
|
+
|
|
180
|
+
// Check if this node is a local reference
|
|
181
|
+
if (node['expr'] === 'local' && node['name'] === localName) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Recursively search all properties
|
|
186
|
+
for (const [key, value] of Object.entries(node)) {
|
|
187
|
+
if (key === 'expr' || key === 'kind') continue;
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
for (const item of value) {
|
|
190
|
+
if (hasLocalReference(item, localName)) return true;
|
|
191
|
+
}
|
|
192
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
193
|
+
if (hasLocalReference(value, localName)) return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if view contains a specific role anywhere in the tree (including inside each body)
|
|
202
|
+
*/
|
|
203
|
+
function hasRoleAnywhere(view: unknown, role: string): boolean {
|
|
204
|
+
if (!view || typeof view !== 'object') return false;
|
|
205
|
+
const node = view as Record<string, unknown>;
|
|
206
|
+
|
|
207
|
+
// Check if this element has the role
|
|
208
|
+
if (node['kind'] === 'element' && node['props']) {
|
|
209
|
+
const props = node['props'] as Record<string, unknown>;
|
|
210
|
+
if (props['role'] && typeof props['role'] === 'object') {
|
|
211
|
+
const roleExpr = props['role'] as Record<string, unknown>;
|
|
212
|
+
if (roleExpr['expr'] === 'lit' && roleExpr['value'] === role) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check children
|
|
219
|
+
if (node['children'] && Array.isArray(node['children'])) {
|
|
220
|
+
for (const child of node['children']) {
|
|
221
|
+
if (hasRoleAnywhere(child, role)) return true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check conditional branches
|
|
226
|
+
if (node['kind'] === 'if') {
|
|
227
|
+
if (hasRoleAnywhere(node['then'], role)) return true;
|
|
228
|
+
if (node['else'] && hasRoleAnywhere(node['else'], role)) return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check each body
|
|
232
|
+
if (node['kind'] === 'each' && hasRoleAnywhere(node['body'], role)) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if view contains a specific element tag
|
|
241
|
+
*/
|
|
242
|
+
function hasElementTag(view: unknown, targetTag: string): boolean {
|
|
243
|
+
if (!view || typeof view !== 'object') return false;
|
|
244
|
+
const node = view as Record<string, unknown>;
|
|
245
|
+
|
|
246
|
+
if (node['kind'] === 'element' && node['tag'] === targetTag) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check children
|
|
251
|
+
if (node['children'] && Array.isArray(node['children'])) {
|
|
252
|
+
for (const child of node['children']) {
|
|
253
|
+
if (hasElementTag(child, targetTag)) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check conditional branches
|
|
260
|
+
if (node['kind'] === 'if') {
|
|
261
|
+
if (hasElementTag(node['then'], targetTag)) return true;
|
|
262
|
+
if (node['else'] && hasElementTag(node['else'], targetTag)) return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Check each body
|
|
266
|
+
if (node['kind'] === 'each' && hasElementTag(node['body'], targetTag)) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Find an expression call in the view tree
|
|
275
|
+
*/
|
|
276
|
+
function findExpressionCall(view: unknown, methodName: string): unknown | null {
|
|
277
|
+
if (!view || typeof view !== 'object') return null;
|
|
278
|
+
const node = view as Record<string, unknown>;
|
|
279
|
+
|
|
280
|
+
// Check if this node is a call expression
|
|
281
|
+
if (node['expr'] === 'call' && node['method'] === methodName) {
|
|
282
|
+
return node;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Search in props
|
|
286
|
+
if (node['props'] && typeof node['props'] === 'object') {
|
|
287
|
+
for (const prop of Object.values(node['props'] as Record<string, unknown>)) {
|
|
288
|
+
const found = findExpressionCall(prop, methodName);
|
|
289
|
+
if (found) return found;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Search in children
|
|
294
|
+
if (node['children'] && Array.isArray(node['children'])) {
|
|
295
|
+
for (const child of node['children']) {
|
|
296
|
+
const found = findExpressionCall(child, methodName);
|
|
297
|
+
if (found) return found;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Search conditional branches
|
|
302
|
+
if (node['kind'] === 'if') {
|
|
303
|
+
const thenResult = findExpressionCall(node['then'], methodName);
|
|
304
|
+
if (thenResult) return thenResult;
|
|
305
|
+
if (node['else']) {
|
|
306
|
+
const elseResult = findExpressionCall(node['else'], methodName);
|
|
307
|
+
if (elseResult) return elseResult;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Search each body and items
|
|
312
|
+
if (node['kind'] === 'each') {
|
|
313
|
+
const itemsResult = findExpressionCall(node['items'], methodName);
|
|
314
|
+
if (itemsResult) return itemsResult;
|
|
315
|
+
const bodyResult = findExpressionCall(node['body'], methodName);
|
|
316
|
+
if (bodyResult) return bodyResult;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Search in get expression obj
|
|
320
|
+
if (node['expr'] === 'get' && node['obj']) {
|
|
321
|
+
const objResult = findExpressionCall(node['obj'], methodName);
|
|
322
|
+
if (objResult) return objResult;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Search in call expression target and args
|
|
326
|
+
if (node['expr'] === 'call') {
|
|
327
|
+
if (node['target']) {
|
|
328
|
+
const targetResult = findExpressionCall(node['target'], methodName);
|
|
329
|
+
if (targetResult) return targetResult;
|
|
330
|
+
}
|
|
331
|
+
if (node['args'] && Array.isArray(node['args'])) {
|
|
332
|
+
for (const arg of node['args']) {
|
|
333
|
+
const argResult = findExpressionCall(arg, methodName);
|
|
334
|
+
if (argResult) return argResult;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Check if view uses a specific style preset
|
|
344
|
+
*/
|
|
345
|
+
function usesStylePreset(view: unknown, presetName: string): boolean {
|
|
346
|
+
if (!view || typeof view !== 'object') return false;
|
|
347
|
+
const node = view as Record<string, unknown>;
|
|
348
|
+
|
|
349
|
+
// Check className prop for style expression
|
|
350
|
+
if (node['props'] && typeof node['props'] === 'object') {
|
|
351
|
+
const props = node['props'] as Record<string, unknown>;
|
|
352
|
+
if (props['className'] && typeof props['className'] === 'object') {
|
|
353
|
+
const className = props['className'] as Record<string, unknown>;
|
|
354
|
+
if (className['expr'] === 'style' && className['preset'] === presetName) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Search in children
|
|
361
|
+
if (node['children'] && Array.isArray(node['children'])) {
|
|
362
|
+
for (const child of node['children']) {
|
|
363
|
+
if (usesStylePreset(child, presetName)) {
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Search conditional branches
|
|
370
|
+
if (node['kind'] === 'if') {
|
|
371
|
+
if (usesStylePreset(node['then'], presetName)) return true;
|
|
372
|
+
if (node['else'] && usesStylePreset(node['else'], presetName)) return true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Search each body
|
|
376
|
+
if (node['kind'] === 'each' && usesStylePreset(node['body'], presetName)) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ==================== Test Contexts ====================
|
|
384
|
+
|
|
385
|
+
interface ChartTestContext {
|
|
386
|
+
barChart: ComponentDef;
|
|
387
|
+
lineChart: ComponentDef;
|
|
388
|
+
areaChart: ComponentDef;
|
|
389
|
+
pieChart: ComponentDef;
|
|
390
|
+
donutChart: ComponentDef;
|
|
391
|
+
scatterChart: ComponentDef;
|
|
392
|
+
radarChart: ComponentDef;
|
|
393
|
+
chartLegend: ComponentDef;
|
|
394
|
+
chartTooltip: ComponentDef;
|
|
395
|
+
chartAxis: ComponentDef;
|
|
396
|
+
styles: Record<string, StylePreset>;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
describe('Chart Component Suite', () => {
|
|
400
|
+
let ctx: ChartTestContext;
|
|
401
|
+
|
|
402
|
+
beforeAll(() => {
|
|
403
|
+
ctx = {
|
|
404
|
+
barChart: loadChartComponent('bar-chart'),
|
|
405
|
+
lineChart: loadChartComponent('line-chart'),
|
|
406
|
+
areaChart: loadChartComponent('area-chart'),
|
|
407
|
+
pieChart: loadChartComponent('pie-chart'),
|
|
408
|
+
donutChart: loadChartComponent('donut-chart'),
|
|
409
|
+
scatterChart: loadChartComponent('scatter-chart'),
|
|
410
|
+
radarChart: loadChartComponent('radar-chart'),
|
|
411
|
+
chartLegend: loadChartComponent('chart-legend'),
|
|
412
|
+
chartTooltip: loadChartComponent('chart-tooltip'),
|
|
413
|
+
chartAxis: loadChartComponent('chart-axis'),
|
|
414
|
+
styles: loadChartStyles(),
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// ============================================================
|
|
419
|
+
// BAR CHART COMPONENT
|
|
420
|
+
// ============================================================
|
|
421
|
+
|
|
422
|
+
describe('BarChart Component', () => {
|
|
423
|
+
// ==================== Component Structure Tests ====================
|
|
424
|
+
|
|
425
|
+
describe('Component Structure', () => {
|
|
426
|
+
it('should have valid component structure', () => {
|
|
427
|
+
assertValidComponent(ctx.barChart);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should have svg as root element', () => {
|
|
431
|
+
const rootTag = getRootTag(ctx.barChart);
|
|
432
|
+
expect(rootTag).toBe('svg');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should have role="img" on SVG for accessibility', () => {
|
|
436
|
+
expect(hasRole(ctx.barChart.view, 'img')).toBe(true);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should have className using StyleExpr with chartSvg preset', () => {
|
|
440
|
+
const className = findPropInView(ctx.barChart.view, 'className');
|
|
441
|
+
expect(className).not.toBeNull();
|
|
442
|
+
expect(className).toMatchObject({
|
|
443
|
+
expr: 'style',
|
|
444
|
+
preset: 'chartSvg',
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should contain rect elements for bars', () => {
|
|
449
|
+
expect(hasElementTag(ctx.barChart.view, 'rect')).toBe(true);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// ==================== Params Validation Tests ====================
|
|
454
|
+
|
|
455
|
+
describe('Params Validation', () => {
|
|
456
|
+
describe('required params', () => {
|
|
457
|
+
it('should have data param as required with type list', () => {
|
|
458
|
+
expect(isRequiredParam(ctx.barChart, 'data')).toBe(true);
|
|
459
|
+
expect(hasParamType(ctx.barChart, 'data', 'list')).toBe(true);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('should have valueKey param as required with type string', () => {
|
|
463
|
+
expect(isRequiredParam(ctx.barChart, 'valueKey')).toBe(true);
|
|
464
|
+
expect(hasParamType(ctx.barChart, 'valueKey', 'string')).toBe(true);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
describe('optional params', () => {
|
|
469
|
+
const optionalParams = [
|
|
470
|
+
{ name: 'labelKey', type: 'string' },
|
|
471
|
+
{ name: 'width', type: 'number' },
|
|
472
|
+
{ name: 'height', type: 'number' },
|
|
473
|
+
{ name: 'colors', type: 'list' },
|
|
474
|
+
{ name: 'showGrid', type: 'boolean' },
|
|
475
|
+
{ name: 'showLabels', type: 'boolean' },
|
|
476
|
+
{ name: 'orientation', type: 'string' },
|
|
477
|
+
{ name: 'barGap', type: 'number' },
|
|
478
|
+
{ name: 'barRadius', type: 'number' },
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
it.each(optionalParams)(
|
|
482
|
+
'should have $name param as optional with type $type',
|
|
483
|
+
({ name, type }) => {
|
|
484
|
+
expect(isOptionalParam(ctx.barChart, name)).toBe(true);
|
|
485
|
+
expect(hasParamType(ctx.barChart, name, type as 'string' | 'number' | 'boolean' | 'list')).toBe(true);
|
|
486
|
+
}
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should have all expected params', () => {
|
|
491
|
+
const expectedParams = [
|
|
492
|
+
'data',
|
|
493
|
+
'valueKey',
|
|
494
|
+
'labelKey',
|
|
495
|
+
'width',
|
|
496
|
+
'height',
|
|
497
|
+
'colors',
|
|
498
|
+
'showGrid',
|
|
499
|
+
'showLabels',
|
|
500
|
+
'orientation',
|
|
501
|
+
'barGap',
|
|
502
|
+
'barRadius',
|
|
503
|
+
];
|
|
504
|
+
expect(hasParams(ctx.barChart, expectedParams)).toBe(true);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// ==================== Accessibility Tests ====================
|
|
509
|
+
|
|
510
|
+
describe('Accessibility', () => {
|
|
511
|
+
it('should have role="img" on SVG for screen readers', () => {
|
|
512
|
+
const role = findPropInView(ctx.barChart.view, 'role');
|
|
513
|
+
expect(role).not.toBeNull();
|
|
514
|
+
expect(role).toMatchObject({
|
|
515
|
+
expr: 'lit',
|
|
516
|
+
value: 'img',
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('should have aria-label attribute', () => {
|
|
521
|
+
expect(hasAriaAttribute(ctx.barChart.view, 'label')).toBe(true);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('should have title element for chart description', () => {
|
|
525
|
+
expect(hasElementTag(ctx.barChart.view, 'title')).toBe(true);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('should have aria-labelledby referencing title element', () => {
|
|
529
|
+
expect(hasAriaAttribute(ctx.barChart.view, 'labelledby')).toBe(true);
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ==================== Rendering Tests ====================
|
|
534
|
+
|
|
535
|
+
describe('Rendering', () => {
|
|
536
|
+
it('should use getBarDimensions helper for bar positioning', () => {
|
|
537
|
+
const call = findExpressionCall(ctx.barChart.view, 'getBarDimensions');
|
|
538
|
+
expect(call).not.toBeNull();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('should apply chartBar style preset to bar elements', () => {
|
|
542
|
+
expect(usesStylePreset(ctx.barChart.view, 'chartBar')).toBe(true);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should render grid when showGrid is true', () => {
|
|
546
|
+
// Grid should be conditionally rendered using chartGrid preset
|
|
547
|
+
expect(usesStylePreset(ctx.barChart.view, 'chartGrid')).toBe(true);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('should render labels when showLabels is true', () => {
|
|
551
|
+
// Labels should use chartLabel preset
|
|
552
|
+
expect(usesStylePreset(ctx.barChart.view, 'chartLabel')).toBe(true);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('should support both vertical and horizontal orientations', () => {
|
|
556
|
+
// Component should have orientation param
|
|
557
|
+
expect(hasParams(ctx.barChart, ['orientation'])).toBe(true);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// ==================== Style Preset Usage Tests ====================
|
|
562
|
+
|
|
563
|
+
describe('Style Preset Usage', () => {
|
|
564
|
+
it('should use chartSvg preset for root SVG', () => {
|
|
565
|
+
const className = findPropInView(ctx.barChart.view, 'className');
|
|
566
|
+
expect(className).toMatchObject({
|
|
567
|
+
expr: 'style',
|
|
568
|
+
preset: 'chartSvg',
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('should use chartBar preset for bar rectangles', () => {
|
|
573
|
+
expect(usesStylePreset(ctx.barChart.view, 'chartBar')).toBe(true);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// ============================================================
|
|
579
|
+
// LINE CHART COMPONENT
|
|
580
|
+
// ============================================================
|
|
581
|
+
|
|
582
|
+
describe('LineChart Component', () => {
|
|
583
|
+
// ==================== Component Structure Tests ====================
|
|
584
|
+
|
|
585
|
+
describe('Component Structure', () => {
|
|
586
|
+
it('should have valid component structure', () => {
|
|
587
|
+
assertValidComponent(ctx.lineChart);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('should have svg as root element', () => {
|
|
591
|
+
const rootTag = getRootTag(ctx.lineChart);
|
|
592
|
+
expect(rootTag).toBe('svg');
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('should have role="img" on SVG for accessibility', () => {
|
|
596
|
+
expect(hasRole(ctx.lineChart.view, 'img')).toBe(true);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('should have className using StyleExpr with chartSvg preset', () => {
|
|
600
|
+
const className = findPropInView(ctx.lineChart.view, 'className');
|
|
601
|
+
expect(className).not.toBeNull();
|
|
602
|
+
expect(className).toMatchObject({
|
|
603
|
+
expr: 'style',
|
|
604
|
+
preset: 'chartSvg',
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('should contain path element for line', () => {
|
|
609
|
+
expect(hasElementTag(ctx.lineChart.view, 'path')).toBe(true);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('should contain circle elements for data points when showPoints is true', () => {
|
|
613
|
+
expect(hasElementTag(ctx.lineChart.view, 'circle')).toBe(true);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// ==================== Params Validation Tests ====================
|
|
618
|
+
|
|
619
|
+
describe('Params Validation', () => {
|
|
620
|
+
describe('required params', () => {
|
|
621
|
+
it('should have data param as required with type list', () => {
|
|
622
|
+
expect(isRequiredParam(ctx.lineChart, 'data')).toBe(true);
|
|
623
|
+
expect(hasParamType(ctx.lineChart, 'data', 'list')).toBe(true);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('should have valueKey param as required with type string', () => {
|
|
627
|
+
expect(isRequiredParam(ctx.lineChart, 'valueKey')).toBe(true);
|
|
628
|
+
expect(hasParamType(ctx.lineChart, 'valueKey', 'string')).toBe(true);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
describe('optional params', () => {
|
|
633
|
+
const optionalParams = [
|
|
634
|
+
{ name: 'labelKey', type: 'string' },
|
|
635
|
+
{ name: 'width', type: 'number' },
|
|
636
|
+
{ name: 'height', type: 'number' },
|
|
637
|
+
{ name: 'colors', type: 'list' },
|
|
638
|
+
{ name: 'showGrid', type: 'boolean' },
|
|
639
|
+
{ name: 'showLabels', type: 'boolean' },
|
|
640
|
+
{ name: 'curved', type: 'boolean' },
|
|
641
|
+
{ name: 'showPoints', type: 'boolean' },
|
|
642
|
+
{ name: 'pointRadius', type: 'number' },
|
|
643
|
+
{ name: 'strokeWidth', type: 'number' },
|
|
644
|
+
];
|
|
645
|
+
|
|
646
|
+
it.each(optionalParams)(
|
|
647
|
+
'should have $name param as optional with type $type',
|
|
648
|
+
({ name, type }) => {
|
|
649
|
+
expect(isOptionalParam(ctx.lineChart, name)).toBe(true);
|
|
650
|
+
expect(hasParamType(ctx.lineChart, name, type as 'string' | 'number' | 'boolean' | 'list')).toBe(true);
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('should have all expected params', () => {
|
|
656
|
+
const expectedParams = [
|
|
657
|
+
'data',
|
|
658
|
+
'valueKey',
|
|
659
|
+
'labelKey',
|
|
660
|
+
'width',
|
|
661
|
+
'height',
|
|
662
|
+
'colors',
|
|
663
|
+
'showGrid',
|
|
664
|
+
'showLabels',
|
|
665
|
+
'curved',
|
|
666
|
+
'showPoints',
|
|
667
|
+
'pointRadius',
|
|
668
|
+
'strokeWidth',
|
|
669
|
+
];
|
|
670
|
+
expect(hasParams(ctx.lineChart, expectedParams)).toBe(true);
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// ==================== Accessibility Tests ====================
|
|
675
|
+
|
|
676
|
+
describe('Accessibility', () => {
|
|
677
|
+
it('should have role="img" on SVG for screen readers', () => {
|
|
678
|
+
const role = findPropInView(ctx.lineChart.view, 'role');
|
|
679
|
+
expect(role).not.toBeNull();
|
|
680
|
+
expect(role).toMatchObject({
|
|
681
|
+
expr: 'lit',
|
|
682
|
+
value: 'img',
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('should have aria-label attribute', () => {
|
|
687
|
+
expect(hasAriaAttribute(ctx.lineChart.view, 'label')).toBe(true);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it('should have title element for chart description', () => {
|
|
691
|
+
expect(hasElementTag(ctx.lineChart.view, 'title')).toBe(true);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('should have aria-labelledby referencing title element', () => {
|
|
695
|
+
expect(hasAriaAttribute(ctx.lineChart.view, 'labelledby')).toBe(true);
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// ==================== Rendering Tests ====================
|
|
700
|
+
|
|
701
|
+
describe('Rendering', () => {
|
|
702
|
+
it('should use getLinePath helper for path generation', () => {
|
|
703
|
+
const call = findExpressionCall(ctx.lineChart.view, 'getLinePath');
|
|
704
|
+
expect(call).not.toBeNull();
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('should apply chartLine style preset to path elements', () => {
|
|
708
|
+
expect(usesStylePreset(ctx.lineChart.view, 'chartLine')).toBe(true);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('should apply chartPoint style preset to point circles', () => {
|
|
712
|
+
expect(usesStylePreset(ctx.lineChart.view, 'chartPoint')).toBe(true);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it('should render grid when showGrid is true', () => {
|
|
716
|
+
expect(usesStylePreset(ctx.lineChart.view, 'chartGrid')).toBe(true);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('should render labels when showLabels is true', () => {
|
|
720
|
+
expect(usesStylePreset(ctx.lineChart.view, 'chartLabel')).toBe(true);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('should support curved lines when curved is true', () => {
|
|
724
|
+
// Component should have curved param
|
|
725
|
+
expect(hasParams(ctx.lineChart, ['curved'])).toBe(true);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* TDD Red Phase: Task 3 - curved parameter integration tests
|
|
730
|
+
* These tests verify that the curved parameter is passed to getLinePath.
|
|
731
|
+
* Currently the call only has 1 argument (points), so tests will FAIL.
|
|
732
|
+
*/
|
|
733
|
+
it('should pass curved parameter to getLinePath call (Task 3)', () => {
|
|
734
|
+
// The getLinePath call should have 2 arguments: points and curved
|
|
735
|
+
const call = findExpressionCall(ctx.lineChart.view, 'getLinePath') as {
|
|
736
|
+
args?: unknown[];
|
|
737
|
+
} | null;
|
|
738
|
+
expect(call).not.toBeNull();
|
|
739
|
+
expect(call?.args).toHaveLength(2);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('should include curved param reference in getLinePath args (Task 3)', () => {
|
|
743
|
+
// The second argument should reference the curved param
|
|
744
|
+
const call = findExpressionCall(ctx.lineChart.view, 'getLinePath') as {
|
|
745
|
+
args?: Array<{ expr?: string; name?: string }>;
|
|
746
|
+
} | null;
|
|
747
|
+
expect(call).not.toBeNull();
|
|
748
|
+
expect(call?.args?.[1]).toMatchObject({
|
|
749
|
+
expr: 'param',
|
|
750
|
+
name: 'curved',
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// ==================== Style Preset Usage Tests ====================
|
|
756
|
+
|
|
757
|
+
describe('Style Preset Usage', () => {
|
|
758
|
+
it('should use chartSvg preset for root SVG', () => {
|
|
759
|
+
const className = findPropInView(ctx.lineChart.view, 'className');
|
|
760
|
+
expect(className).toMatchObject({
|
|
761
|
+
expr: 'style',
|
|
762
|
+
preset: 'chartSvg',
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it('should use chartLine preset for line paths', () => {
|
|
767
|
+
expect(usesStylePreset(ctx.lineChart.view, 'chartLine')).toBe(true);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('should use chartPoint preset for data points', () => {
|
|
771
|
+
expect(usesStylePreset(ctx.lineChart.view, 'chartPoint')).toBe(true);
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// ============================================================
|
|
777
|
+
// AREA CHART COMPONENT
|
|
778
|
+
// ============================================================
|
|
779
|
+
|
|
780
|
+
describe('AreaChart Component', () => {
|
|
781
|
+
// ==================== Component Structure Tests ====================
|
|
782
|
+
|
|
783
|
+
describe('Component Structure', () => {
|
|
784
|
+
it('should have valid component structure', () => {
|
|
785
|
+
assertValidComponent(ctx.areaChart);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('should have svg as root element', () => {
|
|
789
|
+
const rootTag = getRootTag(ctx.areaChart);
|
|
790
|
+
expect(rootTag).toBe('svg');
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('should have role="img" on SVG for accessibility', () => {
|
|
794
|
+
expect(hasRole(ctx.areaChart.view, 'img')).toBe(true);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('should have className using StyleExpr with chartSvg preset', () => {
|
|
798
|
+
const className = findPropInView(ctx.areaChart.view, 'className');
|
|
799
|
+
expect(className).not.toBeNull();
|
|
800
|
+
expect(className).toMatchObject({
|
|
801
|
+
expr: 'style',
|
|
802
|
+
preset: 'chartSvg',
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('should contain path element for area fill', () => {
|
|
807
|
+
expect(hasElementTag(ctx.areaChart.view, 'path')).toBe(true);
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// ==================== Params Validation Tests ====================
|
|
812
|
+
|
|
813
|
+
describe('Params Validation', () => {
|
|
814
|
+
describe('required params', () => {
|
|
815
|
+
it('should have data param as required with type list', () => {
|
|
816
|
+
expect(isRequiredParam(ctx.areaChart, 'data')).toBe(true);
|
|
817
|
+
expect(hasParamType(ctx.areaChart, 'data', 'list')).toBe(true);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('should have valueKey param as required with type string', () => {
|
|
821
|
+
expect(isRequiredParam(ctx.areaChart, 'valueKey')).toBe(true);
|
|
822
|
+
expect(hasParamType(ctx.areaChart, 'valueKey', 'string')).toBe(true);
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
describe('optional params', () => {
|
|
827
|
+
const optionalParams = [
|
|
828
|
+
{ name: 'labelKey', type: 'string' },
|
|
829
|
+
{ name: 'width', type: 'number' },
|
|
830
|
+
{ name: 'height', type: 'number' },
|
|
831
|
+
{ name: 'colors', type: 'list' },
|
|
832
|
+
{ name: 'showGrid', type: 'boolean' },
|
|
833
|
+
{ name: 'showLabels', type: 'boolean' },
|
|
834
|
+
{ name: 'curved', type: 'boolean' },
|
|
835
|
+
{ name: 'showPoints', type: 'boolean' },
|
|
836
|
+
{ name: 'pointRadius', type: 'number' },
|
|
837
|
+
{ name: 'strokeWidth', type: 'number' },
|
|
838
|
+
{ name: 'fillOpacity', type: 'number' },
|
|
839
|
+
];
|
|
840
|
+
|
|
841
|
+
it.each(optionalParams)(
|
|
842
|
+
'should have $name param as optional with type $type',
|
|
843
|
+
({ name, type }) => {
|
|
844
|
+
expect(isOptionalParam(ctx.areaChart, name)).toBe(true);
|
|
845
|
+
expect(hasParamType(ctx.areaChart, name, type as 'string' | 'number' | 'boolean' | 'list')).toBe(true);
|
|
846
|
+
}
|
|
847
|
+
);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('should have all expected params', () => {
|
|
851
|
+
const expectedParams = [
|
|
852
|
+
'data',
|
|
853
|
+
'valueKey',
|
|
854
|
+
'labelKey',
|
|
855
|
+
'width',
|
|
856
|
+
'height',
|
|
857
|
+
'colors',
|
|
858
|
+
'showGrid',
|
|
859
|
+
'showLabels',
|
|
860
|
+
'curved',
|
|
861
|
+
'showPoints',
|
|
862
|
+
'pointRadius',
|
|
863
|
+
'strokeWidth',
|
|
864
|
+
'fillOpacity',
|
|
865
|
+
];
|
|
866
|
+
expect(hasParams(ctx.areaChart, expectedParams)).toBe(true);
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// ==================== Accessibility Tests ====================
|
|
871
|
+
|
|
872
|
+
describe('Accessibility', () => {
|
|
873
|
+
it('should have role="img" on SVG for screen readers', () => {
|
|
874
|
+
const role = findPropInView(ctx.areaChart.view, 'role');
|
|
875
|
+
expect(role).not.toBeNull();
|
|
876
|
+
expect(role).toMatchObject({
|
|
877
|
+
expr: 'lit',
|
|
878
|
+
value: 'img',
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('should have aria-label attribute', () => {
|
|
883
|
+
expect(hasAriaAttribute(ctx.areaChart.view, 'label')).toBe(true);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it('should have title element for chart description', () => {
|
|
887
|
+
expect(hasElementTag(ctx.areaChart.view, 'title')).toBe(true);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it('should have aria-labelledby referencing title element', () => {
|
|
891
|
+
expect(hasAriaAttribute(ctx.areaChart.view, 'labelledby')).toBe(true);
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// ==================== Rendering Tests ====================
|
|
896
|
+
|
|
897
|
+
describe('Rendering', () => {
|
|
898
|
+
it('should use getAreaPath helper for area fill path generation', () => {
|
|
899
|
+
const call = findExpressionCall(ctx.areaChart.view, 'getAreaPath');
|
|
900
|
+
expect(call).not.toBeNull();
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('should apply chartArea style preset to area path', () => {
|
|
904
|
+
expect(usesStylePreset(ctx.areaChart.view, 'chartArea')).toBe(true);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it('should render grid when showGrid is true', () => {
|
|
908
|
+
expect(usesStylePreset(ctx.areaChart.view, 'chartGrid')).toBe(true);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('should render labels when showLabels is true', () => {
|
|
912
|
+
expect(usesStylePreset(ctx.areaChart.view, 'chartLabel')).toBe(true);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it('should support curved areas when curved is true', () => {
|
|
916
|
+
expect(hasParams(ctx.areaChart, ['curved'])).toBe(true);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('should support configurable fill opacity', () => {
|
|
920
|
+
expect(hasParams(ctx.areaChart, ['fillOpacity'])).toBe(true);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* TDD Red Phase: Task 3 - curved parameter integration tests for AreaChart
|
|
925
|
+
* These tests verify that the curved parameter is passed to getAreaPath and getLinePath.
|
|
926
|
+
* Currently the calls don't include the curved parameter, so tests will FAIL.
|
|
927
|
+
*/
|
|
928
|
+
it('should pass curved parameter to getAreaPath call (Task 3)', () => {
|
|
929
|
+
// The getAreaPath call should have 3 arguments: points, baseline, and curved
|
|
930
|
+
const call = findExpressionCall(ctx.areaChart.view, 'getAreaPath') as {
|
|
931
|
+
args?: unknown[];
|
|
932
|
+
} | null;
|
|
933
|
+
expect(call).not.toBeNull();
|
|
934
|
+
expect(call?.args).toHaveLength(3);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it('should include curved param reference in getAreaPath args (Task 3)', () => {
|
|
938
|
+
// The third argument should reference the curved param
|
|
939
|
+
const call = findExpressionCall(ctx.areaChart.view, 'getAreaPath') as {
|
|
940
|
+
args?: Array<{ expr?: string; name?: string }>;
|
|
941
|
+
} | null;
|
|
942
|
+
expect(call).not.toBeNull();
|
|
943
|
+
expect(call?.args?.[2]).toMatchObject({
|
|
944
|
+
expr: 'param',
|
|
945
|
+
name: 'curved',
|
|
946
|
+
});
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
it('should pass curved parameter to getLinePath call for stroke (Task 3)', () => {
|
|
950
|
+
// AreaChart uses getLinePath for the stroke overlay
|
|
951
|
+
// It should also receive the curved parameter
|
|
952
|
+
const call = findExpressionCall(ctx.areaChart.view, 'getLinePath') as {
|
|
953
|
+
args?: unknown[];
|
|
954
|
+
} | null;
|
|
955
|
+
expect(call).not.toBeNull();
|
|
956
|
+
expect(call?.args).toHaveLength(2);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it('should include curved param reference in getLinePath args for stroke (Task 3)', () => {
|
|
960
|
+
// The second argument should reference the curved param
|
|
961
|
+
const call = findExpressionCall(ctx.areaChart.view, 'getLinePath') as {
|
|
962
|
+
args?: Array<{ expr?: string; name?: string }>;
|
|
963
|
+
} | null;
|
|
964
|
+
expect(call).not.toBeNull();
|
|
965
|
+
expect(call?.args?.[1]).toMatchObject({
|
|
966
|
+
expr: 'param',
|
|
967
|
+
name: 'curved',
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// ==================== Style Preset Usage Tests ====================
|
|
973
|
+
|
|
974
|
+
describe('Style Preset Usage', () => {
|
|
975
|
+
it('should use chartSvg preset for root SVG', () => {
|
|
976
|
+
const className = findPropInView(ctx.areaChart.view, 'className');
|
|
977
|
+
expect(className).toMatchObject({
|
|
978
|
+
expr: 'style',
|
|
979
|
+
preset: 'chartSvg',
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
it('should use chartArea preset for area fill', () => {
|
|
984
|
+
expect(usesStylePreset(ctx.areaChart.view, 'chartArea')).toBe(true);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it('should optionally use chartLine preset for stroke overlay', () => {
|
|
988
|
+
expect(usesStylePreset(ctx.areaChart.view, 'chartLine')).toBe(true);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
it('should use chartPoint preset for data points when showPoints is true', () => {
|
|
992
|
+
expect(usesStylePreset(ctx.areaChart.view, 'chartPoint')).toBe(true);
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// ============================================================
|
|
998
|
+
// PIE CHART COMPONENT
|
|
999
|
+
// ============================================================
|
|
1000
|
+
|
|
1001
|
+
describe('PieChart Component', () => {
|
|
1002
|
+
// ==================== Component Structure Tests ====================
|
|
1003
|
+
|
|
1004
|
+
describe('Component Structure', () => {
|
|
1005
|
+
it('should have valid component structure', () => {
|
|
1006
|
+
assertValidComponent(ctx.pieChart);
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
it('should have svg as root element', () => {
|
|
1010
|
+
const rootTag = getRootTag(ctx.pieChart);
|
|
1011
|
+
expect(rootTag).toBe('svg');
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('should have role="img" on SVG for accessibility', () => {
|
|
1015
|
+
expect(hasRole(ctx.pieChart.view, 'img')).toBe(true);
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
it('should have className using StyleExpr with chartSvg preset', () => {
|
|
1019
|
+
const className = findPropInView(ctx.pieChart.view, 'className');
|
|
1020
|
+
expect(className).not.toBeNull();
|
|
1021
|
+
expect(className).toMatchObject({
|
|
1022
|
+
expr: 'style',
|
|
1023
|
+
preset: 'chartSvg',
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it('should contain path elements for pie slices', () => {
|
|
1028
|
+
expect(hasElementTag(ctx.pieChart.view, 'path')).toBe(true);
|
|
1029
|
+
});
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
// ==================== Params Validation Tests ====================
|
|
1033
|
+
|
|
1034
|
+
describe('Params Validation', () => {
|
|
1035
|
+
describe('required params', () => {
|
|
1036
|
+
it('should have data param as required with type list', () => {
|
|
1037
|
+
expect(isRequiredParam(ctx.pieChart, 'data')).toBe(true);
|
|
1038
|
+
expect(hasParamType(ctx.pieChart, 'data', 'list')).toBe(true);
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
it('should have valueKey param as required with type string', () => {
|
|
1042
|
+
expect(isRequiredParam(ctx.pieChart, 'valueKey')).toBe(true);
|
|
1043
|
+
expect(hasParamType(ctx.pieChart, 'valueKey', 'string')).toBe(true);
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
describe('optional params', () => {
|
|
1048
|
+
const optionalParams = [
|
|
1049
|
+
{ name: 'labelKey', type: 'string' },
|
|
1050
|
+
{ name: 'width', type: 'number' },
|
|
1051
|
+
{ name: 'height', type: 'number' },
|
|
1052
|
+
{ name: 'colors', type: 'list' },
|
|
1053
|
+
{ name: 'showLabels', type: 'boolean' },
|
|
1054
|
+
{ name: 'showPercentage', type: 'boolean' },
|
|
1055
|
+
];
|
|
1056
|
+
|
|
1057
|
+
it.each(optionalParams)(
|
|
1058
|
+
'should have $name param as optional with type $type',
|
|
1059
|
+
({ name, type }) => {
|
|
1060
|
+
expect(isOptionalParam(ctx.pieChart, name)).toBe(true);
|
|
1061
|
+
expect(hasParamType(ctx.pieChart, name, type as 'string' | 'number' | 'boolean' | 'list')).toBe(true);
|
|
1062
|
+
}
|
|
1063
|
+
);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it('should have all expected params', () => {
|
|
1067
|
+
const expectedParams = [
|
|
1068
|
+
'data',
|
|
1069
|
+
'valueKey',
|
|
1070
|
+
'labelKey',
|
|
1071
|
+
'width',
|
|
1072
|
+
'height',
|
|
1073
|
+
'colors',
|
|
1074
|
+
'showLabels',
|
|
1075
|
+
'showPercentage',
|
|
1076
|
+
];
|
|
1077
|
+
expect(hasParams(ctx.pieChart, expectedParams)).toBe(true);
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
// ==================== Accessibility Tests ====================
|
|
1082
|
+
|
|
1083
|
+
describe('Accessibility', () => {
|
|
1084
|
+
it('should have role="img" on SVG for screen readers', () => {
|
|
1085
|
+
const role = findPropInView(ctx.pieChart.view, 'role');
|
|
1086
|
+
expect(role).not.toBeNull();
|
|
1087
|
+
expect(role).toMatchObject({
|
|
1088
|
+
expr: 'lit',
|
|
1089
|
+
value: 'img',
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
it('should have aria-label attribute', () => {
|
|
1094
|
+
expect(hasAriaAttribute(ctx.pieChart.view, 'label')).toBe(true);
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
it('should have title element for chart description', () => {
|
|
1098
|
+
expect(hasElementTag(ctx.pieChart.view, 'title')).toBe(true);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
it('should have aria-labelledby referencing title element', () => {
|
|
1102
|
+
expect(hasAriaAttribute(ctx.pieChart.view, 'labelledby')).toBe(true);
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// ==================== Rendering Tests ====================
|
|
1107
|
+
|
|
1108
|
+
describe('Rendering', () => {
|
|
1109
|
+
it('should use getPieSlices helper for slice calculation', () => {
|
|
1110
|
+
const call = findExpressionCall(ctx.pieChart.view, 'getPieSlices');
|
|
1111
|
+
expect(call).not.toBeNull();
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
it('should use getArcPath helper for path generation', () => {
|
|
1115
|
+
const call = findExpressionCall(ctx.pieChart.view, 'getArcPath');
|
|
1116
|
+
expect(call).not.toBeNull();
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
it('should apply chartSlice style preset to slice elements', () => {
|
|
1120
|
+
expect(usesStylePreset(ctx.pieChart.view, 'chartSlice')).toBe(true);
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it('should render labels when showLabels is true', () => {
|
|
1124
|
+
expect(usesStylePreset(ctx.pieChart.view, 'chartLabel')).toBe(true);
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
it('should support percentage display when showPercentage is true', () => {
|
|
1128
|
+
expect(hasParams(ctx.pieChart, ['showPercentage'])).toBe(true);
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
// ==================== Style Preset Usage Tests ====================
|
|
1133
|
+
|
|
1134
|
+
describe('Style Preset Usage', () => {
|
|
1135
|
+
it('should use chartSvg preset for root SVG', () => {
|
|
1136
|
+
const className = findPropInView(ctx.pieChart.view, 'className');
|
|
1137
|
+
expect(className).toMatchObject({
|
|
1138
|
+
expr: 'style',
|
|
1139
|
+
preset: 'chartSvg',
|
|
1140
|
+
});
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
it('should use chartSlice preset for pie slices', () => {
|
|
1144
|
+
expect(usesStylePreset(ctx.pieChart.view, 'chartSlice')).toBe(true);
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// ============================================================
|
|
1150
|
+
// DONUT CHART COMPONENT
|
|
1151
|
+
// ============================================================
|
|
1152
|
+
|
|
1153
|
+
describe('DonutChart Component', () => {
|
|
1154
|
+
// ==================== Component Structure Tests ====================
|
|
1155
|
+
|
|
1156
|
+
describe('Component Structure', () => {
|
|
1157
|
+
it('should have valid component structure', () => {
|
|
1158
|
+
assertValidComponent(ctx.donutChart);
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
it('should have svg as root element', () => {
|
|
1162
|
+
const rootTag = getRootTag(ctx.donutChart);
|
|
1163
|
+
expect(rootTag).toBe('svg');
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
it('should have role="img" on SVG for accessibility', () => {
|
|
1167
|
+
expect(hasRole(ctx.donutChart.view, 'img')).toBe(true);
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
it('should have className using StyleExpr with chartSvg preset', () => {
|
|
1171
|
+
const className = findPropInView(ctx.donutChart.view, 'className');
|
|
1172
|
+
expect(className).not.toBeNull();
|
|
1173
|
+
expect(className).toMatchObject({
|
|
1174
|
+
expr: 'style',
|
|
1175
|
+
preset: 'chartSvg',
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
it('should contain path elements for donut slices', () => {
|
|
1180
|
+
expect(hasElementTag(ctx.donutChart.view, 'path')).toBe(true);
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
it('should contain text element for center label when centerLabel is provided', () => {
|
|
1184
|
+
expect(hasElementTag(ctx.donutChart.view, 'text')).toBe(true);
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
// ==================== Params Validation Tests ====================
|
|
1189
|
+
|
|
1190
|
+
describe('Params Validation', () => {
|
|
1191
|
+
describe('required params', () => {
|
|
1192
|
+
it('should have data param as required with type list', () => {
|
|
1193
|
+
expect(isRequiredParam(ctx.donutChart, 'data')).toBe(true);
|
|
1194
|
+
expect(hasParamType(ctx.donutChart, 'data', 'list')).toBe(true);
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
it('should have valueKey param as required with type string', () => {
|
|
1198
|
+
expect(isRequiredParam(ctx.donutChart, 'valueKey')).toBe(true);
|
|
1199
|
+
expect(hasParamType(ctx.donutChart, 'valueKey', 'string')).toBe(true);
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
describe('optional params', () => {
|
|
1204
|
+
const optionalParams = [
|
|
1205
|
+
{ name: 'labelKey', type: 'string' },
|
|
1206
|
+
{ name: 'width', type: 'number' },
|
|
1207
|
+
{ name: 'height', type: 'number' },
|
|
1208
|
+
{ name: 'colors', type: 'list' },
|
|
1209
|
+
{ name: 'showLabels', type: 'boolean' },
|
|
1210
|
+
{ name: 'showPercentage', type: 'boolean' },
|
|
1211
|
+
{ name: 'innerRadius', type: 'number' },
|
|
1212
|
+
{ name: 'centerLabel', type: 'string' },
|
|
1213
|
+
];
|
|
1214
|
+
|
|
1215
|
+
it.each(optionalParams)(
|
|
1216
|
+
'should have $name param as optional with type $type',
|
|
1217
|
+
({ name, type }) => {
|
|
1218
|
+
expect(isOptionalParam(ctx.donutChart, name)).toBe(true);
|
|
1219
|
+
expect(hasParamType(ctx.donutChart, name, type as 'string' | 'number' | 'boolean' | 'list')).toBe(true);
|
|
1220
|
+
}
|
|
1221
|
+
);
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it('should have all expected params', () => {
|
|
1225
|
+
const expectedParams = [
|
|
1226
|
+
'data',
|
|
1227
|
+
'valueKey',
|
|
1228
|
+
'labelKey',
|
|
1229
|
+
'width',
|
|
1230
|
+
'height',
|
|
1231
|
+
'colors',
|
|
1232
|
+
'showLabels',
|
|
1233
|
+
'showPercentage',
|
|
1234
|
+
'innerRadius',
|
|
1235
|
+
'centerLabel',
|
|
1236
|
+
];
|
|
1237
|
+
expect(hasParams(ctx.donutChart, expectedParams)).toBe(true);
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// ==================== Accessibility Tests ====================
|
|
1242
|
+
|
|
1243
|
+
describe('Accessibility', () => {
|
|
1244
|
+
it('should have role="img" on SVG for screen readers', () => {
|
|
1245
|
+
const role = findPropInView(ctx.donutChart.view, 'role');
|
|
1246
|
+
expect(role).not.toBeNull();
|
|
1247
|
+
expect(role).toMatchObject({
|
|
1248
|
+
expr: 'lit',
|
|
1249
|
+
value: 'img',
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
it('should have aria-label attribute', () => {
|
|
1254
|
+
expect(hasAriaAttribute(ctx.donutChart.view, 'label')).toBe(true);
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
it('should have title element for chart description', () => {
|
|
1258
|
+
expect(hasElementTag(ctx.donutChart.view, 'title')).toBe(true);
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
it('should have aria-labelledby referencing title element', () => {
|
|
1262
|
+
expect(hasAriaAttribute(ctx.donutChart.view, 'labelledby')).toBe(true);
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
// ==================== Rendering Tests ====================
|
|
1267
|
+
|
|
1268
|
+
describe('Rendering', () => {
|
|
1269
|
+
it('should use getDonutSlices helper for slice calculation', () => {
|
|
1270
|
+
const call = findExpressionCall(ctx.donutChart.view, 'getDonutSlices');
|
|
1271
|
+
expect(call).not.toBeNull();
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
it('should use getArcPath helper for path generation', () => {
|
|
1275
|
+
const call = findExpressionCall(ctx.donutChart.view, 'getArcPath');
|
|
1276
|
+
expect(call).not.toBeNull();
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
it('should apply chartSlice style preset to slice elements', () => {
|
|
1280
|
+
expect(usesStylePreset(ctx.donutChart.view, 'chartSlice')).toBe(true);
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
it('should render labels when showLabels is true', () => {
|
|
1284
|
+
expect(usesStylePreset(ctx.donutChart.view, 'chartLabel')).toBe(true);
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
it('should support percentage display when showPercentage is true', () => {
|
|
1288
|
+
expect(hasParams(ctx.donutChart, ['showPercentage'])).toBe(true);
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
it('should support configurable inner radius', () => {
|
|
1292
|
+
expect(hasParams(ctx.donutChart, ['innerRadius'])).toBe(true);
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
it('should support center label display', () => {
|
|
1296
|
+
expect(hasParams(ctx.donutChart, ['centerLabel'])).toBe(true);
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
// ==================== Style Preset Usage Tests ====================
|
|
1301
|
+
|
|
1302
|
+
describe('Style Preset Usage', () => {
|
|
1303
|
+
it('should use chartSvg preset for root SVG', () => {
|
|
1304
|
+
const className = findPropInView(ctx.donutChart.view, 'className');
|
|
1305
|
+
expect(className).toMatchObject({
|
|
1306
|
+
expr: 'style',
|
|
1307
|
+
preset: 'chartSvg',
|
|
1308
|
+
});
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
it('should use chartSlice preset for donut slices', () => {
|
|
1312
|
+
expect(usesStylePreset(ctx.donutChart.view, 'chartSlice')).toBe(true);
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
it('should use chartLabel preset for center label', () => {
|
|
1316
|
+
expect(usesStylePreset(ctx.donutChart.view, 'chartLabel')).toBe(true);
|
|
1317
|
+
});
|
|
1318
|
+
});
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
// ============================================================
|
|
1322
|
+
// SCATTER CHART COMPONENT
|
|
1323
|
+
// ============================================================
|
|
1324
|
+
|
|
1325
|
+
describe('ScatterChart Component', () => {
|
|
1326
|
+
// ==================== Component Structure Tests ====================
|
|
1327
|
+
|
|
1328
|
+
describe('Component Structure', () => {
|
|
1329
|
+
it('should have valid component structure', () => {
|
|
1330
|
+
assertValidComponent(ctx.scatterChart);
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
it('should have svg as root element', () => {
|
|
1334
|
+
const rootTag = getRootTag(ctx.scatterChart);
|
|
1335
|
+
expect(rootTag).toBe('svg');
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
it('should have role="img" on SVG for accessibility', () => {
|
|
1339
|
+
expect(hasRole(ctx.scatterChart.view, 'img')).toBe(true);
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
it('should have className using StyleExpr with chartSvg preset', () => {
|
|
1343
|
+
const className = findPropInView(ctx.scatterChart.view, 'className');
|
|
1344
|
+
expect(className).not.toBeNull();
|
|
1345
|
+
expect(className).toMatchObject({
|
|
1346
|
+
expr: 'style',
|
|
1347
|
+
preset: 'chartSvg',
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
it('should contain circle elements for data points', () => {
|
|
1352
|
+
expect(hasElementTag(ctx.scatterChart.view, 'circle')).toBe(true);
|
|
1353
|
+
});
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
// ==================== Params Validation Tests ====================
|
|
1357
|
+
|
|
1358
|
+
describe('Params Validation', () => {
|
|
1359
|
+
describe('required params', () => {
|
|
1360
|
+
it('should have data param as required with type list', () => {
|
|
1361
|
+
expect(isRequiredParam(ctx.scatterChart, 'data')).toBe(true);
|
|
1362
|
+
expect(hasParamType(ctx.scatterChart, 'data', 'list')).toBe(true);
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
it('should have xKey param as required with type string', () => {
|
|
1366
|
+
expect(isRequiredParam(ctx.scatterChart, 'xKey')).toBe(true);
|
|
1367
|
+
expect(hasParamType(ctx.scatterChart, 'xKey', 'string')).toBe(true);
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
it('should have yKey param as required with type string', () => {
|
|
1371
|
+
expect(isRequiredParam(ctx.scatterChart, 'yKey')).toBe(true);
|
|
1372
|
+
expect(hasParamType(ctx.scatterChart, 'yKey', 'string')).toBe(true);
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
describe('optional params', () => {
|
|
1377
|
+
const optionalParams = [
|
|
1378
|
+
{ name: 'width', type: 'number' },
|
|
1379
|
+
{ name: 'height', type: 'number' },
|
|
1380
|
+
{ name: 'colors', type: 'list' },
|
|
1381
|
+
{ name: 'sizeKey', type: 'string' },
|
|
1382
|
+
{ name: 'colorKey', type: 'string' },
|
|
1383
|
+
{ name: 'pointRadius', type: 'number' },
|
|
1384
|
+
{ name: 'showGrid', type: 'boolean' },
|
|
1385
|
+
{ name: 'showLabels', type: 'boolean' },
|
|
1386
|
+
];
|
|
1387
|
+
|
|
1388
|
+
it.each(optionalParams)(
|
|
1389
|
+
'should have $name param as optional with type $type',
|
|
1390
|
+
({ name, type }) => {
|
|
1391
|
+
expect(isOptionalParam(ctx.scatterChart, name)).toBe(true);
|
|
1392
|
+
expect(hasParamType(ctx.scatterChart, name, type as 'string' | 'number' | 'boolean' | 'list')).toBe(true);
|
|
1393
|
+
}
|
|
1394
|
+
);
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
it('should have all expected params', () => {
|
|
1398
|
+
const expectedParams = [
|
|
1399
|
+
'data',
|
|
1400
|
+
'xKey',
|
|
1401
|
+
'yKey',
|
|
1402
|
+
'width',
|
|
1403
|
+
'height',
|
|
1404
|
+
'colors',
|
|
1405
|
+
'sizeKey',
|
|
1406
|
+
'colorKey',
|
|
1407
|
+
'pointRadius',
|
|
1408
|
+
'showGrid',
|
|
1409
|
+
'showLabels',
|
|
1410
|
+
];
|
|
1411
|
+
expect(hasParams(ctx.scatterChart, expectedParams)).toBe(true);
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
// ==================== Accessibility Tests ====================
|
|
1416
|
+
|
|
1417
|
+
describe('Accessibility', () => {
|
|
1418
|
+
it('should have role="img" on SVG for screen readers', () => {
|
|
1419
|
+
const role = findPropInView(ctx.scatterChart.view, 'role');
|
|
1420
|
+
expect(role).not.toBeNull();
|
|
1421
|
+
expect(role).toMatchObject({
|
|
1422
|
+
expr: 'lit',
|
|
1423
|
+
value: 'img',
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
it('should have aria-label attribute', () => {
|
|
1428
|
+
expect(hasAriaAttribute(ctx.scatterChart.view, 'label')).toBe(true);
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
it('should have title element for chart description', () => {
|
|
1432
|
+
expect(hasElementTag(ctx.scatterChart.view, 'title')).toBe(true);
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
it('should have aria-labelledby referencing title element', () => {
|
|
1436
|
+
expect(hasAriaAttribute(ctx.scatterChart.view, 'labelledby')).toBe(true);
|
|
1437
|
+
});
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
// ==================== Rendering Tests ====================
|
|
1441
|
+
|
|
1442
|
+
describe('Rendering', () => {
|
|
1443
|
+
it('should apply chartPoint style preset to point circles', () => {
|
|
1444
|
+
expect(usesStylePreset(ctx.scatterChart.view, 'chartPoint')).toBe(true);
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
it('should render grid when showGrid is true', () => {
|
|
1448
|
+
expect(usesStylePreset(ctx.scatterChart.view, 'chartGrid')).toBe(true);
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
it('should render labels when showLabels is true', () => {
|
|
1452
|
+
expect(usesStylePreset(ctx.scatterChart.view, 'chartLabel')).toBe(true);
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
it('should support variable point sizes with sizeKey', () => {
|
|
1456
|
+
expect(hasParams(ctx.scatterChart, ['sizeKey'])).toBe(true);
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
it('should support variable point colors with colorKey', () => {
|
|
1460
|
+
expect(hasParams(ctx.scatterChart, ['colorKey'])).toBe(true);
|
|
1461
|
+
});
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
// ==================== Style Preset Usage Tests ====================
|
|
1465
|
+
|
|
1466
|
+
describe('Style Preset Usage', () => {
|
|
1467
|
+
it('should use chartSvg preset for root SVG', () => {
|
|
1468
|
+
const className = findPropInView(ctx.scatterChart.view, 'className');
|
|
1469
|
+
expect(className).toMatchObject({
|
|
1470
|
+
expr: 'style',
|
|
1471
|
+
preset: 'chartSvg',
|
|
1472
|
+
});
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
it('should use chartPoint preset for data points', () => {
|
|
1476
|
+
expect(usesStylePreset(ctx.scatterChart.view, 'chartPoint')).toBe(true);
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
// ============================================================
|
|
1482
|
+
// RADAR CHART COMPONENT
|
|
1483
|
+
// ============================================================
|
|
1484
|
+
|
|
1485
|
+
describe('RadarChart Component', () => {
|
|
1486
|
+
// ==================== Component Structure Tests ====================
|
|
1487
|
+
|
|
1488
|
+
describe('Component Structure', () => {
|
|
1489
|
+
it('should have valid component structure', () => {
|
|
1490
|
+
assertValidComponent(ctx.radarChart);
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
it('should have svg as root element', () => {
|
|
1494
|
+
const rootTag = getRootTag(ctx.radarChart);
|
|
1495
|
+
expect(rootTag).toBe('svg');
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
it('should have role="img" on SVG for accessibility', () => {
|
|
1499
|
+
expect(hasRole(ctx.radarChart.view, 'img')).toBe(true);
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
it('should have className using StyleExpr with chartSvg preset', () => {
|
|
1503
|
+
const className = findPropInView(ctx.radarChart.view, 'className');
|
|
1504
|
+
expect(className).not.toBeNull();
|
|
1505
|
+
expect(className).toMatchObject({
|
|
1506
|
+
expr: 'style',
|
|
1507
|
+
preset: 'chartSvg',
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
it('should contain polygon element for radar data', () => {
|
|
1512
|
+
expect(hasElementTag(ctx.radarChart.view, 'polygon')).toBe(true);
|
|
1513
|
+
});
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
// ==================== Params Validation Tests ====================
|
|
1517
|
+
|
|
1518
|
+
describe('Params Validation', () => {
|
|
1519
|
+
describe('required params', () => {
|
|
1520
|
+
it('should have data param as required with type list', () => {
|
|
1521
|
+
expect(isRequiredParam(ctx.radarChart, 'data')).toBe(true);
|
|
1522
|
+
expect(hasParamType(ctx.radarChart, 'data', 'list')).toBe(true);
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
it('should have valueKey param as required with type string', () => {
|
|
1526
|
+
expect(isRequiredParam(ctx.radarChart, 'valueKey')).toBe(true);
|
|
1527
|
+
expect(hasParamType(ctx.radarChart, 'valueKey', 'string')).toBe(true);
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
it('should have labelKey param as required with type string', () => {
|
|
1531
|
+
expect(isRequiredParam(ctx.radarChart, 'labelKey')).toBe(true);
|
|
1532
|
+
expect(hasParamType(ctx.radarChart, 'labelKey', 'string')).toBe(true);
|
|
1533
|
+
});
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
describe('optional params', () => {
|
|
1537
|
+
const optionalParams = [
|
|
1538
|
+
{ name: 'width', type: 'number' },
|
|
1539
|
+
{ name: 'height', type: 'number' },
|
|
1540
|
+
{ name: 'colors', type: 'list' },
|
|
1541
|
+
{ name: 'maxValue', type: 'number' },
|
|
1542
|
+
{ name: 'showGrid', type: 'boolean' },
|
|
1543
|
+
{ name: 'fillOpacity', type: 'number' },
|
|
1544
|
+
];
|
|
1545
|
+
|
|
1546
|
+
it.each(optionalParams)(
|
|
1547
|
+
'should have $name param as optional with type $type',
|
|
1548
|
+
({ name, type }) => {
|
|
1549
|
+
expect(isOptionalParam(ctx.radarChart, name)).toBe(true);
|
|
1550
|
+
expect(hasParamType(ctx.radarChart, name, type as 'string' | 'number' | 'boolean' | 'list')).toBe(true);
|
|
1551
|
+
}
|
|
1552
|
+
);
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
it('should have all expected params', () => {
|
|
1556
|
+
const expectedParams = [
|
|
1557
|
+
'data',
|
|
1558
|
+
'valueKey',
|
|
1559
|
+
'labelKey',
|
|
1560
|
+
'width',
|
|
1561
|
+
'height',
|
|
1562
|
+
'colors',
|
|
1563
|
+
'maxValue',
|
|
1564
|
+
'showGrid',
|
|
1565
|
+
'fillOpacity',
|
|
1566
|
+
];
|
|
1567
|
+
expect(hasParams(ctx.radarChart, expectedParams)).toBe(true);
|
|
1568
|
+
});
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
// ==================== Accessibility Tests ====================
|
|
1572
|
+
|
|
1573
|
+
describe('Accessibility', () => {
|
|
1574
|
+
it('should have role="img" on SVG for screen readers', () => {
|
|
1575
|
+
const role = findPropInView(ctx.radarChart.view, 'role');
|
|
1576
|
+
expect(role).not.toBeNull();
|
|
1577
|
+
expect(role).toMatchObject({
|
|
1578
|
+
expr: 'lit',
|
|
1579
|
+
value: 'img',
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
it('should have aria-label attribute', () => {
|
|
1584
|
+
expect(hasAriaAttribute(ctx.radarChart.view, 'label')).toBe(true);
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
it('should have title element for chart description', () => {
|
|
1588
|
+
expect(hasElementTag(ctx.radarChart.view, 'title')).toBe(true);
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
it('should have aria-labelledby referencing title element', () => {
|
|
1592
|
+
expect(hasAriaAttribute(ctx.radarChart.view, 'labelledby')).toBe(true);
|
|
1593
|
+
});
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
// ==================== Rendering Tests ====================
|
|
1597
|
+
|
|
1598
|
+
describe('Rendering', () => {
|
|
1599
|
+
it('should use getRadarPoints helper for polygon calculation', () => {
|
|
1600
|
+
const call = findExpressionCall(ctx.radarChart.view, 'getRadarPoints');
|
|
1601
|
+
expect(call).not.toBeNull();
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
it('should use getRadarAxes helper for axis lines', () => {
|
|
1605
|
+
const call = findExpressionCall(ctx.radarChart.view, 'getRadarAxes');
|
|
1606
|
+
expect(call).not.toBeNull();
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
it('should apply chartRadar style preset to polygon element', () => {
|
|
1610
|
+
expect(usesStylePreset(ctx.radarChart.view, 'chartRadar')).toBe(true);
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
it('should render grid when showGrid is true', () => {
|
|
1614
|
+
expect(usesStylePreset(ctx.radarChart.view, 'chartGrid')).toBe(true);
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
it('should support configurable max value', () => {
|
|
1618
|
+
expect(hasParams(ctx.radarChart, ['maxValue'])).toBe(true);
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
it('should support configurable fill opacity', () => {
|
|
1622
|
+
expect(hasParams(ctx.radarChart, ['fillOpacity'])).toBe(true);
|
|
1623
|
+
});
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
// ==================== Style Preset Usage Tests ====================
|
|
1627
|
+
|
|
1628
|
+
describe('Style Preset Usage', () => {
|
|
1629
|
+
it('should use chartSvg preset for root SVG', () => {
|
|
1630
|
+
const className = findPropInView(ctx.radarChart.view, 'className');
|
|
1631
|
+
expect(className).toMatchObject({
|
|
1632
|
+
expr: 'style',
|
|
1633
|
+
preset: 'chartSvg',
|
|
1634
|
+
});
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
it('should use chartRadar preset for radar polygon', () => {
|
|
1638
|
+
expect(usesStylePreset(ctx.radarChart.view, 'chartRadar')).toBe(true);
|
|
1639
|
+
});
|
|
1640
|
+
});
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
// ============================================================
|
|
1644
|
+
// CHART LEGEND COMPONENT
|
|
1645
|
+
// ============================================================
|
|
1646
|
+
|
|
1647
|
+
describe('ChartLegend Component', () => {
|
|
1648
|
+
// ==================== Component Structure Tests ====================
|
|
1649
|
+
|
|
1650
|
+
describe('Component Structure', () => {
|
|
1651
|
+
it('should have valid component structure', () => {
|
|
1652
|
+
assertValidComponent(ctx.chartLegend);
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
it('should have div as root element', () => {
|
|
1656
|
+
const rootTag = getRootTag(ctx.chartLegend);
|
|
1657
|
+
expect(rootTag).toBe('div');
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
it('should have role="list" on root element for accessibility', () => {
|
|
1661
|
+
expect(hasRole(ctx.chartLegend.view, 'list')).toBe(true);
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
it('should have className using StyleExpr with chartLegend preset', () => {
|
|
1665
|
+
const className = findPropInView(ctx.chartLegend.view, 'className');
|
|
1666
|
+
expect(className).not.toBeNull();
|
|
1667
|
+
expect(className).toMatchObject({
|
|
1668
|
+
expr: 'style',
|
|
1669
|
+
preset: 'chartLegend',
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
it('should contain list items for each legend entry', () => {
|
|
1674
|
+
// Legend should iterate over items and render list items
|
|
1675
|
+
expect(hasElementTag(ctx.chartLegend.view, 'div')).toBe(true);
|
|
1676
|
+
});
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
// ==================== Params Validation Tests ====================
|
|
1680
|
+
|
|
1681
|
+
describe('Params Validation', () => {
|
|
1682
|
+
describe('required params', () => {
|
|
1683
|
+
it('should have items param as required with type list', () => {
|
|
1684
|
+
expect(isRequiredParam(ctx.chartLegend, 'items')).toBe(true);
|
|
1685
|
+
expect(hasParamType(ctx.chartLegend, 'items', 'list')).toBe(true);
|
|
1686
|
+
});
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
describe('optional params', () => {
|
|
1690
|
+
const optionalParams = [
|
|
1691
|
+
{ name: 'orientation', type: 'string' },
|
|
1692
|
+
{ name: 'position', type: 'string' },
|
|
1693
|
+
];
|
|
1694
|
+
|
|
1695
|
+
it.each(optionalParams)(
|
|
1696
|
+
'should have $name param as optional with type $type',
|
|
1697
|
+
({ name, type }) => {
|
|
1698
|
+
expect(isOptionalParam(ctx.chartLegend, name)).toBe(true);
|
|
1699
|
+
expect(hasParamType(ctx.chartLegend, name, type as 'string' | 'number' | 'boolean' | 'list')).toBe(true);
|
|
1700
|
+
}
|
|
1701
|
+
);
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
it('should have all expected params', () => {
|
|
1705
|
+
const expectedParams = ['items', 'orientation', 'position'];
|
|
1706
|
+
expect(hasParams(ctx.chartLegend, expectedParams)).toBe(true);
|
|
1707
|
+
});
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
// ==================== Accessibility Tests ====================
|
|
1711
|
+
|
|
1712
|
+
describe('Accessibility', () => {
|
|
1713
|
+
it('should have role="list" on root element', () => {
|
|
1714
|
+
const role = findPropInView(ctx.chartLegend.view, 'role');
|
|
1715
|
+
expect(role).not.toBeNull();
|
|
1716
|
+
expect(role).toMatchObject({
|
|
1717
|
+
expr: 'lit',
|
|
1718
|
+
value: 'list',
|
|
1719
|
+
});
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
it('should have role="listitem" on legend items', () => {
|
|
1723
|
+
// Each legend item should have role="listitem"
|
|
1724
|
+
expect(hasRoleAnywhere(ctx.chartLegend.view, 'listitem')).toBe(true);
|
|
1725
|
+
});
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
// ==================== Rendering Tests ====================
|
|
1729
|
+
|
|
1730
|
+
describe('Rendering', () => {
|
|
1731
|
+
it('should apply chartLegend style preset to container', () => {
|
|
1732
|
+
expect(usesStylePreset(ctx.chartLegend.view, 'chartLegend')).toBe(true);
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
it('should apply chartLegendItem style preset to legend items', () => {
|
|
1736
|
+
expect(usesStylePreset(ctx.chartLegend.view, 'chartLegendItem')).toBe(true);
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
it('should render color indicators for each item', () => {
|
|
1740
|
+
// Each legend item should have a color indicator element
|
|
1741
|
+
expect(hasElementTag(ctx.chartLegend.view, 'span')).toBe(true);
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
it('should render labels for each item', () => {
|
|
1745
|
+
// Each legend item should have a label
|
|
1746
|
+
expect(hasElementTag(ctx.chartLegend.view, 'span')).toBe(true);
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
it('should support horizontal orientation', () => {
|
|
1750
|
+
expect(hasParams(ctx.chartLegend, ['orientation'])).toBe(true);
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
it('should support vertical orientation', () => {
|
|
1754
|
+
expect(hasParams(ctx.chartLegend, ['orientation'])).toBe(true);
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
it('should support configurable position', () => {
|
|
1758
|
+
expect(hasParams(ctx.chartLegend, ['position'])).toBe(true);
|
|
1759
|
+
});
|
|
1760
|
+
});
|
|
1761
|
+
|
|
1762
|
+
// ==================== Style Preset Usage Tests ====================
|
|
1763
|
+
|
|
1764
|
+
describe('Style Preset Usage', () => {
|
|
1765
|
+
it('should use chartLegend preset for container', () => {
|
|
1766
|
+
const className = findPropInView(ctx.chartLegend.view, 'className');
|
|
1767
|
+
expect(className).toMatchObject({
|
|
1768
|
+
expr: 'style',
|
|
1769
|
+
preset: 'chartLegend',
|
|
1770
|
+
});
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
it('should use chartLegendItem preset for legend items', () => {
|
|
1774
|
+
expect(usesStylePreset(ctx.chartLegend.view, 'chartLegendItem')).toBe(true);
|
|
1775
|
+
});
|
|
1776
|
+
});
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
// ============================================================
|
|
1780
|
+
// CHART TOOLTIP COMPONENT
|
|
1781
|
+
// ============================================================
|
|
1782
|
+
|
|
1783
|
+
describe('ChartTooltip Component', () => {
|
|
1784
|
+
// ==================== Component Structure Tests ====================
|
|
1785
|
+
|
|
1786
|
+
describe('Component Structure', () => {
|
|
1787
|
+
it('should have valid component structure', () => {
|
|
1788
|
+
assertValidComponent(ctx.chartTooltip);
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
it('should have div as root element', () => {
|
|
1792
|
+
const rootTag = getRootTag(ctx.chartTooltip);
|
|
1793
|
+
expect(rootTag).toBe('div');
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
it('should have role="tooltip" for accessibility', () => {
|
|
1797
|
+
expect(hasRole(ctx.chartTooltip.view, 'tooltip')).toBe(true);
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
it('should have className using StyleExpr with chartTooltip preset', () => {
|
|
1801
|
+
const className = findPropInView(ctx.chartTooltip.view, 'className');
|
|
1802
|
+
expect(className).not.toBeNull();
|
|
1803
|
+
expect(className).toMatchObject({
|
|
1804
|
+
expr: 'style',
|
|
1805
|
+
preset: 'chartTooltip',
|
|
1806
|
+
});
|
|
1807
|
+
});
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
// ==================== Params Validation Tests ====================
|
|
1811
|
+
|
|
1812
|
+
describe('Params Validation', () => {
|
|
1813
|
+
describe('required params', () => {
|
|
1814
|
+
it('should have content param as required with type string', () => {
|
|
1815
|
+
expect(isRequiredParam(ctx.chartTooltip, 'content')).toBe(true);
|
|
1816
|
+
expect(hasParamType(ctx.chartTooltip, 'content', 'string')).toBe(true);
|
|
1817
|
+
});
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
describe('optional params', () => {
|
|
1821
|
+
const optionalParams = [
|
|
1822
|
+
{ name: 'x', type: 'number' },
|
|
1823
|
+
{ name: 'y', type: 'number' },
|
|
1824
|
+
{ name: 'visible', type: 'boolean' },
|
|
1825
|
+
];
|
|
1826
|
+
|
|
1827
|
+
it.each(optionalParams)(
|
|
1828
|
+
'should have $name param as optional with type $type',
|
|
1829
|
+
({ name, type }) => {
|
|
1830
|
+
expect(isOptionalParam(ctx.chartTooltip, name)).toBe(true);
|
|
1831
|
+
expect(hasParamType(ctx.chartTooltip, name, type as 'string' | 'number' | 'boolean' | 'list')).toBe(true);
|
|
1832
|
+
}
|
|
1833
|
+
);
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
it('should have all expected params', () => {
|
|
1837
|
+
const expectedParams = ['content', 'x', 'y', 'visible'];
|
|
1838
|
+
expect(hasParams(ctx.chartTooltip, expectedParams)).toBe(true);
|
|
1839
|
+
});
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
// ==================== Accessibility Tests ====================
|
|
1843
|
+
|
|
1844
|
+
describe('Accessibility', () => {
|
|
1845
|
+
it('should have role="tooltip" on root element', () => {
|
|
1846
|
+
const role = findPropInView(ctx.chartTooltip.view, 'role');
|
|
1847
|
+
expect(role).not.toBeNull();
|
|
1848
|
+
expect(role).toMatchObject({
|
|
1849
|
+
expr: 'lit',
|
|
1850
|
+
value: 'tooltip',
|
|
1851
|
+
});
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
it('should have aria-hidden attribute based on visibility', () => {
|
|
1855
|
+
expect(hasAriaAttribute(ctx.chartTooltip.view, 'hidden')).toBe(true);
|
|
1856
|
+
});
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
// ==================== Rendering Tests ====================
|
|
1860
|
+
|
|
1861
|
+
describe('Rendering', () => {
|
|
1862
|
+
it('should apply chartTooltip style preset', () => {
|
|
1863
|
+
expect(usesStylePreset(ctx.chartTooltip.view, 'chartTooltip')).toBe(true);
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
it('should have absolute positioning', () => {
|
|
1867
|
+
// Tooltip should be positioned absolutely using x and y
|
|
1868
|
+
const className = findPropInView(ctx.chartTooltip.view, 'className');
|
|
1869
|
+
expect(className).not.toBeNull();
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
it('should render content from content param', () => {
|
|
1873
|
+
// Content should be rendered inside the tooltip
|
|
1874
|
+
expect(hasParams(ctx.chartTooltip, ['content'])).toBe(true);
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
it('should support visibility toggle', () => {
|
|
1878
|
+
expect(hasParams(ctx.chartTooltip, ['visible'])).toBe(true);
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
it('should support x position', () => {
|
|
1882
|
+
expect(hasParams(ctx.chartTooltip, ['x'])).toBe(true);
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
it('should support y position', () => {
|
|
1886
|
+
expect(hasParams(ctx.chartTooltip, ['y'])).toBe(true);
|
|
1887
|
+
});
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
// ==================== Style Preset Usage Tests ====================
|
|
1891
|
+
|
|
1892
|
+
describe('Style Preset Usage', () => {
|
|
1893
|
+
it('should use chartTooltip preset for tooltip container', () => {
|
|
1894
|
+
const className = findPropInView(ctx.chartTooltip.view, 'className');
|
|
1895
|
+
expect(className).toMatchObject({
|
|
1896
|
+
expr: 'style',
|
|
1897
|
+
preset: 'chartTooltip',
|
|
1898
|
+
});
|
|
1899
|
+
});
|
|
1900
|
+
});
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
// ============================================================
|
|
1904
|
+
// CHART AXIS COMPONENT
|
|
1905
|
+
// ============================================================
|
|
1906
|
+
|
|
1907
|
+
describe('ChartAxis Component', () => {
|
|
1908
|
+
// ==================== Component Structure Tests ====================
|
|
1909
|
+
|
|
1910
|
+
describe('Component Structure', () => {
|
|
1911
|
+
it('should have valid component structure', () => {
|
|
1912
|
+
assertValidComponent(ctx.chartAxis);
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
it('should have g (SVG group) as root element', () => {
|
|
1916
|
+
const rootTag = getRootTag(ctx.chartAxis);
|
|
1917
|
+
expect(rootTag).toBe('g');
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
it('should have className using StyleExpr with chartAxis preset', () => {
|
|
1921
|
+
const className = findPropInView(ctx.chartAxis.view, 'className');
|
|
1922
|
+
expect(className).not.toBeNull();
|
|
1923
|
+
expect(className).toMatchObject({
|
|
1924
|
+
expr: 'style',
|
|
1925
|
+
preset: 'chartAxis',
|
|
1926
|
+
});
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
it('should contain line elements for axis line', () => {
|
|
1930
|
+
expect(hasElementTag(ctx.chartAxis.view, 'line')).toBe(true);
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
it('should contain text elements for tick labels when showLabels is true', () => {
|
|
1934
|
+
expect(hasElementTag(ctx.chartAxis.view, 'text')).toBe(true);
|
|
1935
|
+
});
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
// ==================== Params Validation Tests ====================
|
|
1939
|
+
|
|
1940
|
+
describe('Params Validation', () => {
|
|
1941
|
+
describe('required params', () => {
|
|
1942
|
+
it('should have ticks param as required with type list', () => {
|
|
1943
|
+
expect(isRequiredParam(ctx.chartAxis, 'ticks')).toBe(true);
|
|
1944
|
+
expect(hasParamType(ctx.chartAxis, 'ticks', 'list')).toBe(true);
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
it('should have orientation param as required with type string', () => {
|
|
1948
|
+
expect(isRequiredParam(ctx.chartAxis, 'orientation')).toBe(true);
|
|
1949
|
+
expect(hasParamType(ctx.chartAxis, 'orientation', 'string')).toBe(true);
|
|
1950
|
+
});
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
describe('optional params', () => {
|
|
1954
|
+
const optionalParams = [
|
|
1955
|
+
{ name: 'width', type: 'number' },
|
|
1956
|
+
{ name: 'height', type: 'number' },
|
|
1957
|
+
{ name: 'showLabels', type: 'boolean' },
|
|
1958
|
+
{ name: 'labelFormatter', type: 'string' },
|
|
1959
|
+
];
|
|
1960
|
+
|
|
1961
|
+
it.each(optionalParams)(
|
|
1962
|
+
'should have $name param as optional with type $type',
|
|
1963
|
+
({ name, type }) => {
|
|
1964
|
+
expect(isOptionalParam(ctx.chartAxis, name)).toBe(true);
|
|
1965
|
+
expect(hasParamType(ctx.chartAxis, name, type as 'string' | 'number' | 'boolean' | 'list')).toBe(true);
|
|
1966
|
+
}
|
|
1967
|
+
);
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
it('should have all expected params', () => {
|
|
1971
|
+
const expectedParams = ['ticks', 'orientation', 'width', 'height', 'showLabels', 'labelFormatter'];
|
|
1972
|
+
expect(hasParams(ctx.chartAxis, expectedParams)).toBe(true);
|
|
1973
|
+
});
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
// ==================== Accessibility Tests ====================
|
|
1977
|
+
|
|
1978
|
+
describe('Accessibility', () => {
|
|
1979
|
+
it('should have aria-hidden="true" since axis is decorative', () => {
|
|
1980
|
+
expect(hasAriaAttribute(ctx.chartAxis.view, 'hidden')).toBe(true);
|
|
1981
|
+
});
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
// ==================== Rendering Tests ====================
|
|
1985
|
+
|
|
1986
|
+
describe('Rendering', () => {
|
|
1987
|
+
it('should use generateTicks helper for tick generation', () => {
|
|
1988
|
+
const call = findExpressionCall(ctx.chartAxis.view, 'generateTicks');
|
|
1989
|
+
expect(call).not.toBeNull();
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
it('should apply chartAxis style preset to group element', () => {
|
|
1993
|
+
expect(usesStylePreset(ctx.chartAxis.view, 'chartAxis')).toBe(true);
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
it('should apply chartLabel style preset to tick labels', () => {
|
|
1997
|
+
expect(usesStylePreset(ctx.chartAxis.view, 'chartLabel')).toBe(true);
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
it('should support horizontal orientation', () => {
|
|
2001
|
+
expect(hasParams(ctx.chartAxis, ['orientation'])).toBe(true);
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
it('should support vertical orientation', () => {
|
|
2005
|
+
expect(hasParams(ctx.chartAxis, ['orientation'])).toBe(true);
|
|
2006
|
+
});
|
|
2007
|
+
|
|
2008
|
+
it('should render tick marks for each tick', () => {
|
|
2009
|
+
expect(hasElementTag(ctx.chartAxis.view, 'line')).toBe(true);
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
it('should render labels when showLabels is true', () => {
|
|
2013
|
+
expect(hasParams(ctx.chartAxis, ['showLabels'])).toBe(true);
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
it('should support custom label formatting', () => {
|
|
2017
|
+
expect(hasParams(ctx.chartAxis, ['labelFormatter'])).toBe(true);
|
|
2018
|
+
});
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
// ==================== Style Preset Usage Tests ====================
|
|
2022
|
+
|
|
2023
|
+
describe('Style Preset Usage', () => {
|
|
2024
|
+
it('should use chartAxis preset for axis group', () => {
|
|
2025
|
+
const className = findPropInView(ctx.chartAxis.view, 'className');
|
|
2026
|
+
expect(className).toMatchObject({
|
|
2027
|
+
expr: 'style',
|
|
2028
|
+
preset: 'chartAxis',
|
|
2029
|
+
});
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
it('should use chartLabel preset for tick labels', () => {
|
|
2033
|
+
expect(usesStylePreset(ctx.chartAxis.view, 'chartLabel')).toBe(true);
|
|
2034
|
+
});
|
|
2035
|
+
});
|
|
2036
|
+
});
|
|
2037
|
+
|
|
2038
|
+
// ============================================================
|
|
2039
|
+
// CHART STYLE PRESETS
|
|
2040
|
+
// ============================================================
|
|
2041
|
+
|
|
2042
|
+
describe('Chart Style Presets', () => {
|
|
2043
|
+
// ==================== chartContainer ====================
|
|
2044
|
+
|
|
2045
|
+
describe('Style Preset: chartContainer', () => {
|
|
2046
|
+
it('should have valid style preset structure', () => {
|
|
2047
|
+
const chartContainer = ctx.styles['chartContainer'];
|
|
2048
|
+
expect(chartContainer).toBeDefined();
|
|
2049
|
+
assertValidStylePreset(chartContainer);
|
|
2050
|
+
});
|
|
2051
|
+
|
|
2052
|
+
it('should have base classes for container layout', () => {
|
|
2053
|
+
const chartContainer = ctx.styles['chartContainer'];
|
|
2054
|
+
expect(chartContainer.base).toBeDefined();
|
|
2055
|
+
expect(typeof chartContainer.base).toBe('string');
|
|
2056
|
+
expect(chartContainer.base.length).toBeGreaterThan(0);
|
|
2057
|
+
});
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
// ==================== chartSvg ====================
|
|
2061
|
+
|
|
2062
|
+
describe('Style Preset: chartSvg', () => {
|
|
2063
|
+
it('should have valid style preset structure', () => {
|
|
2064
|
+
const chartSvg = ctx.styles['chartSvg'];
|
|
2065
|
+
expect(chartSvg).toBeDefined();
|
|
2066
|
+
assertValidStylePreset(chartSvg);
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
it('should have base classes for SVG styling', () => {
|
|
2070
|
+
const chartSvg = ctx.styles['chartSvg'];
|
|
2071
|
+
expect(chartSvg.base).toBeDefined();
|
|
2072
|
+
expect(typeof chartSvg.base).toBe('string');
|
|
2073
|
+
});
|
|
2074
|
+
|
|
2075
|
+
it('should have size variants', () => {
|
|
2076
|
+
const chartSvg = ctx.styles['chartSvg'];
|
|
2077
|
+
expect(hasVariants(chartSvg, ['size'])).toBe(true);
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
it('should have sm, default, lg size options', () => {
|
|
2081
|
+
const chartSvg = ctx.styles['chartSvg'];
|
|
2082
|
+
expect(hasVariantOptions(chartSvg, 'size', ['sm', 'default', 'lg'])).toBe(true);
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
it('should have default size set to default', () => {
|
|
2086
|
+
const chartSvg = ctx.styles['chartSvg'];
|
|
2087
|
+
expect(hasDefaultVariants(chartSvg, { size: 'default' })).toBe(true);
|
|
2088
|
+
});
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
// ==================== chartBar ====================
|
|
2092
|
+
|
|
2093
|
+
describe('Style Preset: chartBar', () => {
|
|
2094
|
+
it('should have valid style preset structure', () => {
|
|
2095
|
+
const chartBar = ctx.styles['chartBar'];
|
|
2096
|
+
expect(chartBar).toBeDefined();
|
|
2097
|
+
assertValidStylePreset(chartBar);
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
it('should have base classes for bar styling', () => {
|
|
2101
|
+
const chartBar = ctx.styles['chartBar'];
|
|
2102
|
+
expect(chartBar.base).toBeDefined();
|
|
2103
|
+
expect(typeof chartBar.base).toBe('string');
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
it('should have state variants for hover and active', () => {
|
|
2107
|
+
const chartBar = ctx.styles['chartBar'];
|
|
2108
|
+
expect(hasVariants(chartBar, ['state'])).toBe(true);
|
|
2109
|
+
expect(hasVariantOptions(chartBar, 'state', ['default', 'hover', 'active'])).toBe(true);
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
it('should have default state set to default', () => {
|
|
2113
|
+
const chartBar = ctx.styles['chartBar'];
|
|
2114
|
+
expect(hasDefaultVariants(chartBar, { state: 'default' })).toBe(true);
|
|
2115
|
+
});
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
// ==================== chartLine ====================
|
|
2119
|
+
|
|
2120
|
+
describe('Style Preset: chartLine', () => {
|
|
2121
|
+
it('should have valid style preset structure', () => {
|
|
2122
|
+
const chartLine = ctx.styles['chartLine'];
|
|
2123
|
+
expect(chartLine).toBeDefined();
|
|
2124
|
+
assertValidStylePreset(chartLine);
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
it('should have base classes for line styling', () => {
|
|
2128
|
+
const chartLine = ctx.styles['chartLine'];
|
|
2129
|
+
expect(chartLine.base).toBeDefined();
|
|
2130
|
+
expect(typeof chartLine.base).toBe('string');
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
it('should include stroke-related CSS properties in base', () => {
|
|
2134
|
+
const chartLine = ctx.styles['chartLine'];
|
|
2135
|
+
// Should have classes for stroke styling
|
|
2136
|
+
expect(chartLine.base).toBeDefined();
|
|
2137
|
+
});
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
// ==================== chartArea ====================
|
|
2141
|
+
|
|
2142
|
+
describe('Style Preset: chartArea', () => {
|
|
2143
|
+
it('should have valid style preset structure', () => {
|
|
2144
|
+
const chartArea = ctx.styles['chartArea'];
|
|
2145
|
+
expect(chartArea).toBeDefined();
|
|
2146
|
+
assertValidStylePreset(chartArea);
|
|
2147
|
+
});
|
|
2148
|
+
|
|
2149
|
+
it('should have base classes for area fill styling', () => {
|
|
2150
|
+
const chartArea = ctx.styles['chartArea'];
|
|
2151
|
+
expect(chartArea.base).toBeDefined();
|
|
2152
|
+
expect(typeof chartArea.base).toBe('string');
|
|
2153
|
+
});
|
|
2154
|
+
|
|
2155
|
+
it('should have opacity variants', () => {
|
|
2156
|
+
const chartArea = ctx.styles['chartArea'];
|
|
2157
|
+
expect(hasVariants(chartArea, ['opacity'])).toBe(true);
|
|
2158
|
+
});
|
|
2159
|
+
|
|
2160
|
+
it('should have light, default, dark opacity options', () => {
|
|
2161
|
+
const chartArea = ctx.styles['chartArea'];
|
|
2162
|
+
expect(hasVariantOptions(chartArea, 'opacity', ['light', 'default', 'dark'])).toBe(true);
|
|
2163
|
+
});
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
// ==================== chartPoint ====================
|
|
2167
|
+
|
|
2168
|
+
describe('Style Preset: chartPoint', () => {
|
|
2169
|
+
it('should have valid style preset structure', () => {
|
|
2170
|
+
const chartPoint = ctx.styles['chartPoint'];
|
|
2171
|
+
expect(chartPoint).toBeDefined();
|
|
2172
|
+
assertValidStylePreset(chartPoint);
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
it('should have base classes for point styling', () => {
|
|
2176
|
+
const chartPoint = ctx.styles['chartPoint'];
|
|
2177
|
+
expect(chartPoint.base).toBeDefined();
|
|
2178
|
+
expect(typeof chartPoint.base).toBe('string');
|
|
2179
|
+
});
|
|
2180
|
+
|
|
2181
|
+
it('should have size variants', () => {
|
|
2182
|
+
const chartPoint = ctx.styles['chartPoint'];
|
|
2183
|
+
expect(hasVariants(chartPoint, ['size'])).toBe(true);
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2186
|
+
it('should have sm, default, lg size options', () => {
|
|
2187
|
+
const chartPoint = ctx.styles['chartPoint'];
|
|
2188
|
+
expect(hasVariantOptions(chartPoint, 'size', ['sm', 'default', 'lg'])).toBe(true);
|
|
2189
|
+
});
|
|
2190
|
+
|
|
2191
|
+
it('should have state variants for hover', () => {
|
|
2192
|
+
const chartPoint = ctx.styles['chartPoint'];
|
|
2193
|
+
expect(hasVariants(chartPoint, ['state'])).toBe(true);
|
|
2194
|
+
expect(hasVariantOptions(chartPoint, 'state', ['default', 'hover'])).toBe(true);
|
|
2195
|
+
});
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
// ==================== chartGrid ====================
|
|
2199
|
+
|
|
2200
|
+
describe('Style Preset: chartGrid', () => {
|
|
2201
|
+
it('should have valid style preset structure', () => {
|
|
2202
|
+
const chartGrid = ctx.styles['chartGrid'];
|
|
2203
|
+
expect(chartGrid).toBeDefined();
|
|
2204
|
+
assertValidStylePreset(chartGrid);
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
it('should have base classes for grid line styling', () => {
|
|
2208
|
+
const chartGrid = ctx.styles['chartGrid'];
|
|
2209
|
+
expect(chartGrid.base).toBeDefined();
|
|
2210
|
+
expect(typeof chartGrid.base).toBe('string');
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
it('should have axis variants for x and y', () => {
|
|
2214
|
+
const chartGrid = ctx.styles['chartGrid'];
|
|
2215
|
+
expect(hasVariants(chartGrid, ['axis'])).toBe(true);
|
|
2216
|
+
expect(hasVariantOptions(chartGrid, 'axis', ['x', 'y', 'both'])).toBe(true);
|
|
2217
|
+
});
|
|
2218
|
+
});
|
|
2219
|
+
|
|
2220
|
+
// ==================== chartAxis ====================
|
|
2221
|
+
|
|
2222
|
+
describe('Style Preset: chartAxis', () => {
|
|
2223
|
+
it('should have valid style preset structure', () => {
|
|
2224
|
+
const chartAxis = ctx.styles['chartAxis'];
|
|
2225
|
+
expect(chartAxis).toBeDefined();
|
|
2226
|
+
assertValidStylePreset(chartAxis);
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
it('should have base classes for axis styling', () => {
|
|
2230
|
+
const chartAxis = ctx.styles['chartAxis'];
|
|
2231
|
+
expect(chartAxis.base).toBeDefined();
|
|
2232
|
+
expect(typeof chartAxis.base).toBe('string');
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
it('should have position variants for top, right, bottom, left', () => {
|
|
2236
|
+
const chartAxis = ctx.styles['chartAxis'];
|
|
2237
|
+
expect(hasVariants(chartAxis, ['position'])).toBe(true);
|
|
2238
|
+
expect(hasVariantOptions(chartAxis, 'position', ['top', 'right', 'bottom', 'left'])).toBe(true);
|
|
2239
|
+
});
|
|
2240
|
+
});
|
|
2241
|
+
|
|
2242
|
+
// ==================== chartLabel ====================
|
|
2243
|
+
|
|
2244
|
+
describe('Style Preset: chartLabel', () => {
|
|
2245
|
+
it('should have valid style preset structure', () => {
|
|
2246
|
+
const chartLabel = ctx.styles['chartLabel'];
|
|
2247
|
+
expect(chartLabel).toBeDefined();
|
|
2248
|
+
assertValidStylePreset(chartLabel);
|
|
2249
|
+
});
|
|
2250
|
+
|
|
2251
|
+
it('should have base classes for label styling', () => {
|
|
2252
|
+
const chartLabel = ctx.styles['chartLabel'];
|
|
2253
|
+
expect(chartLabel.base).toBeDefined();
|
|
2254
|
+
expect(typeof chartLabel.base).toBe('string');
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
it('should have type variants for axis, data, title', () => {
|
|
2258
|
+
const chartLabel = ctx.styles['chartLabel'];
|
|
2259
|
+
expect(hasVariants(chartLabel, ['type'])).toBe(true);
|
|
2260
|
+
expect(hasVariantOptions(chartLabel, 'type', ['axis', 'data', 'title'])).toBe(true);
|
|
2261
|
+
});
|
|
2262
|
+
|
|
2263
|
+
it('should have size variants', () => {
|
|
2264
|
+
const chartLabel = ctx.styles['chartLabel'];
|
|
2265
|
+
expect(hasVariants(chartLabel, ['size'])).toBe(true);
|
|
2266
|
+
});
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
// ==================== chartSlice ====================
|
|
2270
|
+
|
|
2271
|
+
describe('Style Preset: chartSlice', () => {
|
|
2272
|
+
it('should have valid style preset structure', () => {
|
|
2273
|
+
const chartSlice = ctx.styles['chartSlice'];
|
|
2274
|
+
expect(chartSlice).toBeDefined();
|
|
2275
|
+
assertValidStylePreset(chartSlice);
|
|
2276
|
+
});
|
|
2277
|
+
|
|
2278
|
+
it('should have base classes for slice styling', () => {
|
|
2279
|
+
const chartSlice = ctx.styles['chartSlice'];
|
|
2280
|
+
expect(chartSlice.base).toBeDefined();
|
|
2281
|
+
expect(typeof chartSlice.base).toBe('string');
|
|
2282
|
+
});
|
|
2283
|
+
|
|
2284
|
+
it('should have state variants for default, hover, and active', () => {
|
|
2285
|
+
const chartSlice = ctx.styles['chartSlice'];
|
|
2286
|
+
expect(hasVariants(chartSlice, ['state'])).toBe(true);
|
|
2287
|
+
expect(hasVariantOptions(chartSlice, 'state', ['default', 'hover', 'active'])).toBe(true);
|
|
2288
|
+
});
|
|
2289
|
+
|
|
2290
|
+
it('should have default state set to default', () => {
|
|
2291
|
+
const chartSlice = ctx.styles['chartSlice'];
|
|
2292
|
+
expect(hasDefaultVariants(chartSlice, { state: 'default' })).toBe(true);
|
|
2293
|
+
});
|
|
2294
|
+
});
|
|
2295
|
+
|
|
2296
|
+
// ==================== chartRadar ====================
|
|
2297
|
+
|
|
2298
|
+
describe('Style Preset: chartRadar', () => {
|
|
2299
|
+
it('should have valid style preset structure', () => {
|
|
2300
|
+
const chartRadar = ctx.styles['chartRadar'];
|
|
2301
|
+
expect(chartRadar).toBeDefined();
|
|
2302
|
+
assertValidStylePreset(chartRadar);
|
|
2303
|
+
});
|
|
2304
|
+
|
|
2305
|
+
it('should have base classes for radar polygon styling', () => {
|
|
2306
|
+
const chartRadar = ctx.styles['chartRadar'];
|
|
2307
|
+
expect(chartRadar.base).toBeDefined();
|
|
2308
|
+
expect(typeof chartRadar.base).toBe('string');
|
|
2309
|
+
});
|
|
2310
|
+
|
|
2311
|
+
it('should have opacity variants', () => {
|
|
2312
|
+
const chartRadar = ctx.styles['chartRadar'];
|
|
2313
|
+
expect(hasVariants(chartRadar, ['opacity'])).toBe(true);
|
|
2314
|
+
});
|
|
2315
|
+
|
|
2316
|
+
it('should have light, default, dark opacity options', () => {
|
|
2317
|
+
const chartRadar = ctx.styles['chartRadar'];
|
|
2318
|
+
expect(hasVariantOptions(chartRadar, 'opacity', ['light', 'default', 'dark'])).toBe(true);
|
|
2319
|
+
});
|
|
2320
|
+
|
|
2321
|
+
it('should have default opacity set to default', () => {
|
|
2322
|
+
const chartRadar = ctx.styles['chartRadar'];
|
|
2323
|
+
expect(hasDefaultVariants(chartRadar, { opacity: 'default' })).toBe(true);
|
|
2324
|
+
});
|
|
2325
|
+
});
|
|
2326
|
+
|
|
2327
|
+
// ==================== chartLegend ====================
|
|
2328
|
+
|
|
2329
|
+
describe('Style Preset: chartLegend', () => {
|
|
2330
|
+
it('should have valid style preset structure', () => {
|
|
2331
|
+
const chartLegend = ctx.styles['chartLegend'];
|
|
2332
|
+
expect(chartLegend).toBeDefined();
|
|
2333
|
+
assertValidStylePreset(chartLegend);
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
it('should have base classes for legend container styling', () => {
|
|
2337
|
+
const chartLegend = ctx.styles['chartLegend'];
|
|
2338
|
+
expect(chartLegend.base).toBeDefined();
|
|
2339
|
+
expect(typeof chartLegend.base).toBe('string');
|
|
2340
|
+
});
|
|
2341
|
+
|
|
2342
|
+
it('should have orientation variants for horizontal and vertical', () => {
|
|
2343
|
+
const chartLegend = ctx.styles['chartLegend'];
|
|
2344
|
+
expect(hasVariants(chartLegend, ['orientation'])).toBe(true);
|
|
2345
|
+
expect(hasVariantOptions(chartLegend, 'orientation', ['horizontal', 'vertical'])).toBe(true);
|
|
2346
|
+
});
|
|
2347
|
+
|
|
2348
|
+
it('should have default orientation set to horizontal', () => {
|
|
2349
|
+
const chartLegend = ctx.styles['chartLegend'];
|
|
2350
|
+
expect(hasDefaultVariants(chartLegend, { orientation: 'horizontal' })).toBe(true);
|
|
2351
|
+
});
|
|
2352
|
+
});
|
|
2353
|
+
|
|
2354
|
+
// ==================== chartLegendItem ====================
|
|
2355
|
+
|
|
2356
|
+
describe('Style Preset: chartLegendItem', () => {
|
|
2357
|
+
it('should have valid style preset structure', () => {
|
|
2358
|
+
const chartLegendItem = ctx.styles['chartLegendItem'];
|
|
2359
|
+
expect(chartLegendItem).toBeDefined();
|
|
2360
|
+
assertValidStylePreset(chartLegendItem);
|
|
2361
|
+
});
|
|
2362
|
+
|
|
2363
|
+
it('should have base classes for legend item styling', () => {
|
|
2364
|
+
const chartLegendItem = ctx.styles['chartLegendItem'];
|
|
2365
|
+
expect(chartLegendItem.base).toBeDefined();
|
|
2366
|
+
expect(typeof chartLegendItem.base).toBe('string');
|
|
2367
|
+
});
|
|
2368
|
+
|
|
2369
|
+
it('should have state variants for default, hover, and disabled', () => {
|
|
2370
|
+
const chartLegendItem = ctx.styles['chartLegendItem'];
|
|
2371
|
+
expect(hasVariants(chartLegendItem, ['state'])).toBe(true);
|
|
2372
|
+
expect(hasVariantOptions(chartLegendItem, 'state', ['default', 'hover', 'disabled'])).toBe(true);
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
it('should have default state set to default', () => {
|
|
2376
|
+
const chartLegendItem = ctx.styles['chartLegendItem'];
|
|
2377
|
+
expect(hasDefaultVariants(chartLegendItem, { state: 'default' })).toBe(true);
|
|
2378
|
+
});
|
|
2379
|
+
});
|
|
2380
|
+
|
|
2381
|
+
// ==================== chartTooltip ====================
|
|
2382
|
+
|
|
2383
|
+
describe('Style Preset: chartTooltip', () => {
|
|
2384
|
+
it('should have valid style preset structure', () => {
|
|
2385
|
+
const chartTooltip = ctx.styles['chartTooltip'];
|
|
2386
|
+
expect(chartTooltip).toBeDefined();
|
|
2387
|
+
assertValidStylePreset(chartTooltip);
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
it('should have base classes for tooltip styling', () => {
|
|
2391
|
+
const chartTooltip = ctx.styles['chartTooltip'];
|
|
2392
|
+
expect(chartTooltip.base).toBeDefined();
|
|
2393
|
+
expect(typeof chartTooltip.base).toBe('string');
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
it('should have absolute positioning in base classes', () => {
|
|
2397
|
+
const chartTooltip = ctx.styles['chartTooltip'];
|
|
2398
|
+
expect(chartTooltip.base).toContain('absolute');
|
|
2399
|
+
});
|
|
2400
|
+
|
|
2401
|
+
it('should have visibility variants', () => {
|
|
2402
|
+
const chartTooltip = ctx.styles['chartTooltip'];
|
|
2403
|
+
expect(hasVariants(chartTooltip, ['visible'])).toBe(true);
|
|
2404
|
+
expect(hasVariantOptions(chartTooltip, 'visible', ['true', 'false'])).toBe(true);
|
|
2405
|
+
});
|
|
2406
|
+
|
|
2407
|
+
it('should have default visibility set to false', () => {
|
|
2408
|
+
const chartTooltip = ctx.styles['chartTooltip'];
|
|
2409
|
+
expect(hasDefaultVariants(chartTooltip, { visible: 'false' })).toBe(true);
|
|
2410
|
+
});
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2413
|
+
// ==================== Animation Style Presets ====================
|
|
2414
|
+
|
|
2415
|
+
describe('Animation Style Presets', () => {
|
|
2416
|
+
// -------------------- chartBarAnimated --------------------
|
|
2417
|
+
|
|
2418
|
+
describe('Style Preset: chartBarAnimated', () => {
|
|
2419
|
+
it('should have chartBarAnimated preset defined', () => {
|
|
2420
|
+
const chartBarAnimated = ctx.styles['chartBarAnimated'];
|
|
2421
|
+
expect(chartBarAnimated).toBeDefined();
|
|
2422
|
+
});
|
|
2423
|
+
|
|
2424
|
+
it('should have valid style preset structure', () => {
|
|
2425
|
+
const chartBarAnimated = ctx.styles['chartBarAnimated'];
|
|
2426
|
+
assertValidStylePreset(chartBarAnimated);
|
|
2427
|
+
});
|
|
2428
|
+
|
|
2429
|
+
it('should have origin-bottom in base classes for transform origin', () => {
|
|
2430
|
+
const chartBarAnimated = ctx.styles['chartBarAnimated'];
|
|
2431
|
+
expect(chartBarAnimated.base).toContain('origin-bottom');
|
|
2432
|
+
});
|
|
2433
|
+
|
|
2434
|
+
it('should have bar-grow animation with correct timing', () => {
|
|
2435
|
+
const chartBarAnimated = ctx.styles['chartBarAnimated'];
|
|
2436
|
+
expect(chartBarAnimated.base).toContain('animate-[bar-grow_0.5s_ease-out_forwards]');
|
|
2437
|
+
});
|
|
2438
|
+
});
|
|
2439
|
+
|
|
2440
|
+
// -------------------- chartLineAnimated --------------------
|
|
2441
|
+
|
|
2442
|
+
describe('Style Preset: chartLineAnimated', () => {
|
|
2443
|
+
it('should have chartLineAnimated preset defined', () => {
|
|
2444
|
+
const chartLineAnimated = ctx.styles['chartLineAnimated'];
|
|
2445
|
+
expect(chartLineAnimated).toBeDefined();
|
|
2446
|
+
});
|
|
2447
|
+
|
|
2448
|
+
it('should have valid style preset structure', () => {
|
|
2449
|
+
const chartLineAnimated = ctx.styles['chartLineAnimated'];
|
|
2450
|
+
assertValidStylePreset(chartLineAnimated);
|
|
2451
|
+
});
|
|
2452
|
+
|
|
2453
|
+
it('should have line-draw animation with correct timing', () => {
|
|
2454
|
+
const chartLineAnimated = ctx.styles['chartLineAnimated'];
|
|
2455
|
+
expect(chartLineAnimated.base).toContain('animate-[line-draw_1s_ease-out_forwards]');
|
|
2456
|
+
});
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
// -------------------- chartSliceAnimated --------------------
|
|
2460
|
+
|
|
2461
|
+
describe('Style Preset: chartSliceAnimated', () => {
|
|
2462
|
+
it('should have chartSliceAnimated preset defined', () => {
|
|
2463
|
+
const chartSliceAnimated = ctx.styles['chartSliceAnimated'];
|
|
2464
|
+
expect(chartSliceAnimated).toBeDefined();
|
|
2465
|
+
});
|
|
2466
|
+
|
|
2467
|
+
it('should have valid style preset structure', () => {
|
|
2468
|
+
const chartSliceAnimated = ctx.styles['chartSliceAnimated'];
|
|
2469
|
+
assertValidStylePreset(chartSliceAnimated);
|
|
2470
|
+
});
|
|
2471
|
+
|
|
2472
|
+
it('should have origin-center in base classes for transform origin', () => {
|
|
2473
|
+
const chartSliceAnimated = ctx.styles['chartSliceAnimated'];
|
|
2474
|
+
expect(chartSliceAnimated.base).toContain('origin-center');
|
|
2475
|
+
});
|
|
2476
|
+
|
|
2477
|
+
it('should have slice-rotate animation with correct timing', () => {
|
|
2478
|
+
const chartSliceAnimated = ctx.styles['chartSliceAnimated'];
|
|
2479
|
+
expect(chartSliceAnimated.base).toContain('animate-[slice-rotate_0.5s_ease-out_forwards]');
|
|
2480
|
+
});
|
|
2481
|
+
});
|
|
2482
|
+
});
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
// ============================================================
|
|
2486
|
+
// CHART ANIMATION CSS KEYFRAMES
|
|
2487
|
+
// ============================================================
|
|
2488
|
+
|
|
2489
|
+
describe('Chart Animation CSS Keyframes', () => {
|
|
2490
|
+
const cssFilePath = join(dirname(fileURLToPath(import.meta.url)), '../../styles/chart-animations.css');
|
|
2491
|
+
|
|
2492
|
+
describe('CSS File Existence', () => {
|
|
2493
|
+
it('should have chart-animations.css file in styles directory', () => {
|
|
2494
|
+
expect(existsSync(cssFilePath)).toBe(true);
|
|
2495
|
+
});
|
|
2496
|
+
});
|
|
2497
|
+
|
|
2498
|
+
describe('Keyframe Definitions', () => {
|
|
2499
|
+
let cssContent: string;
|
|
2500
|
+
|
|
2501
|
+
beforeAll(() => {
|
|
2502
|
+
// Only read if file exists; otherwise empty string for failing tests
|
|
2503
|
+
cssContent = existsSync(cssFilePath) ? readFileSync(cssFilePath, 'utf-8') : '';
|
|
2504
|
+
});
|
|
2505
|
+
|
|
2506
|
+
it('should define bar-grow keyframe', () => {
|
|
2507
|
+
expect(cssContent).toContain('@keyframes bar-grow');
|
|
2508
|
+
});
|
|
2509
|
+
|
|
2510
|
+
it('bar-grow should animate from scaleY(0) to scaleY(1)', () => {
|
|
2511
|
+
// Check for scaleY(0) in from/0% and scaleY(1) in to/100%
|
|
2512
|
+
expect(cssContent).toMatch(/bar-grow[\s\S]*scaleY\(0\)/);
|
|
2513
|
+
expect(cssContent).toMatch(/bar-grow[\s\S]*scaleY\(1\)/);
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
it('should define line-draw keyframe', () => {
|
|
2517
|
+
expect(cssContent).toContain('@keyframes line-draw');
|
|
2518
|
+
});
|
|
2519
|
+
|
|
2520
|
+
it('line-draw should animate stroke-dashoffset from 100% to 0', () => {
|
|
2521
|
+
expect(cssContent).toMatch(/line-draw[\s\S]*stroke-dashoffset/);
|
|
2522
|
+
});
|
|
2523
|
+
|
|
2524
|
+
it('should define slice-rotate keyframe', () => {
|
|
2525
|
+
expect(cssContent).toContain('@keyframes slice-rotate');
|
|
2526
|
+
});
|
|
2527
|
+
|
|
2528
|
+
it('slice-rotate should animate from scale(0) rotate(-90deg) to scale(1) rotate(0)', () => {
|
|
2529
|
+
// Check for scale(0) and rotate(-90deg) in from/0%
|
|
2530
|
+
expect(cssContent).toMatch(/slice-rotate[\s\S]*scale\(0\)/);
|
|
2531
|
+
expect(cssContent).toMatch(/slice-rotate[\s\S]*rotate\(-90deg\)/);
|
|
2532
|
+
// Check for scale(1) and rotate(0) in to/100%
|
|
2533
|
+
expect(cssContent).toMatch(/slice-rotate[\s\S]*scale\(1\)/);
|
|
2534
|
+
expect(cssContent).toMatch(/slice-rotate[\s\S]*rotate\(0/);
|
|
2535
|
+
});
|
|
2536
|
+
});
|
|
2537
|
+
});
|
|
2538
|
+
|
|
2539
|
+
// ============================================================
|
|
2540
|
+
// INTEGRATION TESTS
|
|
2541
|
+
// ============================================================
|
|
2542
|
+
|
|
2543
|
+
describe('Integration', () => {
|
|
2544
|
+
describe('Component-Style Consistency', () => {
|
|
2545
|
+
it('BarChart should reference only existing style presets', () => {
|
|
2546
|
+
// All presets used in BarChart should exist in styles
|
|
2547
|
+
expect(ctx.styles['chartSvg']).toBeDefined();
|
|
2548
|
+
expect(ctx.styles['chartBar']).toBeDefined();
|
|
2549
|
+
expect(ctx.styles['chartGrid']).toBeDefined();
|
|
2550
|
+
expect(ctx.styles['chartLabel']).toBeDefined();
|
|
2551
|
+
});
|
|
2552
|
+
|
|
2553
|
+
it('LineChart should reference only existing style presets', () => {
|
|
2554
|
+
expect(ctx.styles['chartSvg']).toBeDefined();
|
|
2555
|
+
expect(ctx.styles['chartLine']).toBeDefined();
|
|
2556
|
+
expect(ctx.styles['chartPoint']).toBeDefined();
|
|
2557
|
+
expect(ctx.styles['chartGrid']).toBeDefined();
|
|
2558
|
+
expect(ctx.styles['chartLabel']).toBeDefined();
|
|
2559
|
+
});
|
|
2560
|
+
|
|
2561
|
+
it('AreaChart should reference only existing style presets', () => {
|
|
2562
|
+
expect(ctx.styles['chartSvg']).toBeDefined();
|
|
2563
|
+
expect(ctx.styles['chartArea']).toBeDefined();
|
|
2564
|
+
expect(ctx.styles['chartLine']).toBeDefined();
|
|
2565
|
+
expect(ctx.styles['chartPoint']).toBeDefined();
|
|
2566
|
+
expect(ctx.styles['chartGrid']).toBeDefined();
|
|
2567
|
+
expect(ctx.styles['chartLabel']).toBeDefined();
|
|
2568
|
+
});
|
|
2569
|
+
|
|
2570
|
+
it('PieChart should reference only existing style presets', () => {
|
|
2571
|
+
expect(ctx.styles['chartSvg']).toBeDefined();
|
|
2572
|
+
expect(ctx.styles['chartSlice']).toBeDefined();
|
|
2573
|
+
expect(ctx.styles['chartLabel']).toBeDefined();
|
|
2574
|
+
});
|
|
2575
|
+
|
|
2576
|
+
it('DonutChart should reference only existing style presets', () => {
|
|
2577
|
+
expect(ctx.styles['chartSvg']).toBeDefined();
|
|
2578
|
+
expect(ctx.styles['chartSlice']).toBeDefined();
|
|
2579
|
+
expect(ctx.styles['chartLabel']).toBeDefined();
|
|
2580
|
+
});
|
|
2581
|
+
|
|
2582
|
+
it('ScatterChart should reference only existing style presets', () => {
|
|
2583
|
+
expect(ctx.styles['chartSvg']).toBeDefined();
|
|
2584
|
+
expect(ctx.styles['chartPoint']).toBeDefined();
|
|
2585
|
+
expect(ctx.styles['chartGrid']).toBeDefined();
|
|
2586
|
+
expect(ctx.styles['chartLabel']).toBeDefined();
|
|
2587
|
+
});
|
|
2588
|
+
|
|
2589
|
+
it('RadarChart should reference only existing style presets', () => {
|
|
2590
|
+
expect(ctx.styles['chartSvg']).toBeDefined();
|
|
2591
|
+
expect(ctx.styles['chartRadar']).toBeDefined();
|
|
2592
|
+
expect(ctx.styles['chartGrid']).toBeDefined();
|
|
2593
|
+
});
|
|
2594
|
+
|
|
2595
|
+
it('ChartLegend should reference only existing style presets', () => {
|
|
2596
|
+
expect(ctx.styles['chartLegend']).toBeDefined();
|
|
2597
|
+
expect(ctx.styles['chartLegendItem']).toBeDefined();
|
|
2598
|
+
});
|
|
2599
|
+
|
|
2600
|
+
it('ChartTooltip should reference only existing style presets', () => {
|
|
2601
|
+
expect(ctx.styles['chartTooltip']).toBeDefined();
|
|
2602
|
+
});
|
|
2603
|
+
|
|
2604
|
+
it('ChartAxis should reference only existing style presets', () => {
|
|
2605
|
+
expect(ctx.styles['chartAxis']).toBeDefined();
|
|
2606
|
+
expect(ctx.styles['chartLabel']).toBeDefined();
|
|
2607
|
+
});
|
|
2608
|
+
});
|
|
2609
|
+
|
|
2610
|
+
describe('Helper Function Usage', () => {
|
|
2611
|
+
it('BarChart should use getBarDimensions for positioning', () => {
|
|
2612
|
+
const call = findExpressionCall(ctx.barChart.view, 'getBarDimensions');
|
|
2613
|
+
expect(call).not.toBeNull();
|
|
2614
|
+
});
|
|
2615
|
+
|
|
2616
|
+
it('LineChart should use getLinePath for path generation', () => {
|
|
2617
|
+
const call = findExpressionCall(ctx.lineChart.view, 'getLinePath');
|
|
2618
|
+
expect(call).not.toBeNull();
|
|
2619
|
+
});
|
|
2620
|
+
|
|
2621
|
+
it('AreaChart should use getAreaPath for area fill', () => {
|
|
2622
|
+
const call = findExpressionCall(ctx.areaChart.view, 'getAreaPath');
|
|
2623
|
+
expect(call).not.toBeNull();
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
it('PieChart should use getPieSlices for slice calculation', () => {
|
|
2627
|
+
const call = findExpressionCall(ctx.pieChart.view, 'getPieSlices');
|
|
2628
|
+
expect(call).not.toBeNull();
|
|
2629
|
+
});
|
|
2630
|
+
|
|
2631
|
+
it('PieChart should use getArcPath for path generation', () => {
|
|
2632
|
+
const call = findExpressionCall(ctx.pieChart.view, 'getArcPath');
|
|
2633
|
+
expect(call).not.toBeNull();
|
|
2634
|
+
});
|
|
2635
|
+
|
|
2636
|
+
it('DonutChart should use getDonutSlices for slice calculation', () => {
|
|
2637
|
+
const call = findExpressionCall(ctx.donutChart.view, 'getDonutSlices');
|
|
2638
|
+
expect(call).not.toBeNull();
|
|
2639
|
+
});
|
|
2640
|
+
|
|
2641
|
+
it('DonutChart should use getArcPath for path generation', () => {
|
|
2642
|
+
const call = findExpressionCall(ctx.donutChart.view, 'getArcPath');
|
|
2643
|
+
expect(call).not.toBeNull();
|
|
2644
|
+
});
|
|
2645
|
+
|
|
2646
|
+
it('RadarChart should use getRadarPoints for polygon calculation', () => {
|
|
2647
|
+
const call = findExpressionCall(ctx.radarChart.view, 'getRadarPoints');
|
|
2648
|
+
expect(call).not.toBeNull();
|
|
2649
|
+
});
|
|
2650
|
+
|
|
2651
|
+
it('RadarChart should use getRadarAxes for axis generation', () => {
|
|
2652
|
+
const call = findExpressionCall(ctx.radarChart.view, 'getRadarAxes');
|
|
2653
|
+
expect(call).not.toBeNull();
|
|
2654
|
+
});
|
|
2655
|
+
|
|
2656
|
+
it('ChartAxis should use generateTicks for tick generation', () => {
|
|
2657
|
+
const call = findExpressionCall(ctx.chartAxis.view, 'generateTicks');
|
|
2658
|
+
expect(call).not.toBeNull();
|
|
2659
|
+
});
|
|
2660
|
+
});
|
|
2661
|
+
|
|
2662
|
+
describe('Common Param Consistency', () => {
|
|
2663
|
+
const commonParams = ['data', 'valueKey', 'labelKey', 'width', 'height', 'colors', 'showGrid', 'showLabels'];
|
|
2664
|
+
const pieDonutCommonParams = ['data', 'valueKey', 'labelKey', 'width', 'height', 'colors', 'showLabels'];
|
|
2665
|
+
const scatterChartParams = ['data', 'xKey', 'yKey', 'width', 'height', 'colors', 'showGrid', 'showLabels'];
|
|
2666
|
+
const radarChartParams = ['data', 'valueKey', 'labelKey', 'width', 'height', 'colors', 'showGrid'];
|
|
2667
|
+
|
|
2668
|
+
it('all chart components should have common params', () => {
|
|
2669
|
+
expect(hasParams(ctx.barChart, commonParams)).toBe(true);
|
|
2670
|
+
expect(hasParams(ctx.lineChart, commonParams)).toBe(true);
|
|
2671
|
+
expect(hasParams(ctx.areaChart, commonParams)).toBe(true);
|
|
2672
|
+
});
|
|
2673
|
+
|
|
2674
|
+
it('pie and donut charts should have common params (without showGrid)', () => {
|
|
2675
|
+
expect(hasParams(ctx.pieChart, pieDonutCommonParams)).toBe(true);
|
|
2676
|
+
expect(hasParams(ctx.donutChart, pieDonutCommonParams)).toBe(true);
|
|
2677
|
+
});
|
|
2678
|
+
|
|
2679
|
+
it('scatter chart should have its required params (xKey, yKey instead of valueKey)', () => {
|
|
2680
|
+
expect(hasParams(ctx.scatterChart, scatterChartParams)).toBe(true);
|
|
2681
|
+
});
|
|
2682
|
+
|
|
2683
|
+
it('radar chart should have its common params', () => {
|
|
2684
|
+
expect(hasParams(ctx.radarChart, radarChartParams)).toBe(true);
|
|
2685
|
+
});
|
|
2686
|
+
|
|
2687
|
+
it('all chart components should have consistent required params', () => {
|
|
2688
|
+
// data and valueKey should be required for all charts
|
|
2689
|
+
expect(isRequiredParam(ctx.barChart, 'data')).toBe(true);
|
|
2690
|
+
expect(isRequiredParam(ctx.barChart, 'valueKey')).toBe(true);
|
|
2691
|
+
expect(isRequiredParam(ctx.lineChart, 'data')).toBe(true);
|
|
2692
|
+
expect(isRequiredParam(ctx.lineChart, 'valueKey')).toBe(true);
|
|
2693
|
+
expect(isRequiredParam(ctx.areaChart, 'data')).toBe(true);
|
|
2694
|
+
expect(isRequiredParam(ctx.areaChart, 'valueKey')).toBe(true);
|
|
2695
|
+
expect(isRequiredParam(ctx.pieChart, 'data')).toBe(true);
|
|
2696
|
+
expect(isRequiredParam(ctx.pieChart, 'valueKey')).toBe(true);
|
|
2697
|
+
expect(isRequiredParam(ctx.donutChart, 'data')).toBe(true);
|
|
2698
|
+
expect(isRequiredParam(ctx.donutChart, 'valueKey')).toBe(true);
|
|
2699
|
+
// ScatterChart uses xKey and yKey instead of valueKey
|
|
2700
|
+
expect(isRequiredParam(ctx.scatterChart, 'data')).toBe(true);
|
|
2701
|
+
expect(isRequiredParam(ctx.scatterChart, 'xKey')).toBe(true);
|
|
2702
|
+
expect(isRequiredParam(ctx.scatterChart, 'yKey')).toBe(true);
|
|
2703
|
+
// RadarChart requires data, valueKey, and labelKey
|
|
2704
|
+
expect(isRequiredParam(ctx.radarChart, 'data')).toBe(true);
|
|
2705
|
+
expect(isRequiredParam(ctx.radarChart, 'valueKey')).toBe(true);
|
|
2706
|
+
expect(isRequiredParam(ctx.radarChart, 'labelKey')).toBe(true);
|
|
2707
|
+
});
|
|
2708
|
+
});
|
|
2709
|
+
|
|
2710
|
+
describe('Accessibility Consistency', () => {
|
|
2711
|
+
it('all chart components should have role="img"', () => {
|
|
2712
|
+
expect(hasRole(ctx.barChart.view, 'img')).toBe(true);
|
|
2713
|
+
expect(hasRole(ctx.lineChart.view, 'img')).toBe(true);
|
|
2714
|
+
expect(hasRole(ctx.areaChart.view, 'img')).toBe(true);
|
|
2715
|
+
expect(hasRole(ctx.pieChart.view, 'img')).toBe(true);
|
|
2716
|
+
expect(hasRole(ctx.donutChart.view, 'img')).toBe(true);
|
|
2717
|
+
expect(hasRole(ctx.scatterChart.view, 'img')).toBe(true);
|
|
2718
|
+
expect(hasRole(ctx.radarChart.view, 'img')).toBe(true);
|
|
2719
|
+
});
|
|
2720
|
+
|
|
2721
|
+
it('all chart components should have aria-label', () => {
|
|
2722
|
+
expect(hasAriaAttribute(ctx.barChart.view, 'label')).toBe(true);
|
|
2723
|
+
expect(hasAriaAttribute(ctx.lineChart.view, 'label')).toBe(true);
|
|
2724
|
+
expect(hasAriaAttribute(ctx.areaChart.view, 'label')).toBe(true);
|
|
2725
|
+
expect(hasAriaAttribute(ctx.pieChart.view, 'label')).toBe(true);
|
|
2726
|
+
expect(hasAriaAttribute(ctx.donutChart.view, 'label')).toBe(true);
|
|
2727
|
+
expect(hasAriaAttribute(ctx.scatterChart.view, 'label')).toBe(true);
|
|
2728
|
+
expect(hasAriaAttribute(ctx.radarChart.view, 'label')).toBe(true);
|
|
2729
|
+
});
|
|
2730
|
+
|
|
2731
|
+
it('all chart components should have title element', () => {
|
|
2732
|
+
expect(hasElementTag(ctx.barChart.view, 'title')).toBe(true);
|
|
2733
|
+
expect(hasElementTag(ctx.lineChart.view, 'title')).toBe(true);
|
|
2734
|
+
expect(hasElementTag(ctx.areaChart.view, 'title')).toBe(true);
|
|
2735
|
+
expect(hasElementTag(ctx.pieChart.view, 'title')).toBe(true);
|
|
2736
|
+
expect(hasElementTag(ctx.donutChart.view, 'title')).toBe(true);
|
|
2737
|
+
expect(hasElementTag(ctx.scatterChart.view, 'title')).toBe(true);
|
|
2738
|
+
expect(hasElementTag(ctx.radarChart.view, 'title')).toBe(true);
|
|
2739
|
+
});
|
|
2740
|
+
});
|
|
2741
|
+
});
|
|
2742
|
+
|
|
2743
|
+
// ============================================================
|
|
2744
|
+
// LOCAL STATE REFACTORING TESTS (TDD Red Phase)
|
|
2745
|
+
// ============================================================
|
|
2746
|
+
// These tests verify that chart components use localState to compute
|
|
2747
|
+
// default values once, eliminating duplicate cond/then/else patterns.
|
|
2748
|
+
|
|
2749
|
+
describe('LocalState Default Value Refactoring', () => {
|
|
2750
|
+
// ==================== BarChart LocalState Tests ====================
|
|
2751
|
+
|
|
2752
|
+
describe('BarChart localState', () => {
|
|
2753
|
+
it('should have localState property defined', () => {
|
|
2754
|
+
expect(ctx.barChart.localState).toBeDefined();
|
|
2755
|
+
});
|
|
2756
|
+
|
|
2757
|
+
describe('_width local state', () => {
|
|
2758
|
+
it('should have _width field in localState', () => {
|
|
2759
|
+
expect(hasLocalState(ctx.barChart, '_width')).toBe(true);
|
|
2760
|
+
});
|
|
2761
|
+
|
|
2762
|
+
it('should have _width as number type', () => {
|
|
2763
|
+
expect(hasLocalStateType(ctx.barChart, '_width', 'number')).toBe(true);
|
|
2764
|
+
});
|
|
2765
|
+
|
|
2766
|
+
it('should have _width initial with cond pattern for width param defaulting to 400', () => {
|
|
2767
|
+
expect(hasCondInitialPattern(ctx.barChart, '_width', 'width', 400)).toBe(true);
|
|
2768
|
+
});
|
|
2769
|
+
});
|
|
2770
|
+
|
|
2771
|
+
describe('_height local state', () => {
|
|
2772
|
+
it('should have _height field in localState', () => {
|
|
2773
|
+
expect(hasLocalState(ctx.barChart, '_height')).toBe(true);
|
|
2774
|
+
});
|
|
2775
|
+
|
|
2776
|
+
it('should have _height as number type', () => {
|
|
2777
|
+
expect(hasLocalStateType(ctx.barChart, '_height', 'number')).toBe(true);
|
|
2778
|
+
});
|
|
2779
|
+
|
|
2780
|
+
it('should have _height initial with cond pattern for height param defaulting to 300', () => {
|
|
2781
|
+
expect(hasCondInitialPattern(ctx.barChart, '_height', 'height', 300)).toBe(true);
|
|
2782
|
+
});
|
|
2783
|
+
});
|
|
2784
|
+
|
|
2785
|
+
describe('_barGap local state', () => {
|
|
2786
|
+
it('should have _barGap field in localState', () => {
|
|
2787
|
+
expect(hasLocalState(ctx.barChart, '_barGap')).toBe(true);
|
|
2788
|
+
});
|
|
2789
|
+
|
|
2790
|
+
it('should have _barGap as number type', () => {
|
|
2791
|
+
expect(hasLocalStateType(ctx.barChart, '_barGap', 'number')).toBe(true);
|
|
2792
|
+
});
|
|
2793
|
+
|
|
2794
|
+
it('should have _barGap initial with cond pattern for barGap param defaulting to 4', () => {
|
|
2795
|
+
expect(hasCondInitialPattern(ctx.barChart, '_barGap', 'barGap', 4)).toBe(true);
|
|
2796
|
+
});
|
|
2797
|
+
});
|
|
2798
|
+
|
|
2799
|
+
describe('_orientation local state', () => {
|
|
2800
|
+
it('should have _orientation field in localState', () => {
|
|
2801
|
+
expect(hasLocalState(ctx.barChart, '_orientation')).toBe(true);
|
|
2802
|
+
});
|
|
2803
|
+
|
|
2804
|
+
it('should have _orientation as string type', () => {
|
|
2805
|
+
expect(hasLocalStateType(ctx.barChart, '_orientation', 'string')).toBe(true);
|
|
2806
|
+
});
|
|
2807
|
+
|
|
2808
|
+
it('should have _orientation initial with cond pattern for orientation param defaulting to "vertical"', () => {
|
|
2809
|
+
expect(hasCondInitialPattern(ctx.barChart, '_orientation', 'orientation', 'vertical')).toBe(true);
|
|
2810
|
+
});
|
|
2811
|
+
});
|
|
2812
|
+
|
|
2813
|
+
describe('view uses local references', () => {
|
|
2814
|
+
it('should reference _width via local expression in view', () => {
|
|
2815
|
+
expect(hasLocalReference(ctx.barChart.view, '_width')).toBe(true);
|
|
2816
|
+
});
|
|
2817
|
+
|
|
2818
|
+
it('should reference _height via local expression in view', () => {
|
|
2819
|
+
expect(hasLocalReference(ctx.barChart.view, '_height')).toBe(true);
|
|
2820
|
+
});
|
|
2821
|
+
|
|
2822
|
+
it('should reference _barGap via local expression in view', () => {
|
|
2823
|
+
expect(hasLocalReference(ctx.barChart.view, '_barGap')).toBe(true);
|
|
2824
|
+
});
|
|
2825
|
+
|
|
2826
|
+
it('should reference _orientation via local expression in view', () => {
|
|
2827
|
+
expect(hasLocalReference(ctx.barChart.view, '_orientation')).toBe(true);
|
|
2828
|
+
});
|
|
2829
|
+
|
|
2830
|
+
it('should have zero or minimal cond patterns for width in view (refactored)', () => {
|
|
2831
|
+
// After refactoring, there should be no repeated cond patterns for width
|
|
2832
|
+
// (only in localState, not in view)
|
|
2833
|
+
const count = countCondPatterns(ctx.barChart.view, 'width');
|
|
2834
|
+
expect(count).toBe(0);
|
|
2835
|
+
});
|
|
2836
|
+
|
|
2837
|
+
it('should have zero or minimal cond patterns for height in view (refactored)', () => {
|
|
2838
|
+
const count = countCondPatterns(ctx.barChart.view, 'height');
|
|
2839
|
+
expect(count).toBe(0);
|
|
2840
|
+
});
|
|
2841
|
+
|
|
2842
|
+
it('should have zero cond patterns for barGap in view (refactored)', () => {
|
|
2843
|
+
const count = countCondPatterns(ctx.barChart.view, 'barGap');
|
|
2844
|
+
expect(count).toBe(0);
|
|
2845
|
+
});
|
|
2846
|
+
|
|
2847
|
+
it('should have zero cond patterns for orientation in view (refactored)', () => {
|
|
2848
|
+
const count = countCondPatterns(ctx.barChart.view, 'orientation');
|
|
2849
|
+
expect(count).toBe(0);
|
|
2850
|
+
});
|
|
2851
|
+
});
|
|
2852
|
+
});
|
|
2853
|
+
|
|
2854
|
+
// ==================== LineChart LocalState Tests ====================
|
|
2855
|
+
|
|
2856
|
+
describe('LineChart localState', () => {
|
|
2857
|
+
it('should have localState property defined', () => {
|
|
2858
|
+
expect(ctx.lineChart.localState).toBeDefined();
|
|
2859
|
+
});
|
|
2860
|
+
|
|
2861
|
+
describe('_width local state', () => {
|
|
2862
|
+
it('should have _width field in localState', () => {
|
|
2863
|
+
expect(hasLocalState(ctx.lineChart, '_width')).toBe(true);
|
|
2864
|
+
});
|
|
2865
|
+
|
|
2866
|
+
it('should have _width as number type', () => {
|
|
2867
|
+
expect(hasLocalStateType(ctx.lineChart, '_width', 'number')).toBe(true);
|
|
2868
|
+
});
|
|
2869
|
+
|
|
2870
|
+
it('should have _width initial with cond pattern for width param defaulting to 400', () => {
|
|
2871
|
+
expect(hasCondInitialPattern(ctx.lineChart, '_width', 'width', 400)).toBe(true);
|
|
2872
|
+
});
|
|
2873
|
+
});
|
|
2874
|
+
|
|
2875
|
+
describe('_height local state', () => {
|
|
2876
|
+
it('should have _height field in localState', () => {
|
|
2877
|
+
expect(hasLocalState(ctx.lineChart, '_height')).toBe(true);
|
|
2878
|
+
});
|
|
2879
|
+
|
|
2880
|
+
it('should have _height as number type', () => {
|
|
2881
|
+
expect(hasLocalStateType(ctx.lineChart, '_height', 'number')).toBe(true);
|
|
2882
|
+
});
|
|
2883
|
+
|
|
2884
|
+
it('should have _height initial with cond pattern for height param defaulting to 300', () => {
|
|
2885
|
+
expect(hasCondInitialPattern(ctx.lineChart, '_height', 'height', 300)).toBe(true);
|
|
2886
|
+
});
|
|
2887
|
+
});
|
|
2888
|
+
|
|
2889
|
+
describe('view uses local references', () => {
|
|
2890
|
+
it('should reference _width via local expression in view', () => {
|
|
2891
|
+
expect(hasLocalReference(ctx.lineChart.view, '_width')).toBe(true);
|
|
2892
|
+
});
|
|
2893
|
+
|
|
2894
|
+
it('should reference _height via local expression in view', () => {
|
|
2895
|
+
expect(hasLocalReference(ctx.lineChart.view, '_height')).toBe(true);
|
|
2896
|
+
});
|
|
2897
|
+
|
|
2898
|
+
it('should have zero cond patterns for width in view (refactored)', () => {
|
|
2899
|
+
const count = countCondPatterns(ctx.lineChart.view, 'width');
|
|
2900
|
+
expect(count).toBe(0);
|
|
2901
|
+
});
|
|
2902
|
+
|
|
2903
|
+
it('should have zero cond patterns for height in view (refactored)', () => {
|
|
2904
|
+
const count = countCondPatterns(ctx.lineChart.view, 'height');
|
|
2905
|
+
expect(count).toBe(0);
|
|
2906
|
+
});
|
|
2907
|
+
});
|
|
2908
|
+
});
|
|
2909
|
+
|
|
2910
|
+
// ==================== AreaChart LocalState Tests ====================
|
|
2911
|
+
|
|
2912
|
+
describe('AreaChart localState', () => {
|
|
2913
|
+
it('should have localState property defined', () => {
|
|
2914
|
+
expect(ctx.areaChart.localState).toBeDefined();
|
|
2915
|
+
});
|
|
2916
|
+
|
|
2917
|
+
describe('_width local state', () => {
|
|
2918
|
+
it('should have _width field in localState', () => {
|
|
2919
|
+
expect(hasLocalState(ctx.areaChart, '_width')).toBe(true);
|
|
2920
|
+
});
|
|
2921
|
+
|
|
2922
|
+
it('should have _width as number type', () => {
|
|
2923
|
+
expect(hasLocalStateType(ctx.areaChart, '_width', 'number')).toBe(true);
|
|
2924
|
+
});
|
|
2925
|
+
|
|
2926
|
+
it('should have _width initial with cond pattern for width param defaulting to 400', () => {
|
|
2927
|
+
expect(hasCondInitialPattern(ctx.areaChart, '_width', 'width', 400)).toBe(true);
|
|
2928
|
+
});
|
|
2929
|
+
});
|
|
2930
|
+
|
|
2931
|
+
describe('_height local state', () => {
|
|
2932
|
+
it('should have _height field in localState', () => {
|
|
2933
|
+
expect(hasLocalState(ctx.areaChart, '_height')).toBe(true);
|
|
2934
|
+
});
|
|
2935
|
+
|
|
2936
|
+
it('should have _height as number type', () => {
|
|
2937
|
+
expect(hasLocalStateType(ctx.areaChart, '_height', 'number')).toBe(true);
|
|
2938
|
+
});
|
|
2939
|
+
|
|
2940
|
+
it('should have _height initial with cond pattern for height param defaulting to 300', () => {
|
|
2941
|
+
expect(hasCondInitialPattern(ctx.areaChart, '_height', 'height', 300)).toBe(true);
|
|
2942
|
+
});
|
|
2943
|
+
});
|
|
2944
|
+
|
|
2945
|
+
describe('view uses local references', () => {
|
|
2946
|
+
it('should reference _width via local expression in view', () => {
|
|
2947
|
+
expect(hasLocalReference(ctx.areaChart.view, '_width')).toBe(true);
|
|
2948
|
+
});
|
|
2949
|
+
|
|
2950
|
+
it('should reference _height via local expression in view', () => {
|
|
2951
|
+
expect(hasLocalReference(ctx.areaChart.view, '_height')).toBe(true);
|
|
2952
|
+
});
|
|
2953
|
+
|
|
2954
|
+
it('should have zero cond patterns for width in view (refactored)', () => {
|
|
2955
|
+
const count = countCondPatterns(ctx.areaChart.view, 'width');
|
|
2956
|
+
expect(count).toBe(0);
|
|
2957
|
+
});
|
|
2958
|
+
|
|
2959
|
+
it('should have zero cond patterns for height in view (refactored)', () => {
|
|
2960
|
+
const count = countCondPatterns(ctx.areaChart.view, 'height');
|
|
2961
|
+
expect(count).toBe(0);
|
|
2962
|
+
});
|
|
2963
|
+
});
|
|
2964
|
+
});
|
|
2965
|
+
|
|
2966
|
+
// ==================== Cross-Component Consistency Tests ====================
|
|
2967
|
+
|
|
2968
|
+
describe('Cross-Component Consistency', () => {
|
|
2969
|
+
it('all charts with width/height should have corresponding localState fields', () => {
|
|
2970
|
+
// BarChart, LineChart, AreaChart should all have _width and _height
|
|
2971
|
+
expect(hasLocalState(ctx.barChart, '_width')).toBe(true);
|
|
2972
|
+
expect(hasLocalState(ctx.barChart, '_height')).toBe(true);
|
|
2973
|
+
expect(hasLocalState(ctx.lineChart, '_width')).toBe(true);
|
|
2974
|
+
expect(hasLocalState(ctx.lineChart, '_height')).toBe(true);
|
|
2975
|
+
expect(hasLocalState(ctx.areaChart, '_width')).toBe(true);
|
|
2976
|
+
expect(hasLocalState(ctx.areaChart, '_height')).toBe(true);
|
|
2977
|
+
});
|
|
2978
|
+
|
|
2979
|
+
it('all localState fields should use underscore prefix naming convention', () => {
|
|
2980
|
+
// Verify naming convention: local state fields start with underscore
|
|
2981
|
+
const barChartLocalState = ctx.barChart.localState;
|
|
2982
|
+
if (barChartLocalState) {
|
|
2983
|
+
for (const fieldName of Object.keys(barChartLocalState)) {
|
|
2984
|
+
expect(fieldName.startsWith('_')).toBe(true);
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
const lineChartLocalState = ctx.lineChart.localState;
|
|
2989
|
+
if (lineChartLocalState) {
|
|
2990
|
+
for (const fieldName of Object.keys(lineChartLocalState)) {
|
|
2991
|
+
expect(fieldName.startsWith('_')).toBe(true);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
const areaChartLocalState = ctx.areaChart.localState;
|
|
2996
|
+
if (areaChartLocalState) {
|
|
2997
|
+
for (const fieldName of Object.keys(areaChartLocalState)) {
|
|
2998
|
+
expect(fieldName.startsWith('_')).toBe(true);
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
});
|
|
3003
|
+
});
|
|
3004
|
+
|
|
3005
|
+
// ============================================================
|
|
3006
|
+
// LABEL POSITION BUG FIX TESTS (Task 2)
|
|
3007
|
+
// ============================================================
|
|
3008
|
+
|
|
3009
|
+
/**
|
|
3010
|
+
* Tests for label positioning bug fix.
|
|
3011
|
+
*
|
|
3012
|
+
* Currently, all label x/y positions are hardcoded:
|
|
3013
|
+
* - bar-chart.constela.json: x: "50", y: "290"
|
|
3014
|
+
* - line-chart.constela.json: x: "50", y: "290"
|
|
3015
|
+
* - area-chart.constela.json: x: "50", y: "290"
|
|
3016
|
+
*
|
|
3017
|
+
* This causes all labels to overlap at the same position.
|
|
3018
|
+
* Labels should be positioned based on data index.
|
|
3019
|
+
*
|
|
3020
|
+
* Expected fix:
|
|
3021
|
+
* - BarChart: x should use getBarDimensions result + width/2 offset
|
|
3022
|
+
* - LineChart/AreaChart: x should use 40 + idx * step formula
|
|
3023
|
+
* - All charts: y should reference _height from localState + offset
|
|
3024
|
+
*/
|
|
3025
|
+
describe('Label Position Bug Fix', () => {
|
|
3026
|
+
/**
|
|
3027
|
+
* Find the label text element props inside showLabels conditional
|
|
3028
|
+
* Returns the props object of the text element inside the each loop
|
|
3029
|
+
*/
|
|
3030
|
+
function findLabelTextProps(view: unknown): Record<string, unknown> | null {
|
|
3031
|
+
if (!view || typeof view !== 'object') return null;
|
|
3032
|
+
const node = view as Record<string, unknown>;
|
|
3033
|
+
|
|
3034
|
+
// Check if this is the showLabels conditional (kind: "if" with test for showLabels param)
|
|
3035
|
+
if (node['kind'] === 'if') {
|
|
3036
|
+
const test = node['test'] as Record<string, unknown> | undefined;
|
|
3037
|
+
if (test && test['expr'] === 'param' && test['name'] === 'showLabels') {
|
|
3038
|
+
// Found the showLabels if block, now find the text element in then branch
|
|
3039
|
+
const thenBranch = node['then'] as Record<string, unknown>;
|
|
3040
|
+
if (thenBranch && thenBranch['children'] && Array.isArray(thenBranch['children'])) {
|
|
3041
|
+
for (const child of thenBranch['children']) {
|
|
3042
|
+
const childNode = child as Record<string, unknown>;
|
|
3043
|
+
if (childNode['kind'] === 'each') {
|
|
3044
|
+
const body = childNode['body'] as Record<string, unknown>;
|
|
3045
|
+
if (body && body['kind'] === 'element' && body['tag'] === 'text') {
|
|
3046
|
+
return body['props'] as Record<string, unknown> | null;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
// Search in children
|
|
3055
|
+
if (node['children'] && Array.isArray(node['children'])) {
|
|
3056
|
+
for (const child of node['children']) {
|
|
3057
|
+
const result = findLabelTextProps(child);
|
|
3058
|
+
if (result) return result;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
return null;
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
/**
|
|
3066
|
+
* Check if an expression is a hardcoded literal with a specific value
|
|
3067
|
+
*/
|
|
3068
|
+
function isHardcodedLiteral(expr: unknown, value: string): boolean {
|
|
3069
|
+
if (!expr || typeof expr !== 'object') return false;
|
|
3070
|
+
const exprObj = expr as Record<string, unknown>;
|
|
3071
|
+
return exprObj['expr'] === 'lit' && exprObj['value'] === value;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
/**
|
|
3075
|
+
* Check if an expression contains a reference to a specific variable
|
|
3076
|
+
*/
|
|
3077
|
+
function containsVarReference(expr: unknown, varName: string): boolean {
|
|
3078
|
+
if (!expr || typeof expr !== 'object') return false;
|
|
3079
|
+
const exprObj = expr as Record<string, unknown>;
|
|
3080
|
+
|
|
3081
|
+
if (exprObj['expr'] === 'var' && exprObj['name'] === varName) {
|
|
3082
|
+
return true;
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
// Recursively search
|
|
3086
|
+
for (const value of Object.values(exprObj)) {
|
|
3087
|
+
if (typeof value === 'object' && value !== null) {
|
|
3088
|
+
if (containsVarReference(value, varName)) return true;
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
return false;
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
/**
|
|
3095
|
+
* Check if an expression contains a call to a specific method
|
|
3096
|
+
*/
|
|
3097
|
+
function containsMethodCall(expr: unknown, methodName: string): boolean {
|
|
3098
|
+
if (!expr || typeof expr !== 'object') return false;
|
|
3099
|
+
const exprObj = expr as Record<string, unknown>;
|
|
3100
|
+
|
|
3101
|
+
if (exprObj['expr'] === 'call' && exprObj['method'] === methodName) {
|
|
3102
|
+
return true;
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
// Recursively search
|
|
3106
|
+
for (const value of Object.values(exprObj)) {
|
|
3107
|
+
if (typeof value === 'object' && value !== null) {
|
|
3108
|
+
if (containsMethodCall(value, methodName)) return true;
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
return false;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
/**
|
|
3115
|
+
* Check if an expression contains a local reference
|
|
3116
|
+
*/
|
|
3117
|
+
function containsLocalReference(expr: unknown, localName: string): boolean {
|
|
3118
|
+
if (!expr || typeof expr !== 'object') return false;
|
|
3119
|
+
const exprObj = expr as Record<string, unknown>;
|
|
3120
|
+
|
|
3121
|
+
if (exprObj['expr'] === 'local' && exprObj['name'] === localName) {
|
|
3122
|
+
return true;
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
// Recursively search
|
|
3126
|
+
for (const value of Object.values(exprObj)) {
|
|
3127
|
+
if (typeof value === 'object' && value !== null) {
|
|
3128
|
+
if (containsLocalReference(value, localName)) return true;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
return false;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
// ==================== BarChart Label Position Tests ====================
|
|
3135
|
+
|
|
3136
|
+
describe('BarChart Label Position', () => {
|
|
3137
|
+
it('should NOT have hardcoded x position "50" for labels', () => {
|
|
3138
|
+
const labelProps = findLabelTextProps(ctx.barChart.view);
|
|
3139
|
+
expect(labelProps).not.toBeNull();
|
|
3140
|
+
// Current implementation uses hardcoded "50" - this test should FAIL
|
|
3141
|
+
expect(isHardcodedLiteral(labelProps!['x'], '50')).toBe(false);
|
|
3142
|
+
});
|
|
3143
|
+
|
|
3144
|
+
it('should NOT have hardcoded y position "290" for labels', () => {
|
|
3145
|
+
const labelProps = findLabelTextProps(ctx.barChart.view);
|
|
3146
|
+
expect(labelProps).not.toBeNull();
|
|
3147
|
+
// Current implementation uses hardcoded "290" - this test should FAIL
|
|
3148
|
+
expect(isHardcodedLiteral(labelProps!['y'], '290')).toBe(false);
|
|
3149
|
+
});
|
|
3150
|
+
|
|
3151
|
+
it('should calculate label x position using getBarDimensions', () => {
|
|
3152
|
+
const labelProps = findLabelTextProps(ctx.barChart.view);
|
|
3153
|
+
expect(labelProps).not.toBeNull();
|
|
3154
|
+
// Label x should call getBarDimensions to get bar center position
|
|
3155
|
+
expect(containsMethodCall(labelProps!['x'], 'getBarDimensions')).toBe(true);
|
|
3156
|
+
});
|
|
3157
|
+
|
|
3158
|
+
it('should calculate label y position using _height reference', () => {
|
|
3159
|
+
const labelProps = findLabelTextProps(ctx.barChart.view);
|
|
3160
|
+
expect(labelProps).not.toBeNull();
|
|
3161
|
+
// Label y should reference _height from localState
|
|
3162
|
+
expect(containsLocalReference(labelProps!['y'], '_height')).toBe(true);
|
|
3163
|
+
});
|
|
3164
|
+
});
|
|
3165
|
+
|
|
3166
|
+
// ==================== LineChart Label Position Tests ====================
|
|
3167
|
+
|
|
3168
|
+
describe('LineChart Label Position', () => {
|
|
3169
|
+
it('should NOT have hardcoded x position "50" for labels', () => {
|
|
3170
|
+
const labelProps = findLabelTextProps(ctx.lineChart.view);
|
|
3171
|
+
expect(labelProps).not.toBeNull();
|
|
3172
|
+
// Current implementation uses hardcoded "50" - this test should FAIL
|
|
3173
|
+
expect(isHardcodedLiteral(labelProps!['x'], '50')).toBe(false);
|
|
3174
|
+
});
|
|
3175
|
+
|
|
3176
|
+
it('should NOT have hardcoded y position "290" for labels', () => {
|
|
3177
|
+
const labelProps = findLabelTextProps(ctx.lineChart.view);
|
|
3178
|
+
expect(labelProps).not.toBeNull();
|
|
3179
|
+
// Current implementation uses hardcoded "290" - this test should FAIL
|
|
3180
|
+
expect(isHardcodedLiteral(labelProps!['y'], '290')).toBe(false);
|
|
3181
|
+
});
|
|
3182
|
+
|
|
3183
|
+
it('should calculate label x position using idx variable', () => {
|
|
3184
|
+
const labelProps = findLabelTextProps(ctx.lineChart.view);
|
|
3185
|
+
expect(labelProps).not.toBeNull();
|
|
3186
|
+
// Label x should use idx for index-based positioning
|
|
3187
|
+
expect(containsVarReference(labelProps!['x'], 'idx')).toBe(true);
|
|
3188
|
+
});
|
|
3189
|
+
|
|
3190
|
+
it('should calculate label y position using _height reference', () => {
|
|
3191
|
+
const labelProps = findLabelTextProps(ctx.lineChart.view);
|
|
3192
|
+
expect(labelProps).not.toBeNull();
|
|
3193
|
+
// Label y should reference _height from localState
|
|
3194
|
+
expect(containsLocalReference(labelProps!['y'], '_height')).toBe(true);
|
|
3195
|
+
});
|
|
3196
|
+
});
|
|
3197
|
+
|
|
3198
|
+
// ==================== AreaChart Label Position Tests ====================
|
|
3199
|
+
|
|
3200
|
+
describe('AreaChart Label Position', () => {
|
|
3201
|
+
it('should NOT have hardcoded x position "50" for labels', () => {
|
|
3202
|
+
const labelProps = findLabelTextProps(ctx.areaChart.view);
|
|
3203
|
+
expect(labelProps).not.toBeNull();
|
|
3204
|
+
// Current implementation uses hardcoded "50" - this test should FAIL
|
|
3205
|
+
expect(isHardcodedLiteral(labelProps!['x'], '50')).toBe(false);
|
|
3206
|
+
});
|
|
3207
|
+
|
|
3208
|
+
it('should NOT have hardcoded y position "290" for labels', () => {
|
|
3209
|
+
const labelProps = findLabelTextProps(ctx.areaChart.view);
|
|
3210
|
+
expect(labelProps).not.toBeNull();
|
|
3211
|
+
// Current implementation uses hardcoded "290" - this test should FAIL
|
|
3212
|
+
expect(isHardcodedLiteral(labelProps!['y'], '290')).toBe(false);
|
|
3213
|
+
});
|
|
3214
|
+
|
|
3215
|
+
it('should calculate label x position using idx variable', () => {
|
|
3216
|
+
const labelProps = findLabelTextProps(ctx.areaChart.view);
|
|
3217
|
+
expect(labelProps).not.toBeNull();
|
|
3218
|
+
// Label x should use idx for index-based positioning
|
|
3219
|
+
expect(containsVarReference(labelProps!['x'], 'idx')).toBe(true);
|
|
3220
|
+
});
|
|
3221
|
+
|
|
3222
|
+
it('should calculate label y position using _height reference', () => {
|
|
3223
|
+
const labelProps = findLabelTextProps(ctx.areaChart.view);
|
|
3224
|
+
expect(labelProps).not.toBeNull();
|
|
3225
|
+
// Label y should reference _height from localState
|
|
3226
|
+
expect(containsLocalReference(labelProps!['y'], '_height')).toBe(true);
|
|
3227
|
+
});
|
|
3228
|
+
});
|
|
3229
|
+
|
|
3230
|
+
// ==================== Cross-Chart Label Position Consistency ====================
|
|
3231
|
+
|
|
3232
|
+
describe('Cross-Chart Label Position Consistency', () => {
|
|
3233
|
+
it('all charts should position labels dynamically based on data index', () => {
|
|
3234
|
+
const barLabelProps = findLabelTextProps(ctx.barChart.view);
|
|
3235
|
+
const lineLabelProps = findLabelTextProps(ctx.lineChart.view);
|
|
3236
|
+
const areaLabelProps = findLabelTextProps(ctx.areaChart.view);
|
|
3237
|
+
|
|
3238
|
+
// All should have non-hardcoded x positions
|
|
3239
|
+
expect(barLabelProps).not.toBeNull();
|
|
3240
|
+
expect(lineLabelProps).not.toBeNull();
|
|
3241
|
+
expect(areaLabelProps).not.toBeNull();
|
|
3242
|
+
|
|
3243
|
+
expect(isHardcodedLiteral(barLabelProps!['x'], '50')).toBe(false);
|
|
3244
|
+
expect(isHardcodedLiteral(lineLabelProps!['x'], '50')).toBe(false);
|
|
3245
|
+
expect(isHardcodedLiteral(areaLabelProps!['x'], '50')).toBe(false);
|
|
3246
|
+
});
|
|
3247
|
+
|
|
3248
|
+
it('all charts should position label y using _height reference', () => {
|
|
3249
|
+
const barLabelProps = findLabelTextProps(ctx.barChart.view);
|
|
3250
|
+
const lineLabelProps = findLabelTextProps(ctx.lineChart.view);
|
|
3251
|
+
const areaLabelProps = findLabelTextProps(ctx.areaChart.view);
|
|
3252
|
+
|
|
3253
|
+
// All y positions should reference _height
|
|
3254
|
+
expect(containsLocalReference(barLabelProps!['y'], '_height')).toBe(true);
|
|
3255
|
+
expect(containsLocalReference(lineLabelProps!['y'], '_height')).toBe(true);
|
|
3256
|
+
expect(containsLocalReference(areaLabelProps!['y'], '_height')).toBe(true);
|
|
3257
|
+
});
|
|
3258
|
+
});
|
|
3259
|
+
});
|
|
3260
|
+
});
|