@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.
- package/dist/cdcwafflechart.js +4449 -4173
- package/package.json +13 -4
- package/src/CdcWaffleChart.tsx +3 -1
- package/src/_stories/WaffleChart.Editor.stories.tsx +740 -0
- package/src/_stories/WaffleChart.stories.tsx +1 -1
- package/src/components/EditorPanel.jsx +2 -2
- package/src/index.jsx +5 -2
- package/src/test/CdcWaffleChart.test.jsx +8 -3
- package/tests/fixtures/example-data.json +50 -0
- package/tests/fixtures/test-config.json +41 -0
- package/vite.config.js +1 -1
- package/src/coreStyles_wafflechart.scss +0 -3
|
@@ -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
|
+
}
|