@cdc/waffle-chart 4.25.7 → 4.25.10

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.
@@ -0,0 +1,740 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, userEvent, expect } from 'storybook/test'
3
+ import WaffleChart from '../CdcWaffleChart'
4
+ import {
5
+ performAndAssert,
6
+ waitForPresence,
7
+ waitForAbsence,
8
+ waitForOptionsToPopulate,
9
+ waitForTextContent,
10
+ waitForEditor,
11
+ openAccordion
12
+ } from '@cdc/core/helpers/testing'
13
+
14
+ const meta: Meta<typeof WaffleChart> = {
15
+ title: 'Components/Templates/WaffleChart/Editor Tests',
16
+ component: WaffleChart,
17
+ parameters: {
18
+ layout: 'fullscreen'
19
+ }
20
+ }
21
+
22
+ export default meta
23
+ type Story = StoryObj<typeof WaffleChart>
24
+
25
+ // ============================================================================
26
+ // SHARED HELPERS - Now imported from @cdc/core/helpers/testing
27
+ // ============================================================================
28
+
29
+ /**
30
+ * GENERAL SECTION TESTS
31
+ * Tests all functionality within the General accordion
32
+ */
33
+ export const GeneralSectionTests: Story = {
34
+ args: {
35
+ configUrl: '/packages/waffle-chart/tests/fixtures/test-config.json',
36
+ isEditor: true
37
+ },
38
+ play: async ({ canvasElement }) => {
39
+ const canvas = within(canvasElement)
40
+ await waitForEditor(canvas)
41
+ await openAccordion(canvas, 'General')
42
+
43
+ // ============================================================================
44
+ // TEST 1: Chart Type Change (Waffle to Gauge and back)
45
+ // ============================================================================
46
+ const chartTypeSelect = canvasElement.querySelector('select[name="visualizationType"]') as HTMLSelectElement
47
+ expect(chartTypeSelect).toBeTruthy()
48
+
49
+ let waffleContainer = canvasElement.querySelector('.cove-waffle-chart')
50
+ expect(waffleContainer).toBeTruthy()
51
+ let gaugeContainer = canvasElement.querySelector('.cove-gauge-chart')
52
+ expect(gaugeContainer).toBeFalsy()
53
+
54
+ await userEvent.selectOptions(chartTypeSelect, 'Gauge')
55
+ await waitForPresence('.cove-gauge-chart', canvasElement)
56
+ await waitForAbsence('.cove-waffle-chart', canvasElement)
57
+
58
+ gaugeContainer = canvasElement.querySelector('.cove-gauge-chart')
59
+ expect(gaugeContainer).toBeTruthy()
60
+ waffleContainer = canvasElement.querySelector('.cove-waffle-chart')
61
+ expect(waffleContainer).toBeFalsy()
62
+
63
+ const gaugeSvg = gaugeContainer.querySelector('svg')
64
+ expect(gaugeSvg).toBeTruthy()
65
+
66
+ await userEvent.selectOptions(chartTypeSelect, 'Waffle')
67
+ await waitForPresence('.cove-waffle-chart', canvasElement)
68
+ await waitForAbsence('.cove-gauge-chart', canvasElement)
69
+
70
+ waffleContainer = canvasElement.querySelector('.cove-waffle-chart')
71
+ expect(waffleContainer).toBeTruthy()
72
+ gaugeContainer = canvasElement.querySelector('.cove-gauge-chart')
73
+ expect(gaugeContainer).toBeFalsy()
74
+
75
+ const waffleSvg = waffleContainer.querySelector('svg')
76
+ expect(waffleSvg).toBeTruthy()
77
+ const waffleNodes = waffleSvg.querySelectorAll('rect, circle, path')
78
+ expect(waffleNodes.length).toBeGreaterThan(0)
79
+
80
+ // ============================================================================
81
+ // TEST 2: Title Update
82
+ // Expectation: Header text updates to new string.
83
+ // ============================================================================
84
+ const titleInput = canvas.getByDisplayValue(/test waffle chart title/i)
85
+ await userEvent.clear(titleInput)
86
+ await userEvent.type(titleInput, 'Updated Waffle Chart Title E2E')
87
+ // Poll for header text update
88
+ await performAndAssert(
89
+ 'Title Update',
90
+ () => canvasElement.querySelector('.cove-component__header h2')?.textContent?.trim() || '',
91
+ async () => {}, // action already performed above
92
+ (before, after) => after === 'Updated Waffle Chart Title E2E'
93
+ )
94
+
95
+ const chartTitleHeader = canvasElement.querySelector('.cove-component__header h2')
96
+ expect(chartTitleHeader).toBeTruthy()
97
+ expect(chartTitleHeader.textContent.trim()).toBe('Updated Waffle Chart Title E2E')
98
+
99
+ // ============================================================================
100
+ // TEST 3: Show Title Toggle
101
+ // Expectation: Header region appears / disappears (DOM visibility change).
102
+ // ============================================================================
103
+ const showTitleCheckbox = canvasElement.querySelector('input[name*="showTitle"]') as HTMLInputElement
104
+ const checkboxWrapper = showTitleCheckbox?.closest('.cove-input__checkbox--small')
105
+ expect(showTitleCheckbox).toBeTruthy()
106
+ expect(checkboxWrapper).toBeTruthy()
107
+
108
+ const wasChecked = showTitleCheckbox.checked
109
+ await performAndAssert(
110
+ 'Title Toggle',
111
+ () => showTitleCheckbox.checked,
112
+ async () => {
113
+ await userEvent.click(checkboxWrapper as HTMLElement)
114
+ },
115
+ (before, after) => after === !wasChecked
116
+ )
117
+ expect(showTitleCheckbox.checked).toBe(!wasChecked)
118
+
119
+ const chartTitleHeaderAfterToggle = canvasElement.querySelector('.cove-component__header')
120
+ if (showTitleCheckbox.checked) {
121
+ expect(chartTitleHeaderAfterToggle).toBeTruthy()
122
+ expect(chartTitleHeaderAfterToggle).not.toHaveStyle('display: none')
123
+ } else {
124
+ expect(chartTitleHeaderAfterToggle).toBeFalsy()
125
+ }
126
+
127
+ await performAndAssert(
128
+ 'Title Toggle Reset',
129
+ () => showTitleCheckbox.checked,
130
+ async () => {
131
+ await userEvent.click(checkboxWrapper as HTMLElement)
132
+ },
133
+ (before, after) => after === wasChecked
134
+ )
135
+
136
+ // ============================================================================
137
+ // TEST 4: Message / Content Change
138
+ // Expectation: Primary message text node updates to new content string.
139
+ // ============================================================================
140
+ const contentTextarea = canvasElement.querySelector('textarea[name*="content"]') as HTMLTextAreaElement
141
+ expect(contentTextarea).toBeTruthy()
142
+
143
+ const newContent = 'Updated test message for E2E testing'
144
+ await userEvent.clear(contentTextarea)
145
+ await userEvent.type(contentTextarea, newContent)
146
+ await waitForTextContent(canvasElement.querySelector('.cove-waffle-chart__data--text') as HTMLElement, newContent)
147
+
148
+ const chartContentElement = canvasElement.querySelector('.cove-waffle-chart__data--text')
149
+ expect(chartContentElement).toBeTruthy()
150
+ expect(chartContentElement.textContent.trim()).toBe(newContent)
151
+
152
+ // ============================================================================
153
+ // TEST 5: Subtext / Citation Change
154
+ // Expectation: Subtext element text content updates to new citation string.
155
+ // ============================================================================
156
+ const subtextInput = canvasElement.querySelector('input[name*="subtext"]') as HTMLInputElement
157
+ expect(subtextInput).toBeTruthy()
158
+
159
+ const newSubtext = 'Updated test citation for E2E testing'
160
+ await userEvent.clear(subtextInput)
161
+ await userEvent.type(subtextInput, newSubtext)
162
+ await waitForTextContent(canvasElement.querySelector('.cove-waffle-chart__subtext') as HTMLElement, newSubtext)
163
+
164
+ const chartSubtextElement = canvasElement.querySelector('.cove-waffle-chart__subtext')
165
+ expect(chartSubtextElement).toBeTruthy()
166
+ expect(chartSubtextElement.textContent.trim()).toBe(newSubtext)
167
+ }
168
+ }
169
+ /**
170
+ * DATA SECTION TESTS
171
+ * Tests all functionality within the Data accordion
172
+ */
173
+ export const DataSectionTests: Story = {
174
+ args: {
175
+ configUrl: '/packages/waffle-chart/tests/fixtures/test-config.json',
176
+ isEditor: true
177
+ },
178
+ play: async ({ canvasElement }) => {
179
+ const canvas = within(canvasElement)
180
+ await waitForEditor(canvas)
181
+ await openAccordion(canvas, 'Data')
182
+
183
+ const getPrimaryEl = () => {
184
+ const el = canvasElement.querySelector('.cove-waffle-chart__data--primary') as HTMLElement
185
+ expect(el).toBeTruthy()
186
+ return el
187
+ }
188
+ const getValueText = () => getPrimaryEl().textContent?.trim() || ''
189
+
190
+ // ============================================================================
191
+ // TEST 1: Data Column Change (Deaths <-> Total Overdoses)
192
+ // Expectation: Primary value text changes.
193
+ // ============================================================================
194
+ const dataColumnSelect = canvasElement.querySelector('select[name="dataColumn"]') as HTMLSelectElement
195
+ await performAndAssert(
196
+ 'Data Column Change',
197
+ getValueText,
198
+ async () => {
199
+ const target = dataColumnSelect.value === 'Deaths' ? 'Total Overdoses' : 'Deaths'
200
+ await userEvent.selectOptions(dataColumnSelect, target)
201
+ },
202
+ (before, after) => after !== before
203
+ )
204
+
205
+ // ============================================================================
206
+ // TEST 2: Data Function Change (Sum <-> Mean)
207
+ // Expectation: Primary value text changes.
208
+ // ============================================================================
209
+ const dataFunctionSelect = canvasElement.querySelector('select[name="dataFunction"]') as HTMLSelectElement
210
+ await performAndAssert(
211
+ 'Data Function Change',
212
+ getValueText,
213
+ async () => {
214
+ const target = dataFunctionSelect.value === 'Sum' ? 'Mean (Average)' : 'Sum'
215
+ await userEvent.selectOptions(dataFunctionSelect, target)
216
+ },
217
+ (before, after) => after !== before
218
+ )
219
+
220
+ // ============================================================================
221
+ // TEST 3: Conditional Column & Operator Setup
222
+ // Expectation: No value change asserted (preparing rule).
223
+ // ============================================================================
224
+ const conditionalColumnSelect = canvasElement.querySelector(
225
+ 'select[name="dataConditionalColumn"]'
226
+ ) as HTMLSelectElement
227
+ const conditionalOperatorSelect = canvasElement.querySelector(
228
+ 'select[name="dataConditionalOperator"]'
229
+ ) as HTMLSelectElement
230
+ await userEvent.selectOptions(conditionalColumnSelect, 'Deaths')
231
+ await userEvent.selectOptions(conditionalOperatorSelect, '<')
232
+ // ============================================================================
233
+ // TEST 4: Conditional Value Entry (<5 fallback 1)
234
+ // Expectation: Primary value text changes (filter applied).
235
+ // ============================================================================
236
+ const conditionalValueInput = canvasElement.querySelector(
237
+ 'input[name="null-null-dataConditionalComparate"]'
238
+ ) as HTMLInputElement
239
+ await performAndAssert(
240
+ 'Conditional Value Entry',
241
+ getValueText,
242
+ async () => {
243
+ await userEvent.clear(conditionalValueInput)
244
+ await userEvent.type(conditionalValueInput, '5')
245
+ },
246
+ (before, after) => after !== before
247
+ )
248
+ // ============================================================================
249
+ // TEST 5: Clear Conditional Value (restore dataset)
250
+ // Expectation: Primary value text changes (dataset restored).
251
+ // ============================================================================
252
+ await performAndAssert(
253
+ 'Clear Conditional Value',
254
+ getValueText,
255
+ async () => {
256
+ await userEvent.clear(conditionalValueInput)
257
+ },
258
+ (before, after) => after !== before
259
+ )
260
+
261
+ // ============================================================================
262
+ // TEST 6: Denominator Function Toggle (Sum <-> Mean)
263
+ // Expectation: Primary value text changes.
264
+ // ============================================================================
265
+ const denomFunctionSelect = canvasElement.querySelector('select[name="dataDenomFunction"]') as HTMLSelectElement
266
+ await performAndAssert(
267
+ 'Denominator Function Toggle',
268
+ getValueText,
269
+ async () => {
270
+ const target = denomFunctionSelect.value === 'Sum' ? 'Mean (Average)' : 'Sum'
271
+ await userEvent.selectOptions(denomFunctionSelect, target)
272
+ },
273
+ (before, after) => after !== before
274
+ )
275
+
276
+ // ============================================================================
277
+ // TEST 7: Denominator Column Toggle (Deaths <-> Total Overdoses)
278
+ // Expectation: Primary value text changes.
279
+ // ============================================================================
280
+ const denomColumnSelect = canvasElement.querySelector('select[name="dataDenomColumn"]') as HTMLSelectElement
281
+ await performAndAssert(
282
+ 'Denominator Column Toggle',
283
+ getValueText,
284
+ async () => {
285
+ const target = denomColumnSelect.value === 'Deaths' ? 'Total Overdoses' : 'Deaths'
286
+ await userEvent.selectOptions(denomColumnSelect, target)
287
+ },
288
+ (before, after) => after !== before
289
+ )
290
+
291
+ // ============================================================================
292
+ // TEST 8: Denominator Function Re-Toggle
293
+ // Expectation: Primary value text changes again (stability validation).
294
+ // ============================================================================
295
+ await performAndAssert(
296
+ 'Denominator Function Re-Toggle',
297
+ getValueText,
298
+ async () => {
299
+ const target = denomFunctionSelect.value === 'Sum' ? 'Mean (Average)' : 'Sum'
300
+ await userEvent.selectOptions(denomFunctionSelect, target)
301
+ },
302
+ (before, after) => after !== before
303
+ )
304
+
305
+ // ============================================================================
306
+ // TEST 9: Custom Denominator Checkbox Toggle (enable static)
307
+ // Expectation: Primary value text changes switching to static denominator mode.
308
+ // ============================================================================
309
+ const customDenomCheckbox = canvasElement.querySelector('input[name*="customDenom"]') as HTMLInputElement
310
+ const customDenomWrapper = customDenomCheckbox.closest('.cove-input__checkbox--small') as HTMLElement
311
+ await performAndAssert(
312
+ 'Custom Denominator Toggle',
313
+ getValueText,
314
+ async () => {
315
+ await userEvent.click(customDenomWrapper)
316
+ },
317
+ (before, after) => after !== before
318
+ )
319
+
320
+ // ============================================================================
321
+ // TEST 10: Static Denominator Value -> 150
322
+ // Expectation: Primary value text changes.
323
+ // ============================================================================
324
+ const staticDenomInput = canvasElement.querySelector('input[name="null-null-dataDenom"]') as HTMLInputElement
325
+ await performAndAssert(
326
+ 'Static Denominator 150',
327
+ getValueText,
328
+ async () => {
329
+ await userEvent.clear(staticDenomInput)
330
+ await userEvent.type(staticDenomInput, '150')
331
+ },
332
+ (before, after) => after !== before
333
+ )
334
+
335
+ // ============================================================================
336
+ // TEST 11: Static Denominator Value -> 160
337
+ // Expectation: Primary value text changes again.
338
+ // ============================================================================
339
+ await performAndAssert(
340
+ 'Static Denominator 160',
341
+ getValueText,
342
+ async () => {
343
+ await userEvent.clear(staticDenomInput)
344
+ await userEvent.type(staticDenomInput, '160')
345
+ },
346
+ (before, after) => after !== before
347
+ )
348
+
349
+ // ============================================================================
350
+ // TEST 12: Increase Precision (Round=1)
351
+ // Expectation: Primary value text formatting changes (decimal introduced).
352
+ // ============================================================================
353
+ const roundingInput = canvasElement.querySelector('input[name*="roundToPlace"]') as HTMLInputElement
354
+ await performAndAssert(
355
+ 'Increase Precision',
356
+ getValueText,
357
+ async () => {
358
+ await userEvent.clear(roundingInput)
359
+ await userEvent.type(roundingInput, '1')
360
+ },
361
+ (before, after) => after !== before
362
+ )
363
+
364
+ // ============================================================================
365
+ // TEST 13: Reset Precision (Round=0)
366
+ // Expectation: Primary value text formatting changes again (decimal removed).
367
+ // ============================================================================
368
+ await performAndAssert(
369
+ 'Reset Precision',
370
+ getValueText,
371
+ async () => {
372
+ await userEvent.clear(roundingInput)
373
+ await userEvent.type(roundingInput, '0')
374
+ },
375
+ (before, after) => after !== before
376
+ )
377
+
378
+ // ============================================================================
379
+ // TEST 14: Prefix -> $ (Formatting)
380
+ // Expectation: Value starts with '$' and differs from prior snapshot.
381
+ // ============================================================================
382
+ const prefixInput = canvasElement.querySelector('input[name*="prefix"]') as HTMLInputElement
383
+ await performAndAssert(
384
+ 'Prefix Update',
385
+ getValueText,
386
+ async () => {
387
+ await userEvent.clear(prefixInput)
388
+ await userEvent.type(prefixInput, '$')
389
+ },
390
+ (before, after) => after !== before && after.startsWith('$')
391
+ )
392
+
393
+ // ============================================================================
394
+ // TEST 15: Suffix -> ' deaths'
395
+ // Expectation: Value ends with 'deaths' and differs from prior snapshot.
396
+ // ============================================================================
397
+ const suffixInput = canvasElement.querySelector('input[name*="suffix"]') as HTMLInputElement
398
+ await performAndAssert(
399
+ 'Suffix Update',
400
+ getValueText,
401
+ async () => {
402
+ await userEvent.clear(suffixInput)
403
+ await userEvent.type(suffixInput, ' deaths')
404
+ },
405
+ (before, after) => after !== before && after.endsWith('deaths')
406
+ )
407
+
408
+ // ============================================================================
409
+ // TEST 16: Add Filter (state = Alaska)
410
+ // Expectation: Primary value text changes after filter applied.
411
+ // ============================================================================
412
+ const addFilterButton = Array.from(canvasElement.querySelectorAll('button')).find(
413
+ b => (b as HTMLButtonElement).textContent?.trim() === 'Add Filter'
414
+ ) as HTMLButtonElement
415
+ await performAndAssert(
416
+ 'Add Filter',
417
+ getValueText,
418
+ async () => {
419
+ await userEvent.click(addFilterButton)
420
+
421
+ await waitForPresence('.filters-list .edit-block:last-of-type', canvasElement)
422
+
423
+ const newFilter = canvasElement.querySelector('.filters-list .edit-block:last-of-type') as HTMLElement
424
+ const [colSelect, valSelect] = Array.from(newFilter.querySelectorAll('select')) as HTMLSelectElement[]
425
+ await userEvent.selectOptions(colSelect, 'state')
426
+
427
+ await waitForOptionsToPopulate(valSelect)
428
+
429
+ await userEvent.selectOptions(valSelect, 'Alaska')
430
+ },
431
+ (before, after) => after !== before
432
+ )
433
+ }
434
+ }
435
+
436
+ /**
437
+ * VISUAL SECTION TESTS
438
+ * Tests all functionality within the Visual accordion
439
+ */
440
+ export const VisualSectionTests: Story = {
441
+ args: {
442
+ configUrl: '/packages/waffle-chart/tests/fixtures/test-config.json',
443
+ isEditor: true
444
+ },
445
+ play: async ({ canvasElement }) => {
446
+ const canvas = within(canvasElement)
447
+ await waitForEditor(canvas)
448
+ await openAccordion(canvas, 'Visual')
449
+ // Core helper functions used throughout the visual tests
450
+ const waffleRoot = () => canvasElement.querySelector('.cove-waffle-chart') as HTMLElement
451
+ const contentContainer = () => canvasElement.querySelector('.cove-component__content > div') as HTMLElement
452
+ expect(waffleRoot()).toBeTruthy()
453
+
454
+ // ============================================================================
455
+ // TEST 1: Shape Change
456
+ // Expectation: Exclusive primitive set switches (only rects OR only circles OR only paths)
457
+ // ============================================================================
458
+ const shapeSig = () => {
459
+ const svg = waffleRoot()?.querySelector('svg')
460
+ if (!svg) return { rect: 0, circle: 0, path: 0 }
461
+ const rect = svg.querySelectorAll('rect').length
462
+ const circle = svg.querySelectorAll('circle').length
463
+ // exclude path elements that might not be person nodes? (All paths for person nodes only here)
464
+ const path = svg.querySelectorAll('path').length
465
+ return { rect, circle, path }
466
+ }
467
+
468
+ const shapeSelect = canvas.getByLabelText(/shape/i) as HTMLSelectElement
469
+ const chooseNextNonPersonIfNeeded = () => {
470
+ const order = ['circle', 'square', 'person']
471
+ let idx = order.indexOf(shapeSelect.value)
472
+ let target = order[(idx + 1) % order.length]
473
+ if (target === 'person') target = order[(order.indexOf(target) + 1) % order.length]
474
+ return target
475
+ }
476
+ await performAndAssert(
477
+ 'Shape Change',
478
+ shapeSig,
479
+ async () => {
480
+ const target = chooseNextNonPersonIfNeeded()
481
+ await userEvent.selectOptions(shapeSelect, target)
482
+ },
483
+ (before, after) =>
484
+ (before.rect !== after.rect || before.circle !== after.circle || before.path !== after.path) &&
485
+ [after.rect, after.circle, after.path].filter(c => c > 0).length === 1
486
+ )
487
+
488
+ // ============================================================================
489
+ // TEST 2: Width (nodeWidth) Change
490
+ // Expectation: SVG width attribute changes (numeric resize)
491
+ // ============================================================================
492
+ const svgWidth = () => {
493
+ const svg = waffleRoot()?.querySelector('svg')
494
+ return svg ? parseInt(svg.getAttribute('width') || '0', 10) : 0
495
+ }
496
+
497
+ const widthInput = canvasElement.querySelector('input[name="null-null-nodeWidth"]') as HTMLInputElement
498
+ expect(widthInput).toBeTruthy()
499
+ await performAndAssert(
500
+ 'Width Change',
501
+ svgWidth,
502
+ async () => {
503
+ const cur = parseInt(widthInput.value)
504
+ const next = isNaN(cur) ? 12 : cur + 1
505
+ await userEvent.clear(widthInput)
506
+ await userEvent.type(widthInput, String(next))
507
+ widthInput.blur()
508
+ },
509
+ (before, after) => after !== before && after > 0
510
+ )
511
+
512
+ // ============================================================================
513
+ // TEST 3: Spacer (nodeSpacer) Change
514
+ // Expectation: Inter-node spacing (delta between first two nodes) changes.
515
+ // ============================================================================
516
+ const nodeSpacing = () => {
517
+ const svg = waffleRoot()?.querySelector('svg')
518
+ if (!svg) return 0
519
+ const node = svg.querySelectorAll('rect, circle')
520
+ if (node.length < 2) return 0
521
+ // Use x vs cx
522
+ const a = (node[0] as any).getAttribute('x') || (node[0] as any).getAttribute('cx') || '0'
523
+ const b = (node[1] as any).getAttribute('x') || (node[1] as any).getAttribute('cx') || '0'
524
+ return Math.abs(parseFloat(b) - parseFloat(a))
525
+ }
526
+
527
+ const spacerInput = canvasElement.querySelector('input[name="null-null-nodeSpacer"]') as HTMLInputElement
528
+ expect(spacerInput).toBeTruthy()
529
+ await performAndAssert(
530
+ 'Spacer Change',
531
+ nodeSpacing,
532
+ async () => {
533
+ const cur = parseInt(spacerInput.value)
534
+ const next = isNaN(cur) ? 3 : cur === 2 ? 4 : 2
535
+ await userEvent.clear(spacerInput)
536
+ await userEvent.type(spacerInput, String(next))
537
+ spacerInput.blur()
538
+ },
539
+ (before, after) => after !== before
540
+ )
541
+
542
+ // ============================================================================
543
+ // TEST 4: Layout (orientation) Change
544
+ // Expectation: Class 'cove-waffle-chart--verical' toggles presence.
545
+ // ============================================================================
546
+ const layoutSelect = canvas.getByLabelText(/layout/i) as HTMLSelectElement
547
+ await performAndAssert(
548
+ 'Layout Change',
549
+ () => ({ hasClass: waffleRoot().classList.contains('cove-waffle-chart--verical'), width: svgWidth() }),
550
+ async () => {
551
+ const target = layoutSelect.value === 'horizontal' ? 'vertical' : 'horizontal'
552
+ await userEvent.selectOptions(layoutSelect, target)
553
+ },
554
+ (before, after) =>
555
+ before.hasClass !== after.hasClass || // primary signal
556
+ (after.width !== before.width && after.width > 0), // fallback: width change due to ratio recalculation
557
+ expect
558
+ )
559
+
560
+ // ============================================================================
561
+ // TEST 5: Data Point Font Size Change
562
+ // Expectation: Inline style font-size on primary value changes.
563
+ // ============================================================================
564
+ const primaryEl = () => waffleRoot()?.querySelector('.cove-waffle-chart__data--primary') as HTMLElement | null
565
+ const inlinePrimaryFontSize = () => primaryEl()?.style.fontSize || ''
566
+
567
+ const pointFontSizeInput = canvasElement.querySelector('input[name="null-null-fontSize"]') as HTMLInputElement
568
+ expect(pointFontSizeInput).toBeTruthy()
569
+ await performAndAssert(
570
+ 'Data Point Font Size Change',
571
+ inlinePrimaryFontSize,
572
+ async () => {
573
+ const cur = parseInt(pointFontSizeInput.value)
574
+ const next = isNaN(cur) ? 45 : cur === 40 ? 42 : 40
575
+ await userEvent.clear(pointFontSizeInput)
576
+ await userEvent.type(pointFontSizeInput, String(next))
577
+ pointFontSizeInput.blur()
578
+ },
579
+ (before, after) => before !== after && after.length > 0
580
+ )
581
+
582
+ // ============================================================================
583
+ // TEST 6: Overall Font Size Change
584
+ // Expectation: font-small|font-medium|font-large class changes on waffle root.
585
+ // ============================================================================
586
+ const overallFontClass = () => {
587
+ const root = waffleRoot()
588
+ if (!root) return ''
589
+ if (root.classList.contains('font-small')) return 'font-small'
590
+ if (root.classList.contains('font-medium')) return 'font-medium'
591
+ if (root.classList.contains('font-large')) return 'font-large'
592
+ return ''
593
+ }
594
+
595
+ const overallFontSizeSelect = canvas.getByLabelText(/overall font size/i) as HTMLSelectElement
596
+ await performAndAssert(
597
+ 'Overall Font Size Change',
598
+ overallFontClass,
599
+ async () => {
600
+ const cycle = ['small', 'medium', 'large']
601
+ const idx = cycle.indexOf(overallFontSizeSelect.value)
602
+ const target = cycle[(idx + 1) % cycle.length]
603
+ await userEvent.selectOptions(overallFontSizeSelect, target)
604
+ },
605
+ (before, after) => before !== after && ['font-small', 'font-medium', 'font-large'].includes(after)
606
+ )
607
+
608
+ // ============================================================================
609
+ // TEST 7: Theme Palette Change
610
+ // Expectation: Fill color of first node changes.
611
+ // ============================================================================
612
+ const firstNodeFill = () => {
613
+ const svg = waffleRoot()?.querySelector('svg')
614
+ if (!svg) return ''
615
+ const node = svg.querySelector('rect, circle, path') as
616
+ | (SVGElement & { getAttribute(name: string): string })
617
+ | null
618
+ return node?.getAttribute('fill') || ''
619
+ }
620
+
621
+ const themeButtons = Array.from(canvasElement.querySelectorAll('.color-palette li')) as HTMLElement[]
622
+ expect(themeButtons.length).toBeGreaterThan(1)
623
+ await performAndAssert(
624
+ 'Theme Change',
625
+ firstNodeFill,
626
+ async () => {
627
+ const selected = themeButtons.find(b => b.classList.contains('selected'))
628
+ const next = themeButtons.find(b => !b.classList.contains('selected')) || themeButtons[0]
629
+ if (next !== selected) await userEvent.click(next)
630
+ else await userEvent.click(themeButtons[(themeButtons.indexOf(next) + 1) % themeButtons.length])
631
+ },
632
+ (before, after) => after !== before && after.length > 0
633
+ )
634
+
635
+ // ============================================================================
636
+ // TEST 8: Display Border Toggle
637
+ // Expectation: Class 'no-borders' on content container toggles.
638
+ // ============================================================================
639
+ const contentClassSig = () => Array.from(contentContainer().classList).sort().join(' ')
640
+
641
+ const borderCheckbox = canvasElement.querySelector('input[name="visual-null-border"]') as HTMLInputElement
642
+ expect(borderCheckbox).toBeTruthy()
643
+ const borderWrapper = borderCheckbox.closest('.cove-input__checkbox--small') as HTMLElement
644
+ expect(borderWrapper).toBeTruthy()
645
+ const borderStyleSig = () => {
646
+ const el = contentContainer()
647
+ const cs = getComputedStyle(el)
648
+ return {
649
+ classes: contentClassSig(),
650
+ top: cs.borderTopWidth + ' ' + cs.borderTopStyle + ' ' + cs.borderTopColor,
651
+ right: cs.borderRightWidth + ' ' + cs.borderRightStyle + ' ' + cs.borderRightColor,
652
+ color: cs.borderColor
653
+ }
654
+ }
655
+ await performAndAssert(
656
+ 'Border Toggle',
657
+ borderStyleSig,
658
+ async () => {
659
+ await userEvent.click(borderWrapper)
660
+ },
661
+ (before, after) =>
662
+ before.classes !== after.classes ||
663
+ before.top !== after.top ||
664
+ before.right !== after.right ||
665
+ before.color !== after.color
666
+ )
667
+
668
+ // ============================================================================
669
+ // TEST 9: Theme Border Color Toggle
670
+ // Expectation: Class 'component--has-borderColorTheme' toggles.
671
+ // ============================================================================
672
+ const borderColorThemeCheckbox = canvasElement.querySelector(
673
+ 'input[name="visual-null-borderColorTheme"]'
674
+ ) as HTMLInputElement
675
+ expect(borderColorThemeCheckbox).toBeTruthy()
676
+ const borderColorThemeWrapper = borderColorThemeCheckbox.closest('.cove-input__checkbox--small') as HTMLElement
677
+ expect(borderColorThemeWrapper).toBeTruthy()
678
+ await performAndAssert(
679
+ 'Border Color Theme Toggle',
680
+ contentClassSig,
681
+ async () => {
682
+ await userEvent.click(borderColorThemeWrapper)
683
+ },
684
+ (before, after) => before !== after && (after.includes('borderColorTheme') || before.includes('borderColorTheme'))
685
+ )
686
+
687
+ // ============================================================================
688
+ // TEST 10: Accent Style Toggle
689
+ // Expectation: Class 'component--has-accent' toggles.
690
+ // ============================================================================
691
+ const accentCheckbox = canvasElement.querySelector('input[name="visual-null-accent"]') as HTMLInputElement
692
+ expect(accentCheckbox).toBeTruthy()
693
+ const accentWrapper = accentCheckbox.closest('.cove-input__checkbox--small') as HTMLElement
694
+ expect(accentWrapper).toBeTruthy()
695
+ await performAndAssert(
696
+ 'Accent Toggle',
697
+ contentClassSig,
698
+ async () => {
699
+ await userEvent.click(accentWrapper)
700
+ },
701
+ (before, after) => before !== after
702
+ )
703
+
704
+ // ============================================================================
705
+ // TEST 11: Theme Background Color Toggle
706
+ // Expectation: Class 'component--has-background' toggles.
707
+ // ============================================================================
708
+ const backgroundCheckbox = canvasElement.querySelector('input[name="visual-null-background"]') as HTMLInputElement
709
+ expect(backgroundCheckbox).toBeTruthy()
710
+ const backgroundWrapper = backgroundCheckbox.closest('.cove-input__checkbox--small') as HTMLElement
711
+ expect(backgroundWrapper).toBeTruthy()
712
+ await performAndAssert(
713
+ 'Background Toggle',
714
+ contentClassSig,
715
+ async () => {
716
+ await userEvent.click(backgroundWrapper)
717
+ },
718
+ (before, after) => before !== after
719
+ )
720
+
721
+ // ============================================================================
722
+ // TEST 12: Hide Background Color Toggle
723
+ // Expectation: Class 'component--hideBackgroundColor' toggles.
724
+ // ============================================================================
725
+ const hideBackgroundCheckbox = canvasElement.querySelector(
726
+ 'input[name="visual-null-hideBackgroundColor"]'
727
+ ) as HTMLInputElement
728
+ expect(hideBackgroundCheckbox).toBeTruthy()
729
+ const hideBackgroundWrapper = hideBackgroundCheckbox.closest('.cove-input__checkbox--small') as HTMLElement
730
+ expect(hideBackgroundWrapper).toBeTruthy()
731
+ await performAndAssert(
732
+ 'Hide Background Toggle',
733
+ contentClassSig,
734
+ async () => {
735
+ await userEvent.click(hideBackgroundWrapper)
736
+ },
737
+ (before, after) => before !== after
738
+ )
739
+ }
740
+ }