@cdc/waffle-chart 4.25.8 → 4.25.11

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