@constela/ui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/components/accordion/accordion-content.constela.json +20 -0
  3. package/components/accordion/accordion-item.constela.json +20 -0
  4. package/components/accordion/accordion-trigger.constela.json +21 -0
  5. package/components/accordion/accordion.constela.json +18 -0
  6. package/components/accordion/accordion.styles.json +54 -0
  7. package/components/accordion/accordion.test.ts +608 -0
  8. package/components/calendar/calendar.constela.json +195 -0
  9. package/components/calendar/calendar.styles.json +33 -0
  10. package/components/calendar/calendar.test.ts +458 -0
  11. package/components/chart/area-chart.constela.json +482 -0
  12. package/components/chart/bar-chart.constela.json +342 -0
  13. package/components/chart/chart-axis.constela.json +224 -0
  14. package/components/chart/chart-legend.constela.json +82 -0
  15. package/components/chart/chart-tooltip.constela.json +61 -0
  16. package/components/chart/chart.styles.json +183 -0
  17. package/components/chart/chart.test.ts +3260 -0
  18. package/components/chart/donut-chart.constela.json +369 -0
  19. package/components/chart/line-chart.constela.json +380 -0
  20. package/components/chart/pie-chart.constela.json +259 -0
  21. package/components/chart/radar-chart.constela.json +297 -0
  22. package/components/chart/scatter-chart.constela.json +300 -0
  23. package/components/data-table/data-table-cell.constela.json +22 -0
  24. package/components/data-table/data-table-header.constela.json +30 -0
  25. package/components/data-table/data-table-pagination.constela.json +19 -0
  26. package/components/data-table/data-table-row.constela.json +30 -0
  27. package/components/data-table/data-table.constela.json +32 -0
  28. package/components/data-table/data-table.styles.json +84 -0
  29. package/components/data-table/data-table.test.ts +873 -0
  30. package/components/datepicker/datepicker.constela.json +128 -0
  31. package/components/datepicker/datepicker.styles.json +47 -0
  32. package/components/datepicker/datepicker.test.ts +540 -0
  33. package/components/tree/tree-node.constela.json +26 -0
  34. package/components/tree/tree.constela.json +24 -0
  35. package/components/tree/tree.styles.json +50 -0
  36. package/components/tree/tree.test.ts +542 -0
  37. package/components/virtual-scroll/virtual-scroll.constela.json +27 -0
  38. package/components/virtual-scroll/virtual-scroll.styles.json +17 -0
  39. package/components/virtual-scroll/virtual-scroll.test.ts +345 -0
  40. package/dist/index.d.ts +1 -2
  41. package/package.json +3 -3
@@ -0,0 +1,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
+ });