@cdc/map 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.
- package/.claude/agents/typescript-organizer.md +118 -0
- package/.claude/settings.local.json +30 -0
- package/dist/{cdcmap-fce76882.es.js → cdcmap-BnB1QM5d.es.js} +6 -13
- package/dist/{cdcmap-c55ac1ea.es.js → cdcmap-D6CG2-Hb.es.js} +5 -12
- package/dist/{cdcmap-31a33da1.es.js → cdcmap-MXgURbdZ.es.js} +6 -13
- package/dist/{cdcmap-1a1724a1.es.js → cdcmap-dgT_1dIT.es.js} +136 -151
- package/dist/cdcmap.js +56991 -53706
- package/examples/example-city-state.json +9 -1
- package/examples/multi-country-centering.json +45 -0
- package/examples/private/c.json +290 -0
- package/examples/private/canvas-city-hover.json +787 -0
- package/examples/private/colors-2.json +221 -0
- package/examples/private/colors.json +221 -0
- package/examples/private/d.json +345 -0
- package/examples/private/g.json +1 -0
- package/examples/private/h.json +105911 -0
- package/examples/private/measles-data.json +378 -0
- package/examples/private/measles.json +211 -0
- package/examples/private/north-dakota.json +1132 -0
- package/examples/private/state-with-pattern.json +883 -0
- package/index.html +36 -34
- package/package.json +26 -5
- package/src/CdcMap.tsx +23 -8
- package/src/CdcMapComponent.tsx +238 -308
- package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
- package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
- package/src/_stories/CdcMap.Editor.stories.tsx +3371 -0
- package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
- package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
- package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
- package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
- package/src/_stories/CdcMap.Table.stories.tsx +2 -2
- package/src/_stories/CdcMap.stories.tsx +37 -9
- package/src/_stories/GoogleMap.stories.tsx +2 -2
- package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
- package/src/_stories/_mock/column-wrap-test.json +265 -0
- package/src/_stories/_mock/equal-number.json +1109 -0
- package/src/_stories/_mock/multi-country-hide.json +78 -0
- package/src/_stories/_mock/multi-country.json +95 -0
- package/src/_stories/_mock/multi-state.json +887 -20403
- package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
- package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
- package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
- package/src/_stories/_mock/us-bubble-cities.json +306 -0
- package/src/_stories/_mock/usa-state-gradient.json +2 -4
- package/src/components/BubbleList.tsx +17 -13
- package/src/components/CityList.tsx +85 -107
- package/src/components/EditorPanel/components/EditorPanel.tsx +787 -709
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +58 -95
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +34 -42
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +354 -0
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/Geo.tsx +22 -3
- package/src/components/Legend/components/Legend.tsx +76 -40
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
- package/src/components/Legend/components/index.scss +1 -1
- package/src/components/MapContainer.tsx +52 -0
- package/src/components/MapControls.tsx +44 -0
- package/src/components/NavigationMenu.tsx +27 -15
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
- package/src/components/SmallMultiples/SmallMultiples.css +32 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
- package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
- package/src/components/SmallMultiples/index.tsx +3 -0
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +36 -4
- package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +23 -4
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
- package/src/components/UsaMap/components/UsaMap.County.tsx +123 -37
- package/src/components/UsaMap/components/UsaMap.Region.tsx +36 -5
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +30 -10
- package/src/components/UsaMap/components/UsaMap.State.tsx +53 -12
- package/src/components/UsaMap/helpers/map.ts +4 -4
- package/src/components/UsaMap/helpers/shapes.ts +9 -6
- package/src/components/WorldMap/WorldMap.tsx +193 -35
- package/src/components/ZoomControls.tsx +6 -9
- package/src/context/LegendMemoContext.tsx +30 -0
- package/src/context.ts +1 -40
- package/src/data/initial-state.js +153 -130
- package/src/data/supported-geos.js +25 -78
- package/src/helpers/addUIDs.ts +13 -2
- package/src/helpers/applyColorToLegend.ts +140 -20
- package/src/helpers/applyLegendToRow.ts +10 -6
- package/src/helpers/componentHelpers.ts +8 -0
- package/src/helpers/constants.ts +12 -14
- package/src/helpers/dataTableHelpers.ts +6 -0
- package/src/helpers/displayGeoName.ts +18 -3
- package/src/helpers/generateRuntimeLegend.ts +44 -10
- package/src/helpers/generateRuntimeLegendHash.ts +4 -2
- package/src/helpers/getColumnNames.ts +1 -1
- package/src/helpers/getCountriesPicked.ts +103 -0
- package/src/helpers/getMapContainerClasses.ts +7 -0
- package/src/helpers/getPatternForRow.ts +33 -0
- package/src/helpers/getStatesPicked.ts +8 -5
- package/src/helpers/index.ts +3 -3
- package/src/helpers/isLegendItemDisabled.ts +16 -0
- package/src/helpers/mapObserverHelpers.ts +40 -0
- package/src/helpers/resetLegendToggles.ts +3 -2
- package/src/helpers/smallMultiplesHelpers.ts +359 -0
- package/src/helpers/tests/titleCase.test.ts +76 -0
- package/src/helpers/titleCase.ts +13 -13
- package/src/helpers/toggleLegendActive.ts +6 -11
- package/src/helpers/urlDataHelpers.ts +70 -0
- package/src/hooks/useCountryZoom.tsx +241 -0
- package/src/hooks/useGeoClickHandler.ts +36 -2
- package/src/hooks/useLegendMemo.ts +17 -0
- package/src/hooks/useMapLayers.tsx +5 -4
- package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
- package/src/hooks/useResizeObserver.ts +5 -2
- package/src/hooks/useStateZoom.tsx +30 -8
- package/src/hooks/useSynchronizedGeographies.ts +56 -0
- package/src/hooks/useTooltip.ts +1 -2
- package/src/index.jsx +1 -2
- package/src/scss/editor-panel.scss +4 -440
- package/src/scss/main.scss +1 -1
- package/src/scss/map.scss +12 -15
- package/src/store/map.actions.ts +7 -7
- package/src/store/map.reducer.ts +17 -6
- package/src/test/CdcMap.test.jsx +11 -0
- package/src/types/MapConfig.ts +46 -18
- package/src/types/MapContext.ts +6 -7
- package/src/types/runtimeLegend.ts +17 -1
- package/vite.config.js +2 -7
- package/vitest.config.ts +16 -0
- package/src/components/DataTable.tsx +0 -385
- package/src/components/EditorPanel/components/Inputs.tsx +0 -59
- package/src/coreStyles_map.scss +0 -3
- package/src/helpers/colorDistributions.ts +0 -12
- package/src/helpers/generateColorsArray.ts +0 -14
- package/src/helpers/tests/generateColorsArray.test.ts +0 -18
- package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
- package/src/hooks/useActiveElement.ts +0 -19
- package/src/scss/mixins.scss +0 -47
- package/src/types/Annotations.ts +0 -24
- /package/dist/{cdcmap-548642e6.es.js → cdcmap-Ct2SB0vL.es.js} +0 -0
|
@@ -0,0 +1,3371 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { within, userEvent, expect } from 'storybook/test'
|
|
3
|
+
import CdcMap from '../CdcMap'
|
|
4
|
+
import usaStateGradientConfig from './_mock/usa-state-gradient.json'
|
|
5
|
+
import multiCountryConfig from './_mock/multi-country.json'
|
|
6
|
+
import wastewaterMapSmallMultiplesConfig from './_mock/small_multiples/wastewater-map-small-multiples.json'
|
|
7
|
+
import { performAndAssert, waitForEditor, waitForPresence, openAccordion } from '@cdc/core/helpers/testing'
|
|
8
|
+
|
|
9
|
+
type Story = StoryObj<typeof CdcMap>
|
|
10
|
+
|
|
11
|
+
const mapMeta: Meta<typeof CdcMap> = {
|
|
12
|
+
title: 'Components/Templates/Map/Editor Tests',
|
|
13
|
+
component: CdcMap,
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: 'fullscreen'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default mapMeta
|
|
20
|
+
|
|
21
|
+
const DEFAULT_ARGS = {
|
|
22
|
+
isEditor: true,
|
|
23
|
+
config: usaStateGradientConfig
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const TypeSectionTests: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
...DEFAULT_ARGS
|
|
29
|
+
},
|
|
30
|
+
play: async ({ canvasElement }) => {
|
|
31
|
+
const canvas = within(canvasElement)
|
|
32
|
+
|
|
33
|
+
await waitForEditor(canvas)
|
|
34
|
+
await waitForPresence('.map-container', canvasElement)
|
|
35
|
+
|
|
36
|
+
await openAccordion(canvas, 'Type')
|
|
37
|
+
|
|
38
|
+
const getMapContainerState = () => {
|
|
39
|
+
const container = canvasElement.querySelector('.map-container') as HTMLElement | null
|
|
40
|
+
const svg = canvasElement.querySelector('svg') as SVGElement | null
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
classes: container ? Array.from(container.classList) : [],
|
|
44
|
+
hasSvg: Boolean(svg),
|
|
45
|
+
isBubble: container?.classList.contains('bubble') ?? false,
|
|
46
|
+
geoType: container?.classList.contains('us-county')
|
|
47
|
+
? 'us-county'
|
|
48
|
+
: container?.classList.contains('us')
|
|
49
|
+
? 'us'
|
|
50
|
+
: container?.classList.contains('single-state')
|
|
51
|
+
? 'single-state'
|
|
52
|
+
: container?.classList.contains('world')
|
|
53
|
+
? 'world'
|
|
54
|
+
: container?.classList.contains('us-region')
|
|
55
|
+
? 'us-region'
|
|
56
|
+
: 'unknown'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ==========================================================================
|
|
61
|
+
// TEST: Geography buttons change the geoType class on the map container
|
|
62
|
+
// ==========================================================================
|
|
63
|
+
const geoButtons = Array.from(canvasElement.querySelectorAll('.geo-buttons button')) as HTMLButtonElement[]
|
|
64
|
+
expect(geoButtons.length).toBeGreaterThanOrEqual(3)
|
|
65
|
+
|
|
66
|
+
const geographyButtonMap = geoButtons.reduce<Record<string, HTMLButtonElement>>((acc, button) => {
|
|
67
|
+
const label = button.textContent?.trim() ?? ''
|
|
68
|
+
acc[label.toLowerCase()] = button
|
|
69
|
+
return acc
|
|
70
|
+
}, {})
|
|
71
|
+
|
|
72
|
+
const targetButtons = [
|
|
73
|
+
{ label: 'world', targetClasses: ['world'] },
|
|
74
|
+
{ label: 'united states', targetClasses: ['us'] }
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
for (const { label, targetClasses } of targetButtons) {
|
|
78
|
+
const button = geographyButtonMap[label]
|
|
79
|
+
expect(button).toBeTruthy()
|
|
80
|
+
|
|
81
|
+
await performAndAssert(
|
|
82
|
+
`GeoType → ${label}`,
|
|
83
|
+
getMapContainerState,
|
|
84
|
+
async () => {
|
|
85
|
+
await userEvent.click(button)
|
|
86
|
+
},
|
|
87
|
+
(before, after) =>
|
|
88
|
+
targetClasses.some(
|
|
89
|
+
targetClass => !before.classes.includes(targetClass) && after.classes.includes(targetClass)
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ==========================================================================
|
|
95
|
+
// TEST: Geography subtype select toggles county/state classes
|
|
96
|
+
// ==========================================================================
|
|
97
|
+
const subtypeSelect = canvas.getByLabelText(/Geography Subtype/i) as HTMLSelectElement
|
|
98
|
+
const subtypeValues = Array.from(subtypeSelect.options).map(option => option.value)
|
|
99
|
+
expect(subtypeValues).toContain('us-county')
|
|
100
|
+
|
|
101
|
+
await performAndAssert(
|
|
102
|
+
'GeoType Subtype → US County',
|
|
103
|
+
getMapContainerState,
|
|
104
|
+
async () => {
|
|
105
|
+
await userEvent.selectOptions(subtypeSelect, 'us-county')
|
|
106
|
+
},
|
|
107
|
+
(before, after) => !before.classes.includes('us-county') && after.classes.includes('us-county')
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
await performAndAssert(
|
|
111
|
+
'GeoType Subtype → Reset',
|
|
112
|
+
getMapContainerState,
|
|
113
|
+
async () => {
|
|
114
|
+
await userEvent.selectOptions(subtypeSelect, 'us')
|
|
115
|
+
},
|
|
116
|
+
(before, after) => before.classes.includes('us-county') && after.classes.includes('us')
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
// ==========================================================================
|
|
120
|
+
// TEST: Map Type select toggles classes/data representation
|
|
121
|
+
// ==========================================================================
|
|
122
|
+
const typeSelect = canvas.getByLabelText(/Map Type/i) as HTMLSelectElement
|
|
123
|
+
const initialType = typeSelect.value
|
|
124
|
+
const mapTypeOptions = Array.from(typeSelect.options).map(option => option.value)
|
|
125
|
+
expect(mapTypeOptions).toContain('navigation')
|
|
126
|
+
|
|
127
|
+
await performAndAssert(
|
|
128
|
+
'Map Type → Navigation',
|
|
129
|
+
getMapContainerState,
|
|
130
|
+
async () => {
|
|
131
|
+
await userEvent.selectOptions(typeSelect, 'navigation')
|
|
132
|
+
},
|
|
133
|
+
(before, after) => !before.classes.includes('navigation') && after.classes.includes('navigation')
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
await performAndAssert(
|
|
137
|
+
'Map Type → Reset',
|
|
138
|
+
getMapContainerState,
|
|
139
|
+
async () => {
|
|
140
|
+
await userEvent.selectOptions(typeSelect, initialType)
|
|
141
|
+
},
|
|
142
|
+
(before, after) =>
|
|
143
|
+
before.classes.includes('navigation') &&
|
|
144
|
+
!after.classes.includes('navigation') &&
|
|
145
|
+
after.classes.includes(initialType)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
// ==========================================================================
|
|
149
|
+
// TEST: Data Classification Type radio buttons
|
|
150
|
+
// Verifies: Legend structure changes between numeric/quantitative and categorical
|
|
151
|
+
// ==========================================================================
|
|
152
|
+
const numericRadio = canvasElement.querySelector('input[type="radio"][value="equalnumber"]') as HTMLInputElement
|
|
153
|
+
const categoryRadio = canvasElement.querySelector('input[type="radio"][value="category"]') as HTMLInputElement
|
|
154
|
+
expect(numericRadio).toBeTruthy()
|
|
155
|
+
expect(categoryRadio).toBeTruthy()
|
|
156
|
+
|
|
157
|
+
const getLegendStructure = () => {
|
|
158
|
+
const legend = canvasElement.querySelector('.map-legend, .legend-container') as HTMLElement | null
|
|
159
|
+
const legendItems = canvasElement.querySelectorAll('.legend-item, .legend-container > div, .legend li')
|
|
160
|
+
const legendRects = legend?.querySelectorAll('rect')
|
|
161
|
+
const legendText = legend?.querySelectorAll('text')
|
|
162
|
+
return {
|
|
163
|
+
legendExists: Boolean(legend),
|
|
164
|
+
legendItemCount: legendItems.length,
|
|
165
|
+
legendRectCount: legendRects?.length || 0,
|
|
166
|
+
legendTextCount: legendText?.length || 0,
|
|
167
|
+
legendFullHTML: legend?.innerHTML || ''
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await performAndAssert(
|
|
172
|
+
'Classification Type → Category',
|
|
173
|
+
getLegendStructure,
|
|
174
|
+
async () => {
|
|
175
|
+
await userEvent.click(categoryRadio)
|
|
176
|
+
},
|
|
177
|
+
(before, after) =>
|
|
178
|
+
before.legendRectCount !== after.legendRectCount &&
|
|
179
|
+
before.legendTextCount !== after.legendTextCount &&
|
|
180
|
+
before.legendFullHTML !== after.legendFullHTML
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
await performAndAssert(
|
|
184
|
+
'Classification Type → Numeric',
|
|
185
|
+
getLegendStructure,
|
|
186
|
+
async () => {
|
|
187
|
+
await userEvent.click(numericRadio)
|
|
188
|
+
},
|
|
189
|
+
(before, after) =>
|
|
190
|
+
before.legendRectCount !== after.legendRectCount &&
|
|
191
|
+
before.legendTextCount !== after.legendTextCount &&
|
|
192
|
+
before.legendFullHTML !== after.legendFullHTML
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
// ==========================================================================
|
|
196
|
+
// TEST: Display As Hex Map checkbox
|
|
197
|
+
// Verifies: Hexagon SVG polygons appear/disappear in map visualization
|
|
198
|
+
// ==========================================================================
|
|
199
|
+
const hexLabel = Array.from(canvasElement.querySelectorAll('label')).find(label =>
|
|
200
|
+
label.textContent?.includes('Display As Hex Map')
|
|
201
|
+
)
|
|
202
|
+
const actualHexCheckbox = hexLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
|
203
|
+
expect(actualHexCheckbox).toBeTruthy()
|
|
204
|
+
|
|
205
|
+
const getHexVisualization = () => {
|
|
206
|
+
const hexElements = canvasElement.querySelectorAll('.territory-wrapper--hex, polygon[points*="22 0 44 12.702"]')
|
|
207
|
+
return {
|
|
208
|
+
hexElementCount: hexElements.length
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await performAndAssert(
|
|
213
|
+
'Display As Hex Map → Enable',
|
|
214
|
+
getHexVisualization,
|
|
215
|
+
async () => {
|
|
216
|
+
await userEvent.click(actualHexCheckbox)
|
|
217
|
+
},
|
|
218
|
+
(before, after) => before.hexElementCount === 0 && after.hexElementCount > 0
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
await performAndAssert(
|
|
222
|
+
'Display As Hex Map → Disable',
|
|
223
|
+
getHexVisualization,
|
|
224
|
+
async () => {
|
|
225
|
+
await userEvent.click(actualHexCheckbox)
|
|
226
|
+
},
|
|
227
|
+
(before, after) => before.hexElementCount > 0 && after.hexElementCount === 0
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
// ==========================================================================
|
|
231
|
+
// TEST: Show state labels checkbox
|
|
232
|
+
// Verifies: State abbreviation text elements appear/disappear on map SVG
|
|
233
|
+
// ==========================================================================
|
|
234
|
+
const stateLabelsCheckbox = canvas.getByLabelText(/Show state labels/i) as HTMLInputElement
|
|
235
|
+
expect(stateLabelsCheckbox).toBeTruthy()
|
|
236
|
+
|
|
237
|
+
const getStateLabelsVisual = () => {
|
|
238
|
+
const mapSvg = canvasElement.querySelector('svg[role="img"]')
|
|
239
|
+
const textElements = mapSvg?.querySelectorAll('text')
|
|
240
|
+
// State labels are text elements with short state abbreviations (2 chars)
|
|
241
|
+
const stateLabelTexts = Array.from(textElements || []).filter(text => {
|
|
242
|
+
const content = text.textContent?.trim()
|
|
243
|
+
return content && content.length === 2 && /^[A-Z]{2}$/.test(content)
|
|
244
|
+
})
|
|
245
|
+
return {
|
|
246
|
+
stateLabelCount: stateLabelTexts.length
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await performAndAssert(
|
|
251
|
+
'Show State Labels → Enable',
|
|
252
|
+
getStateLabelsVisual,
|
|
253
|
+
async () => {
|
|
254
|
+
await userEvent.click(stateLabelsCheckbox)
|
|
255
|
+
},
|
|
256
|
+
(before, after) => before.stateLabelCount === 0 && after.stateLabelCount > 0
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
await performAndAssert(
|
|
260
|
+
'Show State Labels → Disable',
|
|
261
|
+
getStateLabelsVisual,
|
|
262
|
+
async () => {
|
|
263
|
+
await userEvent.click(stateLabelsCheckbox)
|
|
264
|
+
},
|
|
265
|
+
(before, after) => before.stateLabelCount > 0 && after.stateLabelCount === 0
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
// ==========================================================================
|
|
269
|
+
// TEST: Show All Territories checkbox
|
|
270
|
+
// Verifies: Territory SVG elements appear/disappear in visualization
|
|
271
|
+
// ==========================================================================
|
|
272
|
+
const territoriesLabel = Array.from(canvasElement.querySelectorAll('label')).find(label =>
|
|
273
|
+
label.textContent?.includes('Show All Territories')
|
|
274
|
+
)
|
|
275
|
+
const territoriesCheckbox = territoriesLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
|
276
|
+
expect(territoriesCheckbox).toBeTruthy()
|
|
277
|
+
|
|
278
|
+
const getTerritoriesVisual = () => {
|
|
279
|
+
const territorySection = canvasElement.querySelector('.territories')
|
|
280
|
+
const territorySvgs = territorySection?.querySelectorAll('svg')
|
|
281
|
+
return {
|
|
282
|
+
territorySvgCount: territorySvgs?.length || 0,
|
|
283
|
+
hasTerritorySection: Boolean(territorySection)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await performAndAssert(
|
|
288
|
+
'Show All Territories → Enable',
|
|
289
|
+
getTerritoriesVisual,
|
|
290
|
+
async () => {
|
|
291
|
+
await userEvent.click(territoriesCheckbox)
|
|
292
|
+
},
|
|
293
|
+
(before, after) => before.territorySvgCount !== after.territorySvgCount
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
await performAndAssert(
|
|
297
|
+
'Show All Territories → Disable',
|
|
298
|
+
getTerritoriesVisual,
|
|
299
|
+
async () => {
|
|
300
|
+
await userEvent.click(territoriesCheckbox)
|
|
301
|
+
},
|
|
302
|
+
(before, after) => before.territorySvgCount !== after.territorySvgCount
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export const GeneralSectionTests: Story = {
|
|
308
|
+
args: {
|
|
309
|
+
...DEFAULT_ARGS
|
|
310
|
+
},
|
|
311
|
+
play: async ({ canvasElement }) => {
|
|
312
|
+
const canvas = within(canvasElement)
|
|
313
|
+
|
|
314
|
+
await waitForEditor(canvas)
|
|
315
|
+
await waitForPresence('.map-container', canvasElement)
|
|
316
|
+
|
|
317
|
+
await openAccordion(canvas, 'General')
|
|
318
|
+
|
|
319
|
+
// ==========================================================================
|
|
320
|
+
// TEST: Title field
|
|
321
|
+
// Verifies: Title text appears in the Title component on the visualization
|
|
322
|
+
// ==========================================================================
|
|
323
|
+
const titleInput = canvasElement.querySelector('[data-testid="title-input"]') as HTMLInputElement
|
|
324
|
+
expect(titleInput).toBeTruthy()
|
|
325
|
+
|
|
326
|
+
const getTitleVisual = () => {
|
|
327
|
+
const titleElement = canvasElement.querySelector('.map-title')
|
|
328
|
+
return {
|
|
329
|
+
titleText: titleElement?.textContent || '',
|
|
330
|
+
hasTitleElement: Boolean(titleElement)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
await performAndAssert(
|
|
335
|
+
'Title → Update text',
|
|
336
|
+
getTitleVisual,
|
|
337
|
+
async () => {
|
|
338
|
+
await userEvent.clear(titleInput)
|
|
339
|
+
await userEvent.type(titleInput, 'Test Map Title')
|
|
340
|
+
},
|
|
341
|
+
(before, after) => before.titleText !== after.titleText && after.titleText.includes('Test Map Title')
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
// ==========================================================================
|
|
345
|
+
// TEST: Show Title checkbox
|
|
346
|
+
// Verifies: Title element visibility changes (visible/hidden class)
|
|
347
|
+
// ==========================================================================
|
|
348
|
+
const generalAccordion = canvasElement.querySelector('[aria-expanded="true"]')?.closest('.accordion__item')
|
|
349
|
+
const showTitleLabel = Array.from(generalAccordion?.querySelectorAll('label') || []).find(label =>
|
|
350
|
+
label.textContent?.includes('Show Title')
|
|
351
|
+
)
|
|
352
|
+
const showTitleCheckbox = showTitleLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
|
353
|
+
expect(showTitleCheckbox).toBeTruthy()
|
|
354
|
+
|
|
355
|
+
const getTitleVisibility = () => {
|
|
356
|
+
const titleElement = canvasElement.querySelector('.map-title')
|
|
357
|
+
const classes = titleElement ? Array.from(titleElement.classList) : []
|
|
358
|
+
return {
|
|
359
|
+
isVisible: classes.includes('visible'),
|
|
360
|
+
isHidden: classes.includes('hidden')
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Test config has showTitle: true, so title starts visible
|
|
365
|
+
await performAndAssert(
|
|
366
|
+
'Show Title → Hide',
|
|
367
|
+
getTitleVisibility,
|
|
368
|
+
async () => {
|
|
369
|
+
await userEvent.click(showTitleCheckbox)
|
|
370
|
+
},
|
|
371
|
+
(before, after) => before.isVisible && !after.isVisible && after.isHidden
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
await performAndAssert(
|
|
375
|
+
'Show Title → Show',
|
|
376
|
+
getTitleVisibility,
|
|
377
|
+
async () => {
|
|
378
|
+
await userEvent.click(showTitleCheckbox)
|
|
379
|
+
},
|
|
380
|
+
(before, after) => !before.isVisible && after.isVisible && !after.isHidden
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
// ==========================================================================
|
|
384
|
+
// TEST: Super Title field
|
|
385
|
+
// Verifies: Super title text appears in the Title component
|
|
386
|
+
// ==========================================================================
|
|
387
|
+
const superTitleInput = canvas.getByLabelText(/Super Title/i) as HTMLInputElement
|
|
388
|
+
expect(superTitleInput).toBeTruthy()
|
|
389
|
+
|
|
390
|
+
const getSuperTitleVisual = () => {
|
|
391
|
+
const titleElement = canvasElement.querySelector('.map-title')
|
|
392
|
+
return {
|
|
393
|
+
titleText: titleElement?.textContent || ''
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
await performAndAssert(
|
|
398
|
+
'Super Title → Add text',
|
|
399
|
+
getSuperTitleVisual,
|
|
400
|
+
async () => {
|
|
401
|
+
await userEvent.clear(superTitleInput)
|
|
402
|
+
await userEvent.type(superTitleInput, 'Super Title Text')
|
|
403
|
+
},
|
|
404
|
+
(before, after) => !before.titleText.includes('Super Title Text') && after.titleText.includes('Super Title Text')
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
// ==========================================================================
|
|
408
|
+
// TEST: Message/Intro Text field
|
|
409
|
+
// Verifies: Intro text appears in section with class 'introText'
|
|
410
|
+
// ==========================================================================
|
|
411
|
+
const messageInput = canvas.getByLabelText(/Message/i) as HTMLTextAreaElement
|
|
412
|
+
expect(messageInput).toBeTruthy()
|
|
413
|
+
|
|
414
|
+
const getIntroTextVisual = () => {
|
|
415
|
+
const introSection = canvasElement.querySelector('.introText')
|
|
416
|
+
return {
|
|
417
|
+
introText: introSection?.textContent || '',
|
|
418
|
+
hasIntroSection: Boolean(introSection)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
await performAndAssert(
|
|
423
|
+
'Message → Add intro text',
|
|
424
|
+
getIntroTextVisual,
|
|
425
|
+
async () => {
|
|
426
|
+
await userEvent.clear(messageInput)
|
|
427
|
+
await userEvent.type(messageInput, 'This is test intro text')
|
|
428
|
+
},
|
|
429
|
+
(before, after) =>
|
|
430
|
+
!before.introText.includes('This is test intro text') &&
|
|
431
|
+
after.introText.includes('This is test intro text') &&
|
|
432
|
+
after.hasIntroSection
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
// ==========================================================================
|
|
436
|
+
// TEST: Subtext field
|
|
437
|
+
// Verifies: Subtext appears in paragraph with class 'subtext'
|
|
438
|
+
// ==========================================================================
|
|
439
|
+
const subtextInput = canvas.getByLabelText(/Subtext/i) as HTMLTextAreaElement
|
|
440
|
+
expect(subtextInput).toBeTruthy()
|
|
441
|
+
|
|
442
|
+
const getSubtextVisual = () => {
|
|
443
|
+
const subtextElement = canvasElement.querySelector('.subtext')
|
|
444
|
+
return {
|
|
445
|
+
subtextContent: subtextElement?.textContent || '',
|
|
446
|
+
hasSubtext: Boolean(subtextElement)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
await performAndAssert(
|
|
451
|
+
'Subtext → Add text',
|
|
452
|
+
getSubtextVisual,
|
|
453
|
+
async () => {
|
|
454
|
+
await userEvent.clear(subtextInput)
|
|
455
|
+
await userEvent.type(subtextInput, 'This is test subtext')
|
|
456
|
+
},
|
|
457
|
+
(before, after) =>
|
|
458
|
+
!before.subtextContent.includes('This is test subtext') &&
|
|
459
|
+
after.subtextContent.includes('This is test subtext') &&
|
|
460
|
+
after.hasSubtext
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
// ==========================================================================
|
|
464
|
+
// TEST: Footnotes field
|
|
465
|
+
// Verifies: Footnotes appear in section with class 'footnotes'
|
|
466
|
+
// ==========================================================================
|
|
467
|
+
const footnotesInput = canvas.getByLabelText(/Footnotes/i) as HTMLTextAreaElement
|
|
468
|
+
expect(footnotesInput).toBeTruthy()
|
|
469
|
+
|
|
470
|
+
const getFootnotesVisual = () => {
|
|
471
|
+
const footnotesSection = canvasElement.querySelector('.footnotes')
|
|
472
|
+
return {
|
|
473
|
+
footnotesContent: footnotesSection?.textContent || '',
|
|
474
|
+
hasFootnotes: Boolean(footnotesSection)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
await performAndAssert(
|
|
479
|
+
'Footnotes → Add text',
|
|
480
|
+
getFootnotesVisual,
|
|
481
|
+
async () => {
|
|
482
|
+
await userEvent.clear(footnotesInput)
|
|
483
|
+
await userEvent.type(footnotesInput, 'Test footnote text')
|
|
484
|
+
},
|
|
485
|
+
(before, after) =>
|
|
486
|
+
!before.footnotesContent.includes('Test footnote text') &&
|
|
487
|
+
after.footnotesContent.includes('Test footnote text') &&
|
|
488
|
+
after.hasFootnotes
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export const ColumnsSectionTests: Story = {
|
|
494
|
+
args: {
|
|
495
|
+
...DEFAULT_ARGS
|
|
496
|
+
},
|
|
497
|
+
play: async ({ canvasElement }) => {
|
|
498
|
+
const canvas = within(canvasElement)
|
|
499
|
+
|
|
500
|
+
await waitForEditor(canvas)
|
|
501
|
+
await waitForPresence('.map-container', canvasElement)
|
|
502
|
+
|
|
503
|
+
await openAccordion(canvas, 'Columns')
|
|
504
|
+
|
|
505
|
+
// ==========================================================================
|
|
506
|
+
// TEST: Geography column select
|
|
507
|
+
// Verifies: Changing geo column to invalid data makes map gray and legend shows "No data"
|
|
508
|
+
// ==========================================================================
|
|
509
|
+
const columnsAccordion = canvasElement.querySelector('[aria-expanded="true"]')?.closest('.accordion__item')
|
|
510
|
+
const geoColumnLabel = Array.from(columnsAccordion?.querySelectorAll('label') || []).find(label => {
|
|
511
|
+
const columnHeading = label.querySelector('.edit-label.column-heading')
|
|
512
|
+
return columnHeading?.textContent?.includes('Geography')
|
|
513
|
+
})
|
|
514
|
+
const geoSelect = geoColumnLabel?.querySelector('select') as HTMLSelectElement
|
|
515
|
+
expect(geoSelect).toBeTruthy()
|
|
516
|
+
|
|
517
|
+
const getMapAndLegendState = () => {
|
|
518
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
519
|
+
const legendSvg = legendContainer?.querySelector('svg')
|
|
520
|
+
const gradientStops = legendSvg?.querySelectorAll('linearGradient stop')
|
|
521
|
+
|
|
522
|
+
// Gradient legend has color stops when data is valid
|
|
523
|
+
const hasGradientStops = (gradientStops?.length || 0) > 0
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
legendHTML: legendContainer?.innerHTML || '',
|
|
527
|
+
hasGradientStops: hasGradientStops,
|
|
528
|
+
gradientStopCount: gradientStops?.length || 0
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Change to a column that's NOT valid geography data (Rate instead of STATE)
|
|
533
|
+
await performAndAssert(
|
|
534
|
+
'Geography Column → Change to invalid column',
|
|
535
|
+
getMapAndLegendState,
|
|
536
|
+
async () => {
|
|
537
|
+
await userEvent.selectOptions(geoSelect, 'Rate')
|
|
538
|
+
},
|
|
539
|
+
(before, after) => {
|
|
540
|
+
// Legend gradient should lose color stops when geo column is invalid
|
|
541
|
+
return before.hasGradientStops && !after.hasGradientStops
|
|
542
|
+
}
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
// Change back to valid geography column
|
|
546
|
+
await performAndAssert(
|
|
547
|
+
'Geography Column → Change back to valid column',
|
|
548
|
+
getMapAndLegendState,
|
|
549
|
+
async () => {
|
|
550
|
+
await userEvent.selectOptions(geoSelect, 'STATE')
|
|
551
|
+
},
|
|
552
|
+
(before, after) => {
|
|
553
|
+
// Legend gradient should regain color stops when geo column is valid
|
|
554
|
+
return !before.hasGradientStops && after.hasGradientStops
|
|
555
|
+
}
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
// ==========================================================================
|
|
559
|
+
// TEST: Hide Geography Column Name in Tooltip checkbox
|
|
560
|
+
// Verifies: Tooltip content changes when geography label is hidden/shown
|
|
561
|
+
// ==========================================================================
|
|
562
|
+
const hideGeoLabel = Array.from(columnsAccordion?.querySelectorAll('label') || []).find(label =>
|
|
563
|
+
label.textContent?.includes('Hide Geography Column Name in Tooltip')
|
|
564
|
+
)
|
|
565
|
+
const hideGeoCheckbox = hideGeoLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
|
566
|
+
expect(hideGeoCheckbox).toBeTruthy()
|
|
567
|
+
|
|
568
|
+
const getTooltipContent = () => {
|
|
569
|
+
// Get a state geo-group and check its tooltip HTML
|
|
570
|
+
const geoGroup = canvasElement.querySelector('g.geo-group') as SVGGElement
|
|
571
|
+
const tooltipHtml = geoGroup?.getAttribute('data-tooltip-html') || ''
|
|
572
|
+
return {
|
|
573
|
+
tooltipContent: tooltipHtml,
|
|
574
|
+
hasStatePrefix: tooltipHtml.includes('State:')
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
await performAndAssert(
|
|
579
|
+
'Hide Geography Column Name → Enable',
|
|
580
|
+
getTooltipContent,
|
|
581
|
+
async () => {
|
|
582
|
+
await userEvent.click(hideGeoCheckbox)
|
|
583
|
+
},
|
|
584
|
+
(before, after) => {
|
|
585
|
+
// "State:" prefix should disappear when hidden
|
|
586
|
+
return before.hasStatePrefix && !after.hasStatePrefix
|
|
587
|
+
}
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
await performAndAssert(
|
|
591
|
+
'Hide Geography Column Name → Disable',
|
|
592
|
+
getTooltipContent,
|
|
593
|
+
async () => {
|
|
594
|
+
await userEvent.click(hideGeoCheckbox)
|
|
595
|
+
},
|
|
596
|
+
(before, after) => {
|
|
597
|
+
// "State:" prefix should reappear when not hidden
|
|
598
|
+
return !before.hasStatePrefix && after.hasStatePrefix
|
|
599
|
+
}
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
// ==========================================================================
|
|
603
|
+
// TEST: Geography Label field
|
|
604
|
+
// Verifies: Custom geography label appears in tooltip instead of default "State:"
|
|
605
|
+
// ==========================================================================
|
|
606
|
+
const geoLabelInput = canvas.getByLabelText(/Geography Label/i) as HTMLInputElement
|
|
607
|
+
expect(geoLabelInput).toBeTruthy()
|
|
608
|
+
|
|
609
|
+
await performAndAssert(
|
|
610
|
+
'Geography Label → Add custom label',
|
|
611
|
+
getTooltipContent,
|
|
612
|
+
async () => {
|
|
613
|
+
await userEvent.clear(geoLabelInput)
|
|
614
|
+
await userEvent.type(geoLabelInput, 'Region')
|
|
615
|
+
},
|
|
616
|
+
(before, after) => {
|
|
617
|
+
// Custom label "Region:" should appear in tooltip
|
|
618
|
+
return !before.tooltipContent.includes('Region:') && after.tooltipContent.includes('Region:')
|
|
619
|
+
}
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
// ==========================================================================
|
|
623
|
+
// TEST: Data Column select
|
|
624
|
+
// Verifies: Changing data column changes tooltip data values
|
|
625
|
+
// ==========================================================================
|
|
626
|
+
const dataColumnLabel = Array.from(columnsAccordion?.querySelectorAll('label') || []).find(label =>
|
|
627
|
+
label.textContent?.includes('Data Column')
|
|
628
|
+
)
|
|
629
|
+
const dataColumnSelect = dataColumnLabel?.querySelector('select') as HTMLSelectElement
|
|
630
|
+
expect(dataColumnSelect).toBeTruthy()
|
|
631
|
+
|
|
632
|
+
const getDataTooltipContent = () => {
|
|
633
|
+
const geoGroup = canvasElement.querySelector('g.geo-group') as SVGGElement
|
|
634
|
+
const tooltipHtml = geoGroup?.getAttribute('data-tooltip-html') || ''
|
|
635
|
+
return {
|
|
636
|
+
tooltipContent: tooltipHtml
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
await performAndAssert(
|
|
641
|
+
'Data Column → Change to STATE column',
|
|
642
|
+
getDataTooltipContent,
|
|
643
|
+
async () => {
|
|
644
|
+
await userEvent.selectOptions(dataColumnSelect, 'STATE')
|
|
645
|
+
},
|
|
646
|
+
(before, after) => {
|
|
647
|
+
// Tooltip should show STATE values (e.g., state names) instead of Rate values (numbers)
|
|
648
|
+
const beforeHasNumbers = /\d+/.test(before.tooltipContent)
|
|
649
|
+
const afterHasStateNames = /Alabama|Alaska|Arizona|Arkansas|California/.test(after.tooltipContent)
|
|
650
|
+
return beforeHasNumbers && afterHasStateNames
|
|
651
|
+
}
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
// Change back to Rate
|
|
655
|
+
await performAndAssert(
|
|
656
|
+
'Data Column → Change back to Rate',
|
|
657
|
+
getDataTooltipContent,
|
|
658
|
+
async () => {
|
|
659
|
+
await userEvent.selectOptions(dataColumnSelect, 'Rate')
|
|
660
|
+
},
|
|
661
|
+
(before, after) => {
|
|
662
|
+
// Tooltip should show numeric Rate values again
|
|
663
|
+
const beforeHasStateNames = /Alabama|Alaska|Arizona|Arkansas|California/.test(before.tooltipContent)
|
|
664
|
+
const afterHasNumbers = /\d+/.test(after.tooltipContent)
|
|
665
|
+
return beforeHasStateNames && afterHasNumbers
|
|
666
|
+
}
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
// ==========================================================================
|
|
670
|
+
// TEST: Hide Data Column Name in Tooltip
|
|
671
|
+
// Verifies: Data column label disappears from tooltip when hidden
|
|
672
|
+
// ==========================================================================
|
|
673
|
+
const hideDataLabel = Array.from(columnsAccordion?.querySelectorAll('label') || []).find(label =>
|
|
674
|
+
label.textContent?.includes('Hide Data Column Name in Tooltip')
|
|
675
|
+
)
|
|
676
|
+
const hideDataCheckbox = hideDataLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
|
677
|
+
expect(hideDataCheckbox).toBeTruthy()
|
|
678
|
+
|
|
679
|
+
await performAndAssert(
|
|
680
|
+
'Hide Data Column Name → Enable',
|
|
681
|
+
getDataTooltipContent,
|
|
682
|
+
async () => {
|
|
683
|
+
await userEvent.click(hideDataCheckbox)
|
|
684
|
+
},
|
|
685
|
+
(before, after) => {
|
|
686
|
+
// The label prefix (e.g., "Rate:" or similar) should be removed
|
|
687
|
+
// Before should have a colon pattern indicating a label, after should have less structure
|
|
688
|
+
const beforeHasLabelStructure = before.tooltipContent.includes(':</li>') || before.tooltipContent.includes(': ')
|
|
689
|
+
const afterHasLessStructure = after.tooltipContent.length < before.tooltipContent.length
|
|
690
|
+
return beforeHasLabelStructure && afterHasLessStructure
|
|
691
|
+
}
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
await performAndAssert(
|
|
695
|
+
'Hide Data Column Name → Disable',
|
|
696
|
+
getDataTooltipContent,
|
|
697
|
+
async () => {
|
|
698
|
+
await userEvent.click(hideDataCheckbox)
|
|
699
|
+
},
|
|
700
|
+
(before, after) => {
|
|
701
|
+
// The label prefix should reappear
|
|
702
|
+
const beforeHasLessStructure = before.tooltipContent.length < after.tooltipContent.length
|
|
703
|
+
const afterHasLabelStructure = after.tooltipContent.includes(':</li>') || after.tooltipContent.includes(': ')
|
|
704
|
+
return beforeHasLessStructure && afterHasLabelStructure
|
|
705
|
+
}
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
// ==========================================================================
|
|
709
|
+
// TEST: Data Label field
|
|
710
|
+
// Verifies: Custom data label appears in tooltip
|
|
711
|
+
// ==========================================================================
|
|
712
|
+
const dataLabelInputs = Array.from(columnsAccordion?.querySelectorAll('input') || [])
|
|
713
|
+
const dataLabelInput = dataLabelInputs.find(input => {
|
|
714
|
+
const label = input.closest('label')
|
|
715
|
+
return label?.textContent?.includes('Data Label')
|
|
716
|
+
}) as HTMLInputElement
|
|
717
|
+
expect(dataLabelInput).toBeTruthy()
|
|
718
|
+
|
|
719
|
+
await performAndAssert(
|
|
720
|
+
'Data Label → Add custom label',
|
|
721
|
+
getDataTooltipContent,
|
|
722
|
+
async () => {
|
|
723
|
+
await userEvent.clear(dataLabelInput)
|
|
724
|
+
await userEvent.type(dataLabelInput, 'Value')
|
|
725
|
+
},
|
|
726
|
+
(before, after) => {
|
|
727
|
+
// Custom label "Value:" should appear in tooltip
|
|
728
|
+
return !before.tooltipContent.includes('Value:') && after.tooltipContent.includes('Value:')
|
|
729
|
+
}
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
// ==========================================================================
|
|
733
|
+
// TEST: Prefix field
|
|
734
|
+
// Verifies: Prefix appears before data values in both tooltip and legend
|
|
735
|
+
// ==========================================================================
|
|
736
|
+
const prefixInputs = Array.from(columnsAccordion?.querySelectorAll('input') || [])
|
|
737
|
+
const prefixInput = prefixInputs.find(input => {
|
|
738
|
+
const label = input.closest('label')
|
|
739
|
+
return label?.textContent?.includes('Prefix')
|
|
740
|
+
}) as HTMLInputElement
|
|
741
|
+
expect(prefixInput).toBeTruthy()
|
|
742
|
+
|
|
743
|
+
const getPrefixVisualization = () => {
|
|
744
|
+
const geoGroup = canvasElement.querySelector('g.geo-group') as SVGGElement
|
|
745
|
+
const tooltipHtml = geoGroup?.getAttribute('data-tooltip-html') || ''
|
|
746
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
747
|
+
const legendSvg = legendContainer?.querySelector('svg')
|
|
748
|
+
const legendText = legendSvg?.textContent || ''
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
tooltipHasPrefix: tooltipHtml.includes('$'),
|
|
752
|
+
legendHasPrefix: legendText.includes('$'),
|
|
753
|
+
tooltipContent: tooltipHtml,
|
|
754
|
+
legendContent: legendText
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
await performAndAssert(
|
|
759
|
+
'Prefix → Add dollar sign',
|
|
760
|
+
getPrefixVisualization,
|
|
761
|
+
async () => {
|
|
762
|
+
await userEvent.clear(prefixInput)
|
|
763
|
+
await userEvent.type(prefixInput, '$')
|
|
764
|
+
},
|
|
765
|
+
(before, after) => {
|
|
766
|
+
// Dollar sign should appear in both tooltip and legend
|
|
767
|
+
return !before.tooltipHasPrefix && after.tooltipHasPrefix && !before.legendHasPrefix && after.legendHasPrefix
|
|
768
|
+
}
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
// ==========================================================================
|
|
772
|
+
// TEST: Suffix field
|
|
773
|
+
// Verifies: Suffix appears after data values in both tooltip and legend
|
|
774
|
+
// ==========================================================================
|
|
775
|
+
const suffixInputs = Array.from(columnsAccordion?.querySelectorAll('input') || [])
|
|
776
|
+
const suffixInput = suffixInputs.find(input => {
|
|
777
|
+
const label = input.closest('label')
|
|
778
|
+
return label?.textContent?.includes('Suffix')
|
|
779
|
+
}) as HTMLInputElement
|
|
780
|
+
expect(suffixInput).toBeTruthy()
|
|
781
|
+
|
|
782
|
+
const getSuffixVisualization = () => {
|
|
783
|
+
const geoGroup = canvasElement.querySelector('g.geo-group') as SVGGElement
|
|
784
|
+
const tooltipHtml = geoGroup?.getAttribute('data-tooltip-html') || ''
|
|
785
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
786
|
+
const legendSvg = legendContainer?.querySelector('svg')
|
|
787
|
+
const legendText = legendSvg?.textContent || ''
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
tooltipHasSuffix: tooltipHtml.includes('%'),
|
|
791
|
+
legendHasSuffix: legendText.includes('%'),
|
|
792
|
+
tooltipContent: tooltipHtml,
|
|
793
|
+
legendContent: legendText
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
await performAndAssert(
|
|
798
|
+
'Suffix → Add percent sign',
|
|
799
|
+
getSuffixVisualization,
|
|
800
|
+
async () => {
|
|
801
|
+
await userEvent.clear(suffixInput)
|
|
802
|
+
await userEvent.type(suffixInput, '%')
|
|
803
|
+
},
|
|
804
|
+
(before, after) => {
|
|
805
|
+
// Percent sign should appear in both tooltip and legend
|
|
806
|
+
return !before.tooltipHasSuffix && after.tooltipHasSuffix && !before.legendHasSuffix && after.legendHasSuffix
|
|
807
|
+
}
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
// ==========================================================================
|
|
811
|
+
// TEST: Round field
|
|
812
|
+
// Verifies: Changing rounding adds decimal places in both tooltip and legend
|
|
813
|
+
// ==========================================================================
|
|
814
|
+
const roundInputs = Array.from(columnsAccordion?.querySelectorAll('input') || [])
|
|
815
|
+
const roundInput = roundInputs.find(input => {
|
|
816
|
+
const label = input.closest('label')
|
|
817
|
+
return label?.textContent?.includes('Round')
|
|
818
|
+
}) as HTMLInputElement
|
|
819
|
+
expect(roundInput).toBeTruthy()
|
|
820
|
+
|
|
821
|
+
const getRoundingVisualization = () => {
|
|
822
|
+
const geoGroup = canvasElement.querySelector('g.geo-group') as SVGGElement
|
|
823
|
+
const tooltipHtml = geoGroup?.getAttribute('data-tooltip-html') || ''
|
|
824
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
825
|
+
const legendSvg = legendContainer?.querySelector('svg')
|
|
826
|
+
const legendText = legendSvg?.textContent || ''
|
|
827
|
+
|
|
828
|
+
// Check for 2 decimal places pattern (e.g., "12.34", "0.50", etc.)
|
|
829
|
+
const twoDecimalPattern = /\d+\.\d{2}/
|
|
830
|
+
|
|
831
|
+
return {
|
|
832
|
+
tooltipHasTwoDecimals: twoDecimalPattern.test(tooltipHtml),
|
|
833
|
+
legendHasTwoDecimals: twoDecimalPattern.test(legendText),
|
|
834
|
+
tooltipContent: tooltipHtml,
|
|
835
|
+
legendContent: legendText
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
await performAndAssert(
|
|
840
|
+
'Round → Set to 2 decimal places',
|
|
841
|
+
getRoundingVisualization,
|
|
842
|
+
async () => {
|
|
843
|
+
await userEvent.clear(roundInput)
|
|
844
|
+
await userEvent.type(roundInput, '2')
|
|
845
|
+
},
|
|
846
|
+
(before, after) => {
|
|
847
|
+
// Numbers should now show 2 decimal places in both tooltip and legend
|
|
848
|
+
return (
|
|
849
|
+
!before.tooltipHasTwoDecimals &&
|
|
850
|
+
after.tooltipHasTwoDecimals &&
|
|
851
|
+
!before.legendHasTwoDecimals &&
|
|
852
|
+
after.legendHasTwoDecimals
|
|
853
|
+
)
|
|
854
|
+
}
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
// ==========================================================================
|
|
858
|
+
// TEST: Show in Data Table checkbox
|
|
859
|
+
// Verifies: Primary column appears/disappears in the data table
|
|
860
|
+
// ==========================================================================
|
|
861
|
+
const showInTableLabel = Array.from(columnsAccordion?.querySelectorAll('label') || []).find(label =>
|
|
862
|
+
label.textContent?.includes('Show in Data Table')
|
|
863
|
+
)
|
|
864
|
+
const showInTableCheckbox = showInTableLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
|
865
|
+
expect(showInTableCheckbox).toBeTruthy()
|
|
866
|
+
|
|
867
|
+
const getDataTableState = () => {
|
|
868
|
+
const dataTable = canvasElement.querySelector('.data-table-container, table')
|
|
869
|
+
const tableHeaders = Array.from(dataTable?.querySelectorAll('th') || [])
|
|
870
|
+
const tableHeaderText = tableHeaders.map(th => th.textContent?.trim() || '')
|
|
871
|
+
|
|
872
|
+
// Check if "Rate" column header appears in data table
|
|
873
|
+
const hasRateColumn = tableHeaderText.some(text => text.includes('Rate') || text.includes('Value'))
|
|
874
|
+
|
|
875
|
+
return {
|
|
876
|
+
hasDataTable: Boolean(dataTable),
|
|
877
|
+
tableHeaders: tableHeaderText,
|
|
878
|
+
hasRateColumn: hasRateColumn
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Test config has dataTable: true for primary column
|
|
883
|
+
await performAndAssert(
|
|
884
|
+
'Show in Data Table → Disable',
|
|
885
|
+
getDataTableState,
|
|
886
|
+
async () => {
|
|
887
|
+
await userEvent.click(showInTableCheckbox)
|
|
888
|
+
},
|
|
889
|
+
(before, after) => {
|
|
890
|
+
// Rate column should disappear from data table headers
|
|
891
|
+
return before.hasRateColumn && !after.hasRateColumn
|
|
892
|
+
}
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
await performAndAssert(
|
|
896
|
+
'Show in Data Table → Enable',
|
|
897
|
+
getDataTableState,
|
|
898
|
+
async () => {
|
|
899
|
+
await userEvent.click(showInTableCheckbox)
|
|
900
|
+
},
|
|
901
|
+
(before, after) => {
|
|
902
|
+
// Rate column should reappear in data table headers
|
|
903
|
+
return !before.hasRateColumn && after.hasRateColumn
|
|
904
|
+
}
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
// ==========================================================================
|
|
908
|
+
// TEST: Show in Tooltips checkbox
|
|
909
|
+
// Verifies: Primary column data appears/disappears in tooltips
|
|
910
|
+
// ==========================================================================
|
|
911
|
+
const showInTooltipsLabel = Array.from(columnsAccordion?.querySelectorAll('label') || []).find(label =>
|
|
912
|
+
label.textContent?.includes('Show in Tooltips')
|
|
913
|
+
)
|
|
914
|
+
const showInTooltipsCheckbox = showInTooltipsLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
|
915
|
+
expect(showInTooltipsCheckbox).toBeTruthy()
|
|
916
|
+
|
|
917
|
+
const getTooltipDataState = () => {
|
|
918
|
+
const geoGroup = canvasElement.querySelector('g.geo-group') as SVGGElement
|
|
919
|
+
const tooltipHtml = geoGroup?.getAttribute('data-tooltip-html') || ''
|
|
920
|
+
|
|
921
|
+
// Check if tooltip contains data values (numbers with potential $ and % from earlier tests)
|
|
922
|
+
// Look for patterns like list items with numeric values
|
|
923
|
+
const hasDataValues = tooltipHtml.includes('<li') && /\$?\d+\.\d{2}%?/.test(tooltipHtml)
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
tooltipContent: tooltipHtml,
|
|
927
|
+
hasDataValues: hasDataValues,
|
|
928
|
+
tooltipLength: tooltipHtml.length
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Test config has tooltip: true for primary column
|
|
933
|
+
await performAndAssert(
|
|
934
|
+
'Show in Tooltips → Disable',
|
|
935
|
+
getTooltipDataState,
|
|
936
|
+
async () => {
|
|
937
|
+
await userEvent.click(showInTooltipsCheckbox)
|
|
938
|
+
},
|
|
939
|
+
(before, after) => {
|
|
940
|
+
// Tooltip should no longer contain data values, should be shorter
|
|
941
|
+
return before.hasDataValues && !after.hasDataValues && after.tooltipLength < before.tooltipLength
|
|
942
|
+
}
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
await performAndAssert(
|
|
946
|
+
'Show in Tooltips → Enable',
|
|
947
|
+
getTooltipDataState,
|
|
948
|
+
async () => {
|
|
949
|
+
await userEvent.click(showInTooltipsCheckbox)
|
|
950
|
+
},
|
|
951
|
+
(before, after) => {
|
|
952
|
+
// Tooltip should contain data values again, should be longer
|
|
953
|
+
return !before.hasDataValues && after.hasDataValues && after.tooltipLength > before.tooltipLength
|
|
954
|
+
}
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
// ==========================================================================
|
|
958
|
+
// TEST: Add Special Class button
|
|
959
|
+
// Verifies: New special class item appears in legend
|
|
960
|
+
// ==========================================================================
|
|
961
|
+
const addSpecialClassButton = Array.from(columnsAccordion?.querySelectorAll('button') || []).find(button =>
|
|
962
|
+
button.textContent?.includes('Add Special Class')
|
|
963
|
+
) as HTMLButtonElement
|
|
964
|
+
expect(addSpecialClassButton).toBeTruthy()
|
|
965
|
+
|
|
966
|
+
const getSpecialClassVisualization = () => {
|
|
967
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
968
|
+
const legendSvg = legendContainer?.querySelector('svg')
|
|
969
|
+
const gradientStops = legendSvg?.querySelectorAll('linearGradient stop')
|
|
970
|
+
const gradientRects = legendSvg?.querySelectorAll('g.visx-group rect')
|
|
971
|
+
|
|
972
|
+
// Get all stop colors to detect changes
|
|
973
|
+
const stopColors = Array.from(gradientStops || []).map(stop => (stop as SVGStopElement).getAttribute('style'))
|
|
974
|
+
|
|
975
|
+
// Get all rect fills to detect new colors
|
|
976
|
+
const rectFills = Array.from(gradientRects || []).map(rect => (rect as SVGRectElement).getAttribute('fill'))
|
|
977
|
+
|
|
978
|
+
return {
|
|
979
|
+
gradientStopCount: gradientStops?.length || 0,
|
|
980
|
+
gradientRectCount: gradientRects?.length || 0,
|
|
981
|
+
stopColors: stopColors,
|
|
982
|
+
rectFills: rectFills,
|
|
983
|
+
legendHTML: legendContainer?.innerHTML || ''
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
await performAndAssert(
|
|
988
|
+
'Add Special Class → Configure with STATE=Alabama',
|
|
989
|
+
getSpecialClassVisualization,
|
|
990
|
+
async () => {
|
|
991
|
+
// Click the Add Special Class button
|
|
992
|
+
await userEvent.click(addSpecialClassButton)
|
|
993
|
+
|
|
994
|
+
// Find the newly added special class fields
|
|
995
|
+
const specialClassSection = Array.from(columnsAccordion?.querySelectorAll('.edit-block') || []).find(block =>
|
|
996
|
+
block.textContent?.includes('Special Class 1')
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
// Get all selects in the special class section
|
|
1000
|
+
// First select is Data Key, second is Value
|
|
1001
|
+
const selects = Array.from(specialClassSection?.querySelectorAll('select') || [])
|
|
1002
|
+
const dataKeySelect = selects[0] as HTMLSelectElement
|
|
1003
|
+
const valueSelect = selects[1] as HTMLSelectElement
|
|
1004
|
+
|
|
1005
|
+
// Set Data Key to STATE
|
|
1006
|
+
await userEvent.selectOptions(dataKeySelect, 'STATE')
|
|
1007
|
+
|
|
1008
|
+
// Wait for the Value select to populate with options based on STATE column
|
|
1009
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
1010
|
+
|
|
1011
|
+
// Select "Alabama" from the Value select
|
|
1012
|
+
await userEvent.selectOptions(valueSelect, 'Alabama')
|
|
1013
|
+
|
|
1014
|
+
// Find and fill in the Label field
|
|
1015
|
+
const labelInput = Array.from(specialClassSection?.querySelectorAll('input') || []).find(input => {
|
|
1016
|
+
const label = input.closest('label')
|
|
1017
|
+
return label?.textContent?.includes('Label')
|
|
1018
|
+
}) as HTMLInputElement
|
|
1019
|
+
|
|
1020
|
+
await userEvent.clear(labelInput)
|
|
1021
|
+
await userEvent.type(labelInput, 'Alabama')
|
|
1022
|
+
},
|
|
1023
|
+
(before, after) => {
|
|
1024
|
+
// Check that "Alabama" text appears in the legend
|
|
1025
|
+
const alabamaInLegend = after.legendHTML.includes('Alabama')
|
|
1026
|
+
|
|
1027
|
+
// Check that the special class gray color appears
|
|
1028
|
+
const hasGrayColor = after.rectFills.includes('#b4b4b4')
|
|
1029
|
+
|
|
1030
|
+
// Legend should change
|
|
1031
|
+
const legendChanged = after.legendHTML !== before.legendHTML
|
|
1032
|
+
|
|
1033
|
+
return alabamaInLegend && hasGrayColor && legendChanged
|
|
1034
|
+
}
|
|
1035
|
+
)
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
export const LegendSectionTests: Story = {
|
|
1040
|
+
args: {
|
|
1041
|
+
...DEFAULT_ARGS
|
|
1042
|
+
},
|
|
1043
|
+
play: async ({ canvasElement }) => {
|
|
1044
|
+
const canvas = within(canvasElement)
|
|
1045
|
+
|
|
1046
|
+
await waitForEditor(canvas)
|
|
1047
|
+
await waitForPresence('.map-container', canvasElement)
|
|
1048
|
+
|
|
1049
|
+
await openAccordion(canvas, 'Legend')
|
|
1050
|
+
|
|
1051
|
+
// ==========================================================================
|
|
1052
|
+
// TEST: Legend Type
|
|
1053
|
+
// ==========================================================================
|
|
1054
|
+
const legendTypeSelect = Array.from(canvasElement.querySelectorAll('select') || []).find(select => {
|
|
1055
|
+
const label = select.closest('label')
|
|
1056
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
1057
|
+
return labelSpan?.textContent?.includes('Legend Type')
|
|
1058
|
+
}) as HTMLSelectElement
|
|
1059
|
+
|
|
1060
|
+
const getLegendType = () => {
|
|
1061
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
1062
|
+
// For gradient legend, get labels from SVG text elements
|
|
1063
|
+
const textElements = Array.from(legendContainer?.querySelectorAll('text tspan') || [])
|
|
1064
|
+
const labels = textElements.map(el => el.textContent?.trim() || '')
|
|
1065
|
+
return {
|
|
1066
|
+
legendHTML: legendContainer?.innerHTML || '',
|
|
1067
|
+
labels
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
await performAndAssert(
|
|
1072
|
+
'Legend Type → Equal Interval',
|
|
1073
|
+
getLegendType,
|
|
1074
|
+
async () => {
|
|
1075
|
+
await userEvent.selectOptions(legendTypeSelect, 'equalinterval')
|
|
1076
|
+
},
|
|
1077
|
+
(before, after) => {
|
|
1078
|
+
// Equal interval creates evenly spaced ranges (e.g., "0 - < 43.33", "43.33 - < 86.67")
|
|
1079
|
+
return after.labels.length > 0 && after.labels.join(',') !== before.labels.join(',')
|
|
1080
|
+
}
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
await performAndAssert(
|
|
1084
|
+
'Legend Type → Equal Number',
|
|
1085
|
+
getLegendType,
|
|
1086
|
+
async () => {
|
|
1087
|
+
await userEvent.selectOptions(legendTypeSelect, 'equalnumber')
|
|
1088
|
+
},
|
|
1089
|
+
(before, after) => {
|
|
1090
|
+
// Equal number (quantiles) creates ranges with equal counts (e.g., "0 - 40", "40 - 57")
|
|
1091
|
+
return after.labels.length > 0 && after.labels.join(',') !== before.labels.join(',')
|
|
1092
|
+
}
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
// ==========================================================================
|
|
1096
|
+
// TEST: Show Legend checkbox
|
|
1097
|
+
// ==========================================================================
|
|
1098
|
+
const showLegendCheckbox = canvas.getByLabelText('Show Legend')
|
|
1099
|
+
|
|
1100
|
+
const getLegendVisibility = () => {
|
|
1101
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
1102
|
+
return {
|
|
1103
|
+
legendExists: Boolean(legendContainer),
|
|
1104
|
+
isVisible: legendContainer ? !legendContainer.classList.contains('hidden') : false
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
await performAndAssert(
|
|
1109
|
+
'Show Legend → Uncheck',
|
|
1110
|
+
getLegendVisibility,
|
|
1111
|
+
async () => {
|
|
1112
|
+
await userEvent.click(showLegendCheckbox)
|
|
1113
|
+
},
|
|
1114
|
+
(before, after) => {
|
|
1115
|
+
return before.isVisible && !after.isVisible
|
|
1116
|
+
}
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
await performAndAssert(
|
|
1120
|
+
'Show Legend → Check',
|
|
1121
|
+
getLegendVisibility,
|
|
1122
|
+
async () => {
|
|
1123
|
+
await userEvent.click(showLegendCheckbox)
|
|
1124
|
+
},
|
|
1125
|
+
(before, after) => {
|
|
1126
|
+
return !before.isVisible && after.isVisible
|
|
1127
|
+
}
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
// ==========================================================================
|
|
1131
|
+
// TEST: Legend Position
|
|
1132
|
+
// ==========================================================================
|
|
1133
|
+
const legendPositionSelect = Array.from(canvasElement.querySelectorAll('select') || []).find(select => {
|
|
1134
|
+
const label = select.closest('label')
|
|
1135
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
1136
|
+
return labelSpan?.textContent?.includes('Legend Position')
|
|
1137
|
+
}) as HTMLSelectElement
|
|
1138
|
+
|
|
1139
|
+
const getLegendPosition = () => {
|
|
1140
|
+
const legendAside = canvasElement.querySelector('aside[aria-label="Legend"]') as HTMLElement
|
|
1141
|
+
return {
|
|
1142
|
+
classes: legendAside ? Array.from(legendAside.classList) : [],
|
|
1143
|
+
isBottom: legendAside?.classList.contains('bottom') ?? false,
|
|
1144
|
+
isSide: legendAside?.classList.contains('side') ?? false,
|
|
1145
|
+
isTop: legendAside?.classList.contains('top') ?? false
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
await performAndAssert(
|
|
1150
|
+
'Legend Position → Side',
|
|
1151
|
+
getLegendPosition,
|
|
1152
|
+
async () => {
|
|
1153
|
+
await userEvent.selectOptions(legendPositionSelect, 'side')
|
|
1154
|
+
},
|
|
1155
|
+
(before, after) => {
|
|
1156
|
+
return !before.isSide && after.isSide
|
|
1157
|
+
}
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
await performAndAssert(
|
|
1161
|
+
'Legend Position → Top',
|
|
1162
|
+
getLegendPosition,
|
|
1163
|
+
async () => {
|
|
1164
|
+
await userEvent.selectOptions(legendPositionSelect, 'top')
|
|
1165
|
+
},
|
|
1166
|
+
(before, after) => {
|
|
1167
|
+
return !before.isTop && after.isTop
|
|
1168
|
+
}
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
await performAndAssert(
|
|
1172
|
+
'Legend Position → Bottom',
|
|
1173
|
+
getLegendPosition,
|
|
1174
|
+
async () => {
|
|
1175
|
+
await userEvent.selectOptions(legendPositionSelect, 'bottom')
|
|
1176
|
+
},
|
|
1177
|
+
(before, after) => {
|
|
1178
|
+
return !before.isBottom && after.isBottom
|
|
1179
|
+
}
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
// ==========================================================================
|
|
1183
|
+
// TEST: Legend Style
|
|
1184
|
+
// ==========================================================================
|
|
1185
|
+
const legendStyleSelect = Array.from(canvasElement.querySelectorAll('select') || []).find(select => {
|
|
1186
|
+
const label = select.closest('label')
|
|
1187
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
1188
|
+
return labelSpan?.textContent?.includes('Legend Style')
|
|
1189
|
+
}) as HTMLSelectElement
|
|
1190
|
+
|
|
1191
|
+
const getLegendStyle = () => {
|
|
1192
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
1193
|
+
const legendItems = legendContainer?.querySelectorAll('.legend-container__li')
|
|
1194
|
+
const linearGradient = legendContainer?.querySelector('linearGradient')
|
|
1195
|
+
const legendHTML = legendContainer?.innerHTML || ''
|
|
1196
|
+
|
|
1197
|
+
return {
|
|
1198
|
+
hasLegendItems: (legendItems?.length ?? 0) > 0,
|
|
1199
|
+
hasGradient: Boolean(linearGradient),
|
|
1200
|
+
legendHTML
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
await performAndAssert(
|
|
1205
|
+
'Legend Style → Circles',
|
|
1206
|
+
getLegendStyle,
|
|
1207
|
+
async () => {
|
|
1208
|
+
await userEvent.selectOptions(legendStyleSelect, 'circles')
|
|
1209
|
+
},
|
|
1210
|
+
(before, after) => {
|
|
1211
|
+
return after.hasLegendItems && !after.hasGradient && after.legendHTML !== before.legendHTML
|
|
1212
|
+
}
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
await performAndAssert(
|
|
1216
|
+
'Legend Style → Boxes',
|
|
1217
|
+
getLegendStyle,
|
|
1218
|
+
async () => {
|
|
1219
|
+
await userEvent.selectOptions(legendStyleSelect, 'boxes')
|
|
1220
|
+
},
|
|
1221
|
+
(before, after) => {
|
|
1222
|
+
return after.hasLegendItems && !after.hasGradient && after.legendHTML !== before.legendHTML
|
|
1223
|
+
}
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
await performAndAssert(
|
|
1227
|
+
'Legend Style → Gradient',
|
|
1228
|
+
getLegendStyle,
|
|
1229
|
+
async () => {
|
|
1230
|
+
await userEvent.selectOptions(legendStyleSelect, 'gradient')
|
|
1231
|
+
},
|
|
1232
|
+
(before, after) => {
|
|
1233
|
+
return !before.hasGradient && after.hasGradient
|
|
1234
|
+
}
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
// ==========================================================================
|
|
1238
|
+
// TEST: Gradient Style (only visible when Legend Style is gradient)
|
|
1239
|
+
// ==========================================================================
|
|
1240
|
+
const gradientStyleSelect = Array.from(canvasElement.querySelectorAll('select') || []).find(select => {
|
|
1241
|
+
const label = select.closest('label')
|
|
1242
|
+
return label?.textContent?.includes('Gradient Style')
|
|
1243
|
+
}) as HTMLSelectElement
|
|
1244
|
+
|
|
1245
|
+
const getGradientStyle = () => {
|
|
1246
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
1247
|
+
const svgHTML = legendContainer?.querySelector('svg')?.outerHTML || ''
|
|
1248
|
+
|
|
1249
|
+
return {
|
|
1250
|
+
svgHTML,
|
|
1251
|
+
hasLinearGradient: svgHTML.includes('linearGradient')
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
await performAndAssert(
|
|
1256
|
+
'Gradient Style → Linear blocks',
|
|
1257
|
+
getGradientStyle,
|
|
1258
|
+
async () => {
|
|
1259
|
+
await userEvent.selectOptions(gradientStyleSelect, 'linear blocks')
|
|
1260
|
+
},
|
|
1261
|
+
(before, after) => {
|
|
1262
|
+
return after.hasLinearGradient && after.svgHTML !== before.svgHTML
|
|
1263
|
+
}
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
await performAndAssert(
|
|
1267
|
+
'Gradient Style → Smooth',
|
|
1268
|
+
getGradientStyle,
|
|
1269
|
+
async () => {
|
|
1270
|
+
await userEvent.selectOptions(gradientStyleSelect, 'smooth')
|
|
1271
|
+
},
|
|
1272
|
+
(before, after) => {
|
|
1273
|
+
return after.hasLinearGradient && after.svgHTML !== before.svgHTML
|
|
1274
|
+
}
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
// ==========================================================================
|
|
1278
|
+
// TEST: Tick Rotation (only visible when Legend Style is gradient)
|
|
1279
|
+
// ==========================================================================
|
|
1280
|
+
const tickRotationInput = Array.from(canvasElement.querySelectorAll('input[type="number"]') || []).find(input => {
|
|
1281
|
+
const label = input.closest('label')
|
|
1282
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
1283
|
+
return labelSpan?.textContent?.includes('Tick Rotation')
|
|
1284
|
+
}) as HTMLInputElement
|
|
1285
|
+
|
|
1286
|
+
const getTickRotation = () => {
|
|
1287
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
1288
|
+
const svgHTML = legendContainer?.querySelector('svg')?.outerHTML || ''
|
|
1289
|
+
const textElements = Array.from(legendContainer?.querySelectorAll('text') || [])
|
|
1290
|
+
const transforms = textElements.map(el => el.getAttribute('transform') || '')
|
|
1291
|
+
return {
|
|
1292
|
+
svgHTML,
|
|
1293
|
+
transforms,
|
|
1294
|
+
hasRotation: transforms.some(t => t.includes('rotate'))
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
await performAndAssert(
|
|
1299
|
+
'Tick Rotation → Set to 45 degrees',
|
|
1300
|
+
getTickRotation,
|
|
1301
|
+
async () => {
|
|
1302
|
+
await userEvent.clear(tickRotationInput)
|
|
1303
|
+
await userEvent.type(tickRotationInput, '45')
|
|
1304
|
+
},
|
|
1305
|
+
(before, after) => {
|
|
1306
|
+
// Check that rotation transform is applied to text elements
|
|
1307
|
+
return after.hasRotation && after.svgHTML !== before.svgHTML
|
|
1308
|
+
}
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
// ==========================================================================
|
|
1312
|
+
// TEST: Hide Legend Box
|
|
1313
|
+
// ==========================================================================
|
|
1314
|
+
const hideLegendBoxCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]') || []).find(
|
|
1315
|
+
input => {
|
|
1316
|
+
const label = input.closest('label')
|
|
1317
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
1318
|
+
return labelSpan?.textContent?.includes('Hide Legend Box')
|
|
1319
|
+
}
|
|
1320
|
+
) as HTMLInputElement
|
|
1321
|
+
|
|
1322
|
+
const getLegendBorder = () => {
|
|
1323
|
+
const legendAside = canvasElement.querySelector('aside[aria-label="Legend"]') as HTMLElement
|
|
1324
|
+
return {
|
|
1325
|
+
hasNoBorder: legendAside?.classList.contains('no-border') ?? false
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
await performAndAssert(
|
|
1330
|
+
'Hide Legend Box → Uncheck',
|
|
1331
|
+
getLegendBorder,
|
|
1332
|
+
async () => {
|
|
1333
|
+
await userEvent.click(hideLegendBoxCheckbox)
|
|
1334
|
+
},
|
|
1335
|
+
(before, after) => {
|
|
1336
|
+
return before.hasNoBorder && !after.hasNoBorder
|
|
1337
|
+
}
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
await performAndAssert(
|
|
1341
|
+
'Hide Legend Box → Check',
|
|
1342
|
+
getLegendBorder,
|
|
1343
|
+
async () => {
|
|
1344
|
+
await userEvent.click(hideLegendBoxCheckbox)
|
|
1345
|
+
},
|
|
1346
|
+
(before, after) => {
|
|
1347
|
+
return !before.hasNoBorder && after.hasNoBorder
|
|
1348
|
+
}
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
// ==========================================================================
|
|
1352
|
+
// TEST: Vertical sorted legend
|
|
1353
|
+
// ==========================================================================
|
|
1354
|
+
// First switch to boxes style and side position to enable vertical sorting
|
|
1355
|
+
await userEvent.selectOptions(legendStyleSelect, 'boxes')
|
|
1356
|
+
await userEvent.selectOptions(legendPositionSelect, 'side')
|
|
1357
|
+
|
|
1358
|
+
const verticalSortedCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]') || []).find(
|
|
1359
|
+
input => {
|
|
1360
|
+
const label = input.closest('label')
|
|
1361
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
1362
|
+
return labelSpan?.textContent?.includes('Vertical sorted legend')
|
|
1363
|
+
}
|
|
1364
|
+
) as HTMLInputElement
|
|
1365
|
+
|
|
1366
|
+
const getVerticalSorted = () => {
|
|
1367
|
+
const legendUl = canvasElement.querySelector('.legend-container__ul')
|
|
1368
|
+
return {
|
|
1369
|
+
hasVerticalSorted: legendUl?.classList.contains('vertical-sorted') ?? false
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
await performAndAssert(
|
|
1374
|
+
'Vertical sorted legend → Check',
|
|
1375
|
+
getVerticalSorted,
|
|
1376
|
+
async () => {
|
|
1377
|
+
await userEvent.click(verticalSortedCheckbox)
|
|
1378
|
+
},
|
|
1379
|
+
(before, after) => {
|
|
1380
|
+
return !before.hasVerticalSorted && after.hasVerticalSorted
|
|
1381
|
+
}
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
await performAndAssert(
|
|
1385
|
+
'Vertical sorted legend → Uncheck',
|
|
1386
|
+
getVerticalSorted,
|
|
1387
|
+
async () => {
|
|
1388
|
+
await userEvent.click(verticalSortedCheckbox)
|
|
1389
|
+
},
|
|
1390
|
+
(before, after) => {
|
|
1391
|
+
return before.hasVerticalSorted && !after.hasVerticalSorted
|
|
1392
|
+
}
|
|
1393
|
+
)
|
|
1394
|
+
|
|
1395
|
+
// NOTE: Show Special Classes Last is skipped because it doesn't produce
|
|
1396
|
+
// visible changes with the current test data (no special classes present)
|
|
1397
|
+
|
|
1398
|
+
// ==========================================================================
|
|
1399
|
+
// TEST: Separate Zero
|
|
1400
|
+
// ==========================================================================
|
|
1401
|
+
const separateZeroCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]') || []).find(
|
|
1402
|
+
input => {
|
|
1403
|
+
const label = input.closest('label')
|
|
1404
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
1405
|
+
return labelSpan?.textContent?.includes('Separate Zero')
|
|
1406
|
+
}
|
|
1407
|
+
) as HTMLInputElement
|
|
1408
|
+
|
|
1409
|
+
const getSeparateZero = () => {
|
|
1410
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
1411
|
+
const legendItems = Array.from(legendContainer?.querySelectorAll('.legend-container__li') || [])
|
|
1412
|
+
const itemLabels = legendItems.map(item => item.textContent?.trim() || '')
|
|
1413
|
+
const hasZeroItem = itemLabels.some(label => label.includes('0') && !label.match(/[1-9]/))
|
|
1414
|
+
return {
|
|
1415
|
+
itemCount: legendItems.length,
|
|
1416
|
+
itemLabels,
|
|
1417
|
+
hasZeroItem
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
await performAndAssert(
|
|
1422
|
+
'Separate Zero → Check',
|
|
1423
|
+
getSeparateZero,
|
|
1424
|
+
async () => {
|
|
1425
|
+
await userEvent.click(separateZeroCheckbox)
|
|
1426
|
+
},
|
|
1427
|
+
(before, after) => {
|
|
1428
|
+
// Should separate zero into its own legend item (e.g., "0" instead of "0 - 40")
|
|
1429
|
+
return after.hasZeroItem && after.itemLabels.join(',') !== before.itemLabels.join(',')
|
|
1430
|
+
}
|
|
1431
|
+
)
|
|
1432
|
+
|
|
1433
|
+
await performAndAssert(
|
|
1434
|
+
'Separate Zero → Uncheck',
|
|
1435
|
+
getSeparateZero,
|
|
1436
|
+
async () => {
|
|
1437
|
+
await userEvent.click(separateZeroCheckbox)
|
|
1438
|
+
},
|
|
1439
|
+
(before, after) => {
|
|
1440
|
+
// Should merge zero back into a range
|
|
1441
|
+
return !after.hasZeroItem && after.itemLabels.join(',') !== before.itemLabels.join(',')
|
|
1442
|
+
}
|
|
1443
|
+
)
|
|
1444
|
+
|
|
1445
|
+
// ==========================================================================
|
|
1446
|
+
// TEST: Number of Items
|
|
1447
|
+
// ==========================================================================
|
|
1448
|
+
const numberOfItemsSelect = Array.from(canvasElement.querySelectorAll('select') || []).find(select => {
|
|
1449
|
+
const label = select.closest('label')
|
|
1450
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
1451
|
+
return labelSpan?.textContent?.includes('Number of Items')
|
|
1452
|
+
}) as HTMLSelectElement
|
|
1453
|
+
|
|
1454
|
+
const getNumberOfItems = () => {
|
|
1455
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
1456
|
+
const legendItems = legendContainer?.querySelectorAll('.legend-container__li')
|
|
1457
|
+
return {
|
|
1458
|
+
itemCount: legendItems?.length ?? 0,
|
|
1459
|
+
legendHTML: legendContainer?.innerHTML || ''
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
await performAndAssert(
|
|
1464
|
+
'Number of Items → Set to 5',
|
|
1465
|
+
getNumberOfItems,
|
|
1466
|
+
async () => {
|
|
1467
|
+
await userEvent.selectOptions(numberOfItemsSelect, '5')
|
|
1468
|
+
},
|
|
1469
|
+
(before, after) => {
|
|
1470
|
+
// Should have 5 legend items
|
|
1471
|
+
return after.itemCount === 5 && after.itemCount !== before.itemCount
|
|
1472
|
+
}
|
|
1473
|
+
)
|
|
1474
|
+
|
|
1475
|
+
await performAndAssert(
|
|
1476
|
+
'Number of Items → Set to 3',
|
|
1477
|
+
getNumberOfItems,
|
|
1478
|
+
async () => {
|
|
1479
|
+
await userEvent.selectOptions(numberOfItemsSelect, '3')
|
|
1480
|
+
},
|
|
1481
|
+
(before, after) => {
|
|
1482
|
+
// Should have 3 legend items
|
|
1483
|
+
return after.itemCount === 3 && after.itemCount !== before.itemCount
|
|
1484
|
+
}
|
|
1485
|
+
)
|
|
1486
|
+
|
|
1487
|
+
// Switch back to gradient for the legend title test
|
|
1488
|
+
await userEvent.selectOptions(legendPositionSelect, 'bottom')
|
|
1489
|
+
await userEvent.selectOptions(legendStyleSelect, 'gradient')
|
|
1490
|
+
|
|
1491
|
+
// ==========================================================================
|
|
1492
|
+
// TEST: Legend Title
|
|
1493
|
+
// ==========================================================================
|
|
1494
|
+
const legendTitleInput = Array.from(canvasElement.querySelectorAll('input[type="text"]') || []).find(input => {
|
|
1495
|
+
const label = input.closest('label')
|
|
1496
|
+
return label?.textContent?.includes('Legend Title')
|
|
1497
|
+
}) as HTMLInputElement
|
|
1498
|
+
|
|
1499
|
+
const getLegendTitle = () => {
|
|
1500
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
1501
|
+
const titleElement = legendContainer?.querySelector('.legend-container__title, .legend-title')
|
|
1502
|
+
return {
|
|
1503
|
+
titleText: titleElement?.textContent?.trim() || '',
|
|
1504
|
+
titleExists: Boolean(titleElement),
|
|
1505
|
+
legendHTML: legendContainer?.innerHTML || ''
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
await performAndAssert(
|
|
1510
|
+
'Legend Title → Set custom title',
|
|
1511
|
+
getLegendTitle,
|
|
1512
|
+
async () => {
|
|
1513
|
+
await userEvent.clear(legendTitleInput)
|
|
1514
|
+
await userEvent.type(legendTitleInput, 'Custom Legend Title')
|
|
1515
|
+
},
|
|
1516
|
+
(before, after) => {
|
|
1517
|
+
return after.titleText.includes('Custom Legend Title') && after.legendHTML !== before.legendHTML
|
|
1518
|
+
}
|
|
1519
|
+
)
|
|
1520
|
+
|
|
1521
|
+
// ==========================================================================
|
|
1522
|
+
// TEST: Legend Description
|
|
1523
|
+
// ==========================================================================
|
|
1524
|
+
const legendDescriptionTextarea = Array.from(canvasElement.querySelectorAll('textarea') || []).find(textarea => {
|
|
1525
|
+
const label = textarea.closest('label')
|
|
1526
|
+
return label?.textContent?.includes('Legend Description')
|
|
1527
|
+
}) as HTMLTextAreaElement
|
|
1528
|
+
|
|
1529
|
+
const getLegendDescription = () => {
|
|
1530
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
1531
|
+
const descriptionElement = legendContainer?.querySelector('.legend-container__description, .legend-description')
|
|
1532
|
+
return {
|
|
1533
|
+
descriptionText: descriptionElement?.textContent?.trim() || '',
|
|
1534
|
+
descriptionExists: Boolean(descriptionElement),
|
|
1535
|
+
legendHTML: legendContainer?.innerHTML || ''
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
await performAndAssert(
|
|
1540
|
+
'Legend Description → Set custom description',
|
|
1541
|
+
getLegendDescription,
|
|
1542
|
+
async () => {
|
|
1543
|
+
await userEvent.clear(legendDescriptionTextarea)
|
|
1544
|
+
await userEvent.type(legendDescriptionTextarea, 'This is a custom legend description for testing.')
|
|
1545
|
+
},
|
|
1546
|
+
(before, after) => {
|
|
1547
|
+
return (
|
|
1548
|
+
after.descriptionText.includes('This is a custom legend description') &&
|
|
1549
|
+
after.legendHTML !== before.legendHTML
|
|
1550
|
+
)
|
|
1551
|
+
}
|
|
1552
|
+
)
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
export const FiltersSectionTests: Story = {
|
|
1557
|
+
args: {
|
|
1558
|
+
...DEFAULT_ARGS
|
|
1559
|
+
},
|
|
1560
|
+
play: async ({ canvasElement }) => {
|
|
1561
|
+
const canvas = within(canvasElement)
|
|
1562
|
+
|
|
1563
|
+
await waitForEditor(canvas)
|
|
1564
|
+
await waitForPresence('.map-container', canvasElement)
|
|
1565
|
+
|
|
1566
|
+
await openAccordion(canvas, 'Filters')
|
|
1567
|
+
|
|
1568
|
+
// ==========================================================================
|
|
1569
|
+
// TEST: Add Filter and configure it to filter by STATE
|
|
1570
|
+
// ==========================================================================
|
|
1571
|
+
const addFilterButton = canvas.getByRole('button', { name: /Add Filter/i })
|
|
1572
|
+
|
|
1573
|
+
await performAndAssert(
|
|
1574
|
+
'Add Filter → Click button',
|
|
1575
|
+
() => {
|
|
1576
|
+
const filtersList = canvasElement.querySelector('.filters-list')
|
|
1577
|
+
// Count all filter items (both collapsed and expanded)
|
|
1578
|
+
const allFilterItems = Array.from(filtersList?.querySelectorAll('li, .edit-block, .mb-1') || [])
|
|
1579
|
+
const collapsedFilters = filtersList?.querySelectorAll('.mb-1:has(button)') || []
|
|
1580
|
+
return {
|
|
1581
|
+
hasFiltersList: Boolean(filtersList),
|
|
1582
|
+
hasCollapsedFilter: collapsedFilters.length > 0
|
|
1583
|
+
}
|
|
1584
|
+
},
|
|
1585
|
+
async () => {
|
|
1586
|
+
await userEvent.click(addFilterButton)
|
|
1587
|
+
},
|
|
1588
|
+
(before, after) => {
|
|
1589
|
+
// Should create filters list and add a collapsed filter
|
|
1590
|
+
return after.hasFiltersList && after.hasCollapsedFilter
|
|
1591
|
+
}
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
// Find and expand the collapsed filter
|
|
1595
|
+
const filtersList = canvasElement.querySelector('.filters-list')
|
|
1596
|
+
const expandButton = filtersList?.querySelector('.mb-1 button') as HTMLButtonElement
|
|
1597
|
+
await userEvent.click(expandButton)
|
|
1598
|
+
|
|
1599
|
+
// Wait for the expanded filter block
|
|
1600
|
+
await waitForPresence('.filters-list .edit-block', canvasElement)
|
|
1601
|
+
|
|
1602
|
+
// Find the newly added filter section
|
|
1603
|
+
const filterBlock = filtersList?.querySelector('.edit-block') as HTMLElement
|
|
1604
|
+
|
|
1605
|
+
// ==========================================================================
|
|
1606
|
+
// TEST: Select STATE as the filter column
|
|
1607
|
+
// ==========================================================================
|
|
1608
|
+
const filterColumnSelect = Array.from(filterBlock?.querySelectorAll('select') || []).find(select => {
|
|
1609
|
+
const label = select.closest('label')
|
|
1610
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
1611
|
+
return labelSpan?.textContent?.includes('Filter') && !labelSpan?.textContent?.includes('Style')
|
|
1612
|
+
}) as HTMLSelectElement
|
|
1613
|
+
|
|
1614
|
+
const getDefaultValueState = () => {
|
|
1615
|
+
const updatedFilterBlock = filtersList?.querySelector('.edit-block') as HTMLElement
|
|
1616
|
+
const defaultValueSelect = Array.from(updatedFilterBlock?.querySelectorAll('select') || []).find(select => {
|
|
1617
|
+
const label = select.closest('label')
|
|
1618
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
1619
|
+
return labelSpan?.textContent?.includes('Filter Default Value')
|
|
1620
|
+
}) as HTMLSelectElement
|
|
1621
|
+
return {
|
|
1622
|
+
hasDefaultValueSelect: Boolean(defaultValueSelect),
|
|
1623
|
+
optionCount: defaultValueSelect?.options.length || 0
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
await performAndAssert(
|
|
1628
|
+
'Filter Column → Select STATE',
|
|
1629
|
+
getDefaultValueState,
|
|
1630
|
+
async () => {
|
|
1631
|
+
await userEvent.selectOptions(filterColumnSelect, 'STATE')
|
|
1632
|
+
},
|
|
1633
|
+
(before, after) => {
|
|
1634
|
+
// Should populate the default value select with state options
|
|
1635
|
+
return after.hasDefaultValueSelect && after.optionCount > 0
|
|
1636
|
+
}
|
|
1637
|
+
)
|
|
1638
|
+
|
|
1639
|
+
// ==========================================================================
|
|
1640
|
+
// TEST: Filter Intro Text
|
|
1641
|
+
// ==========================================================================
|
|
1642
|
+
const filterIntroTextarea = Array.from(canvasElement.querySelectorAll('textarea') || []).find(textarea => {
|
|
1643
|
+
const label = textarea.closest('label')
|
|
1644
|
+
return label?.textContent?.includes('Filter intro text')
|
|
1645
|
+
}) as HTMLTextAreaElement
|
|
1646
|
+
|
|
1647
|
+
const getFilterIntroText = () => {
|
|
1648
|
+
const filtersSection = canvasElement.querySelector('.filters-section')
|
|
1649
|
+
const filterIntro = filtersSection?.querySelector('.filters-section__intro-text')
|
|
1650
|
+
return {
|
|
1651
|
+
introText: filterIntro?.textContent?.trim() || '',
|
|
1652
|
+
hasIntro: Boolean(filterIntro)
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
await performAndAssert(
|
|
1657
|
+
'Filter Intro Text → Set custom text',
|
|
1658
|
+
getFilterIntroText,
|
|
1659
|
+
async () => {
|
|
1660
|
+
await userEvent.clear(filterIntroTextarea)
|
|
1661
|
+
await userEvent.type(filterIntroTextarea, 'Select a state to filter the map data.')
|
|
1662
|
+
},
|
|
1663
|
+
(before, after) => {
|
|
1664
|
+
return after.hasIntro && after.introText.includes('Select a state to filter the map data')
|
|
1665
|
+
}
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
// ==========================================================================
|
|
1669
|
+
// TEST: Select Alabama as the default filter value
|
|
1670
|
+
// ==========================================================================
|
|
1671
|
+
const updatedFilterBlock = filtersList?.querySelector('.edit-block') as HTMLElement
|
|
1672
|
+
const defaultValueSelect = Array.from(updatedFilterBlock?.querySelectorAll('select') || []).find(select => {
|
|
1673
|
+
const label = select.closest('label')
|
|
1674
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
1675
|
+
return labelSpan?.textContent?.includes('Filter Default Value')
|
|
1676
|
+
}) as HTMLSelectElement
|
|
1677
|
+
|
|
1678
|
+
await performAndAssert(
|
|
1679
|
+
'Filter Default Value → Select Alabama',
|
|
1680
|
+
() => {
|
|
1681
|
+
// Check the legend text labels - when filtered to one state, shows single value
|
|
1682
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
1683
|
+
const textElements = Array.from(legendContainer?.querySelectorAll('text tspan') || [])
|
|
1684
|
+
const labels = textElements.map(el => el.textContent?.trim()).filter(t => t && t !== '')
|
|
1685
|
+
const labelText = labels.join(',')
|
|
1686
|
+
|
|
1687
|
+
// Verify filter dropdown is rendered
|
|
1688
|
+
const vizContainer = canvasElement.querySelector('.cdc-open-viz-module')
|
|
1689
|
+
const filterSelect = Array.from(vizContainer?.querySelectorAll('select') || []).find(select => {
|
|
1690
|
+
const options = Array.from(select.options).map(opt => opt.value)
|
|
1691
|
+
return options.includes('Alabama')
|
|
1692
|
+
})
|
|
1693
|
+
|
|
1694
|
+
return {
|
|
1695
|
+
labelCount: labels.length,
|
|
1696
|
+
labelText,
|
|
1697
|
+
hasFilterSelect: Boolean(filterSelect),
|
|
1698
|
+
selectedValue: filterSelect?.value || ''
|
|
1699
|
+
}
|
|
1700
|
+
},
|
|
1701
|
+
async () => {
|
|
1702
|
+
await userEvent.selectOptions(defaultValueSelect, 'Alabama')
|
|
1703
|
+
},
|
|
1704
|
+
(before, after) => {
|
|
1705
|
+
// After filtering to Alabama: should have filter dropdown, Alabama selected, and exactly 1 legend label
|
|
1706
|
+
// The legend shows a single value when only one state's data is displayed
|
|
1707
|
+
return after.hasFilterSelect && after.selectedValue === 'Alabama' && after.labelCount === 1
|
|
1708
|
+
}
|
|
1709
|
+
)
|
|
1710
|
+
|
|
1711
|
+
// ==========================================================================
|
|
1712
|
+
// TEST: Filter Label
|
|
1713
|
+
// ==========================================================================
|
|
1714
|
+
const labelInput = Array.from(updatedFilterBlock?.querySelectorAll('input[type="text"]') || []).find(input => {
|
|
1715
|
+
const label = input.closest('label')
|
|
1716
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
1717
|
+
return labelSpan?.textContent?.includes('Label') && !labelSpan?.textContent?.includes('Query')
|
|
1718
|
+
}) as HTMLInputElement
|
|
1719
|
+
|
|
1720
|
+
await performAndAssert(
|
|
1721
|
+
'Filter Label → Set custom label',
|
|
1722
|
+
() => {
|
|
1723
|
+
const filtersSection = canvasElement.querySelector('.filters-section')
|
|
1724
|
+
const filterLabel = filtersSection?.querySelector('.form-group label')
|
|
1725
|
+
return {
|
|
1726
|
+
labelText: filterLabel?.textContent?.trim() || ''
|
|
1727
|
+
}
|
|
1728
|
+
},
|
|
1729
|
+
async () => {
|
|
1730
|
+
await userEvent.clear(labelInput)
|
|
1731
|
+
await userEvent.type(labelInput, 'Select State')
|
|
1732
|
+
},
|
|
1733
|
+
(before, after) => {
|
|
1734
|
+
return after.labelText.includes('Select State')
|
|
1735
|
+
}
|
|
1736
|
+
)
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
export const DataTableSectionTests: Story = {
|
|
1741
|
+
args: {
|
|
1742
|
+
...DEFAULT_ARGS
|
|
1743
|
+
},
|
|
1744
|
+
play: async ({ canvasElement }) => {
|
|
1745
|
+
const canvas = within(canvasElement)
|
|
1746
|
+
|
|
1747
|
+
await waitForEditor(canvas)
|
|
1748
|
+
await waitForPresence('.map-container', canvasElement)
|
|
1749
|
+
|
|
1750
|
+
await openAccordion(canvas, 'Data Table')
|
|
1751
|
+
|
|
1752
|
+
// ==========================================================================
|
|
1753
|
+
// TEST: Data Table Title
|
|
1754
|
+
// ==========================================================================
|
|
1755
|
+
const dataTableTitleInput = canvasElement.querySelector('#dataTableTitle') as HTMLInputElement
|
|
1756
|
+
|
|
1757
|
+
await performAndAssert(
|
|
1758
|
+
'Data Table Title → Set custom title',
|
|
1759
|
+
() => {
|
|
1760
|
+
const dataTableHeading = canvasElement.querySelector('.data-table-heading')
|
|
1761
|
+
return {
|
|
1762
|
+
titleText: dataTableHeading?.textContent?.trim() || ''
|
|
1763
|
+
}
|
|
1764
|
+
},
|
|
1765
|
+
async () => {
|
|
1766
|
+
await userEvent.clear(dataTableTitleInput)
|
|
1767
|
+
await userEvent.type(dataTableTitleInput, 'State Population Data')
|
|
1768
|
+
},
|
|
1769
|
+
(before, after) => {
|
|
1770
|
+
return after.titleText.includes('State Population Data')
|
|
1771
|
+
}
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
// ==========================================================================
|
|
1775
|
+
// TEST: Wrap Data Table Columns
|
|
1776
|
+
// ==========================================================================
|
|
1777
|
+
// First expand the data table if it's collapsed
|
|
1778
|
+
const dataTableHeadingButton = canvasElement.querySelector('.data-table-heading') as HTMLElement
|
|
1779
|
+
const dataTableContainer = canvasElement.querySelector('.data-table-container')
|
|
1780
|
+
const isCollapsed = dataTableHeadingButton?.classList.contains('collapsed')
|
|
1781
|
+
if (isCollapsed) {
|
|
1782
|
+
await userEvent.click(dataTableHeadingButton)
|
|
1783
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
const wrapColumnsCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(checkbox => {
|
|
1787
|
+
const label = checkbox.closest('label')
|
|
1788
|
+
return label?.textContent?.includes('WRAP DATA TABLE COLUMNS')
|
|
1789
|
+
}) as HTMLInputElement
|
|
1790
|
+
|
|
1791
|
+
await performAndAssert(
|
|
1792
|
+
'Wrap Columns → Toggle wrapping',
|
|
1793
|
+
() => {
|
|
1794
|
+
const dataTable = canvasElement.querySelector('.data-table')
|
|
1795
|
+
const firstCell = dataTable?.querySelector('tbody td')
|
|
1796
|
+
const whiteSpace = firstCell ? window.getComputedStyle(firstCell).whiteSpace : ''
|
|
1797
|
+
return {
|
|
1798
|
+
whiteSpace,
|
|
1799
|
+
hasCells: Boolean(firstCell)
|
|
1800
|
+
}
|
|
1801
|
+
},
|
|
1802
|
+
async () => {
|
|
1803
|
+
await userEvent.click(wrapColumnsCheckbox)
|
|
1804
|
+
},
|
|
1805
|
+
(before, after) => {
|
|
1806
|
+
// When wrap is enabled, white-space should be 'normal' or 'unset', not 'nowrap'
|
|
1807
|
+
return after.hasCells && before.whiteSpace === 'nowrap' && after.whiteSpace !== 'nowrap'
|
|
1808
|
+
}
|
|
1809
|
+
)
|
|
1810
|
+
|
|
1811
|
+
// ==========================================================================
|
|
1812
|
+
// TEST: Show Data Table
|
|
1813
|
+
// ==========================================================================
|
|
1814
|
+
const showDataTableCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(
|
|
1815
|
+
checkbox => {
|
|
1816
|
+
const label = checkbox.closest('label')
|
|
1817
|
+
return label?.textContent?.includes('Show Data Table') && !label?.textContent?.includes('Non Geographic')
|
|
1818
|
+
}
|
|
1819
|
+
) as HTMLInputElement
|
|
1820
|
+
|
|
1821
|
+
await performAndAssert(
|
|
1822
|
+
'Show Data Table → Hide data table',
|
|
1823
|
+
() => {
|
|
1824
|
+
const dataTableContainer = canvasElement.querySelector('.data-table-container')
|
|
1825
|
+
const isVisible = dataTableContainer && window.getComputedStyle(dataTableContainer).display !== 'none'
|
|
1826
|
+
return {
|
|
1827
|
+
isVisible: Boolean(isVisible)
|
|
1828
|
+
}
|
|
1829
|
+
},
|
|
1830
|
+
async () => {
|
|
1831
|
+
await userEvent.click(showDataTableCheckbox)
|
|
1832
|
+
},
|
|
1833
|
+
(before, after) => {
|
|
1834
|
+
// Should hide the data table
|
|
1835
|
+
return before.isVisible && !after.isVisible
|
|
1836
|
+
}
|
|
1837
|
+
)
|
|
1838
|
+
|
|
1839
|
+
// Toggle it back on for the next test
|
|
1840
|
+
await userEvent.click(showDataTableCheckbox)
|
|
1841
|
+
|
|
1842
|
+
// ==========================================================================
|
|
1843
|
+
// TEST: Show Non Geographic Data
|
|
1844
|
+
// ==========================================================================
|
|
1845
|
+
const showNonGeoDataCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(
|
|
1846
|
+
checkbox => {
|
|
1847
|
+
const label = checkbox.closest('label')
|
|
1848
|
+
return label?.textContent?.includes('Show Non Geographic Data')
|
|
1849
|
+
}
|
|
1850
|
+
) as HTMLInputElement
|
|
1851
|
+
|
|
1852
|
+
await performAndAssert(
|
|
1853
|
+
'Show Non Geographic Data → Toggle visibility',
|
|
1854
|
+
() => {
|
|
1855
|
+
const dataTable = canvasElement.querySelector('.data-table')
|
|
1856
|
+
const rows = Array.from(dataTable?.querySelectorAll('tbody tr') || [])
|
|
1857
|
+
const rowCount = rows.length
|
|
1858
|
+
return {
|
|
1859
|
+
rowCount
|
|
1860
|
+
}
|
|
1861
|
+
},
|
|
1862
|
+
async () => {
|
|
1863
|
+
await userEvent.click(showNonGeoDataCheckbox)
|
|
1864
|
+
},
|
|
1865
|
+
(before, after) => {
|
|
1866
|
+
// Toggling non-geographic data should add rows to the table (overall data object)
|
|
1867
|
+
return after.rowCount > before.rowCount
|
|
1868
|
+
}
|
|
1869
|
+
)
|
|
1870
|
+
|
|
1871
|
+
// ==========================================================================
|
|
1872
|
+
// TEST: Index Column Header
|
|
1873
|
+
// ==========================================================================
|
|
1874
|
+
const indexColumnHeaderInput = Array.from(canvasElement.querySelectorAll('input[type="text"]')).find(input => {
|
|
1875
|
+
const label = input.closest('label')
|
|
1876
|
+
return label?.textContent?.includes('Index Column Header')
|
|
1877
|
+
}) as HTMLInputElement
|
|
1878
|
+
|
|
1879
|
+
await performAndAssert(
|
|
1880
|
+
'Index Column Header → Set custom header',
|
|
1881
|
+
() => {
|
|
1882
|
+
const dataTable = canvasElement.querySelector('.data-table')
|
|
1883
|
+
const firstHeader = dataTable?.querySelector('thead th')
|
|
1884
|
+
return {
|
|
1885
|
+
headerText: firstHeader?.textContent?.trim() || ''
|
|
1886
|
+
}
|
|
1887
|
+
},
|
|
1888
|
+
async () => {
|
|
1889
|
+
await userEvent.clear(indexColumnHeaderInput)
|
|
1890
|
+
await userEvent.type(indexColumnHeaderInput, 'State/Territory')
|
|
1891
|
+
},
|
|
1892
|
+
(before, after) => {
|
|
1893
|
+
return after.headerText.includes('State/Territory')
|
|
1894
|
+
}
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1897
|
+
// ==========================================================================
|
|
1898
|
+
// TEST: Screen Reader Description
|
|
1899
|
+
// ==========================================================================
|
|
1900
|
+
const screenReaderDescTextarea = Array.from(canvasElement.querySelectorAll('textarea')).find(textarea => {
|
|
1901
|
+
const label = textarea.closest('label')
|
|
1902
|
+
return label?.textContent?.includes('Screen Reader Description')
|
|
1903
|
+
}) as HTMLTextAreaElement
|
|
1904
|
+
|
|
1905
|
+
await performAndAssert(
|
|
1906
|
+
'Screen Reader Description → Set custom description',
|
|
1907
|
+
() => {
|
|
1908
|
+
const dataTable = canvasElement.querySelector('.data-table')
|
|
1909
|
+
const caption = dataTable?.querySelector('caption')
|
|
1910
|
+
return {
|
|
1911
|
+
captionText: caption?.textContent?.trim() || ''
|
|
1912
|
+
}
|
|
1913
|
+
},
|
|
1914
|
+
async () => {
|
|
1915
|
+
await userEvent.clear(screenReaderDescTextarea)
|
|
1916
|
+
await userEvent.type(screenReaderDescTextarea, 'Table showing state population data by region')
|
|
1917
|
+
},
|
|
1918
|
+
(before, after) => {
|
|
1919
|
+
return after.captionText.includes('Table showing state population data')
|
|
1920
|
+
}
|
|
1921
|
+
)
|
|
1922
|
+
|
|
1923
|
+
// ==========================================================================
|
|
1924
|
+
// TEST: Limit Table Height
|
|
1925
|
+
// ==========================================================================
|
|
1926
|
+
const limitHeightCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(checkbox => {
|
|
1927
|
+
const label = checkbox.closest('label')
|
|
1928
|
+
return label?.textContent?.includes('Limit Table Height')
|
|
1929
|
+
}) as HTMLInputElement
|
|
1930
|
+
|
|
1931
|
+
await performAndAssert(
|
|
1932
|
+
'Limit Table Height → Enable height limit',
|
|
1933
|
+
() => {
|
|
1934
|
+
// Check if the "Data Table Height" input field appears (conditional rendering)
|
|
1935
|
+
const heightInput = Array.from(canvasElement.querySelectorAll('input[type="number"]')).find(input => {
|
|
1936
|
+
const label = input.closest('label')
|
|
1937
|
+
return label?.textContent?.includes('Data Table Height')
|
|
1938
|
+
})
|
|
1939
|
+
return {
|
|
1940
|
+
hasHeightInput: Boolean(heightInput)
|
|
1941
|
+
}
|
|
1942
|
+
},
|
|
1943
|
+
async () => {
|
|
1944
|
+
await userEvent.click(limitHeightCheckbox)
|
|
1945
|
+
},
|
|
1946
|
+
(before, after) => {
|
|
1947
|
+
// After enabling, the "Data Table Height" input field should appear
|
|
1948
|
+
return !before.hasHeightInput && after.hasHeightInput
|
|
1949
|
+
}
|
|
1950
|
+
)
|
|
1951
|
+
|
|
1952
|
+
// ==========================================================================
|
|
1953
|
+
// TEST: Table Cell Min Width
|
|
1954
|
+
// ==========================================================================
|
|
1955
|
+
const cellMinWidthInput = Array.from(canvasElement.querySelectorAll('input[type="number"]')).find(input => {
|
|
1956
|
+
const label = input.closest('label')
|
|
1957
|
+
return label?.textContent?.includes('Table Cell Min Width')
|
|
1958
|
+
}) as HTMLInputElement
|
|
1959
|
+
|
|
1960
|
+
await performAndAssert(
|
|
1961
|
+
'Table Cell Min Width → Set minimum width',
|
|
1962
|
+
() => {
|
|
1963
|
+
const dataTable = canvasElement.querySelector('.data-table')
|
|
1964
|
+
const firstCell = dataTable?.querySelector('thead th')
|
|
1965
|
+
const minWidth = firstCell ? window.getComputedStyle(firstCell).minWidth : ''
|
|
1966
|
+
return {
|
|
1967
|
+
minWidth
|
|
1968
|
+
}
|
|
1969
|
+
},
|
|
1970
|
+
async () => {
|
|
1971
|
+
await userEvent.clear(cellMinWidthInput)
|
|
1972
|
+
await userEvent.type(cellMinWidthInput, '150')
|
|
1973
|
+
},
|
|
1974
|
+
(before, after) => {
|
|
1975
|
+
// After setting min width to 150px, cells should have minWidth of 150px
|
|
1976
|
+
return before.minWidth !== after.minWidth && after.minWidth === '150px'
|
|
1977
|
+
}
|
|
1978
|
+
)
|
|
1979
|
+
|
|
1980
|
+
// ==========================================================================
|
|
1981
|
+
// TEST: Show Download CSV Link
|
|
1982
|
+
// ==========================================================================
|
|
1983
|
+
const showDownloadCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(checkbox => {
|
|
1984
|
+
const label = checkbox.closest('label')
|
|
1985
|
+
return label?.textContent?.includes('Show Download CSV Link')
|
|
1986
|
+
}) as HTMLInputElement
|
|
1987
|
+
|
|
1988
|
+
// First toggle it off, then back on
|
|
1989
|
+
await performAndAssert(
|
|
1990
|
+
'Show Download CSV Link → Toggle off',
|
|
1991
|
+
() => {
|
|
1992
|
+
const downloadLink = Array.from(canvasElement.querySelectorAll('a')).find(link =>
|
|
1993
|
+
link.textContent?.includes('Download Data')
|
|
1994
|
+
)
|
|
1995
|
+
return {
|
|
1996
|
+
hasDownloadLink: Boolean(downloadLink)
|
|
1997
|
+
}
|
|
1998
|
+
},
|
|
1999
|
+
async () => {
|
|
2000
|
+
await userEvent.click(showDownloadCheckbox)
|
|
2001
|
+
},
|
|
2002
|
+
(before, after) => {
|
|
2003
|
+
// After disabling, the download link should disappear
|
|
2004
|
+
return before.hasDownloadLink && !after.hasDownloadLink
|
|
2005
|
+
}
|
|
2006
|
+
)
|
|
2007
|
+
|
|
2008
|
+
await performAndAssert(
|
|
2009
|
+
'Show Download CSV Link → Toggle back on',
|
|
2010
|
+
() => {
|
|
2011
|
+
const downloadLink = Array.from(canvasElement.querySelectorAll('a')).find(link =>
|
|
2012
|
+
link.textContent?.includes('Download Data')
|
|
2013
|
+
)
|
|
2014
|
+
return {
|
|
2015
|
+
hasDownloadLink: Boolean(downloadLink)
|
|
2016
|
+
}
|
|
2017
|
+
},
|
|
2018
|
+
async () => {
|
|
2019
|
+
await userEvent.click(showDownloadCheckbox)
|
|
2020
|
+
},
|
|
2021
|
+
(before, after) => {
|
|
2022
|
+
// After re-enabling, the download link should reappear
|
|
2023
|
+
return !before.hasDownloadLink && after.hasDownloadLink
|
|
2024
|
+
}
|
|
2025
|
+
)
|
|
2026
|
+
|
|
2027
|
+
// ==========================================================================
|
|
2028
|
+
// TEST: Show Link Below Table
|
|
2029
|
+
// ==========================================================================
|
|
2030
|
+
const showLinkBelowCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(
|
|
2031
|
+
checkbox => {
|
|
2032
|
+
const label = checkbox.closest('label')
|
|
2033
|
+
return label?.textContent?.includes('Show Link Below Table')
|
|
2034
|
+
}
|
|
2035
|
+
) as HTMLInputElement
|
|
2036
|
+
|
|
2037
|
+
// First toggle it off (move link above), then back on (move link below)
|
|
2038
|
+
await performAndAssert(
|
|
2039
|
+
'Show Link Below Table → Toggle off (move above)',
|
|
2040
|
+
() => {
|
|
2041
|
+
const dataTableContainer = canvasElement.querySelector('.data-table-container')
|
|
2042
|
+
const downloadSection = canvasElement.querySelector('.download-links')
|
|
2043
|
+
// Check if download link is positioned after the data table
|
|
2044
|
+
const containerRect = dataTableContainer?.getBoundingClientRect()
|
|
2045
|
+
const downloadRect = downloadSection?.getBoundingClientRect()
|
|
2046
|
+
const isBelow = downloadRect && containerRect && downloadRect.top > containerRect.bottom
|
|
2047
|
+
return {
|
|
2048
|
+
isBelow: Boolean(isBelow)
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
async () => {
|
|
2052
|
+
await userEvent.click(showLinkBelowCheckbox)
|
|
2053
|
+
},
|
|
2054
|
+
(before, after) => {
|
|
2055
|
+
// After disabling, download link should move above the table
|
|
2056
|
+
return before.isBelow && !after.isBelow
|
|
2057
|
+
}
|
|
2058
|
+
)
|
|
2059
|
+
|
|
2060
|
+
await performAndAssert(
|
|
2061
|
+
'Show Link Below Table → Toggle back on (move below)',
|
|
2062
|
+
() => {
|
|
2063
|
+
const dataTableContainer = canvasElement.querySelector('.data-table-container')
|
|
2064
|
+
const downloadSection = canvasElement.querySelector('.download-links')
|
|
2065
|
+
const containerRect = dataTableContainer?.getBoundingClientRect()
|
|
2066
|
+
const downloadRect = downloadSection?.getBoundingClientRect()
|
|
2067
|
+
const isBelow = downloadRect && containerRect && downloadRect.top > containerRect.bottom
|
|
2068
|
+
return {
|
|
2069
|
+
isBelow: Boolean(isBelow)
|
|
2070
|
+
}
|
|
2071
|
+
},
|
|
2072
|
+
async () => {
|
|
2073
|
+
await userEvent.click(showLinkBelowCheckbox)
|
|
2074
|
+
},
|
|
2075
|
+
(before, after) => {
|
|
2076
|
+
// After re-enabling, download link should move back below the table
|
|
2077
|
+
return !before.isBelow && after.isBelow
|
|
2078
|
+
}
|
|
2079
|
+
)
|
|
2080
|
+
|
|
2081
|
+
// ==========================================================================
|
|
2082
|
+
// TEST: Enable Image Download
|
|
2083
|
+
// ==========================================================================
|
|
2084
|
+
const enableImageDownloadCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(
|
|
2085
|
+
checkbox => {
|
|
2086
|
+
const label = checkbox.closest('label')
|
|
2087
|
+
return label?.textContent?.includes('Enable Image Download')
|
|
2088
|
+
}
|
|
2089
|
+
) as HTMLInputElement
|
|
2090
|
+
|
|
2091
|
+
await performAndAssert(
|
|
2092
|
+
'Enable Image Download → Enable button',
|
|
2093
|
+
() => {
|
|
2094
|
+
const downloadImgButton = Array.from(canvasElement.querySelectorAll('button')).find(
|
|
2095
|
+
btn => btn.textContent?.includes('Download Image') || btn.classList.contains('download-image')
|
|
2096
|
+
)
|
|
2097
|
+
return {
|
|
2098
|
+
hasDownloadImgButton: Boolean(downloadImgButton)
|
|
2099
|
+
}
|
|
2100
|
+
},
|
|
2101
|
+
async () => {
|
|
2102
|
+
await userEvent.click(enableImageDownloadCheckbox)
|
|
2103
|
+
},
|
|
2104
|
+
(before, after) => {
|
|
2105
|
+
// After enabling, the download image button should appear
|
|
2106
|
+
return !before.hasDownloadImgButton && after.hasDownloadImgButton
|
|
2107
|
+
}
|
|
2108
|
+
)
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// =================================================================================================
|
|
2113
|
+
// Visual Section Tests
|
|
2114
|
+
// =================================================================================================
|
|
2115
|
+
export const VisualSectionTests: StoryObj<typeof CdcMap> = {
|
|
2116
|
+
name: 'Visual Section Tests',
|
|
2117
|
+
args: {
|
|
2118
|
+
...DEFAULT_ARGS
|
|
2119
|
+
},
|
|
2120
|
+
play: async ({ canvasElement }) => {
|
|
2121
|
+
const canvas = within(canvasElement)
|
|
2122
|
+
|
|
2123
|
+
// Wait for the editor to load
|
|
2124
|
+
await waitForEditor(canvas)
|
|
2125
|
+
|
|
2126
|
+
// Open the Visual accordion
|
|
2127
|
+
await openAccordion(canvas, 'Visual')
|
|
2128
|
+
|
|
2129
|
+
// ==========================================================================
|
|
2130
|
+
// TEST: Header Theme
|
|
2131
|
+
// ==========================================================================
|
|
2132
|
+
await performAndAssert(
|
|
2133
|
+
'Header Theme → Select purple theme',
|
|
2134
|
+
() => {
|
|
2135
|
+
const innerContainer = canvasElement.querySelector('.cdc-map-inner-container')
|
|
2136
|
+
const currentTheme = Array.from(innerContainer?.classList || []).find(cls => cls.startsWith('theme-'))
|
|
2137
|
+
return {
|
|
2138
|
+
currentTheme: currentTheme || ''
|
|
2139
|
+
}
|
|
2140
|
+
},
|
|
2141
|
+
async () => {
|
|
2142
|
+
// Find the color palette and click on the purple theme button
|
|
2143
|
+
const colorPalette = canvasElement.querySelector('.color-palette')
|
|
2144
|
+
const purpleTheme = Array.from(colorPalette?.querySelectorAll('button') || []).find(button =>
|
|
2145
|
+
button.classList.contains('theme-purple')
|
|
2146
|
+
) as HTMLElement
|
|
2147
|
+
await userEvent.click(purpleTheme)
|
|
2148
|
+
},
|
|
2149
|
+
(before, after) => {
|
|
2150
|
+
// After clicking, the header should have the purple theme
|
|
2151
|
+
return after.currentTheme === 'theme-purple'
|
|
2152
|
+
}
|
|
2153
|
+
)
|
|
2154
|
+
|
|
2155
|
+
// ==========================================================================
|
|
2156
|
+
// TEST: Show Title
|
|
2157
|
+
// ==========================================================================
|
|
2158
|
+
const showTitleCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(checkbox => {
|
|
2159
|
+
const label = checkbox.closest('label')
|
|
2160
|
+
return label?.textContent?.includes('Show Title')
|
|
2161
|
+
}) as HTMLInputElement
|
|
2162
|
+
|
|
2163
|
+
await performAndAssert(
|
|
2164
|
+
'Show Title → Toggle off',
|
|
2165
|
+
() => {
|
|
2166
|
+
const title = canvasElement.querySelector('.map-title')
|
|
2167
|
+
const isVisible = title?.classList.contains('visible')
|
|
2168
|
+
return {
|
|
2169
|
+
isVisible: Boolean(isVisible)
|
|
2170
|
+
}
|
|
2171
|
+
},
|
|
2172
|
+
async () => {
|
|
2173
|
+
await userEvent.click(showTitleCheckbox)
|
|
2174
|
+
},
|
|
2175
|
+
(before, after) => {
|
|
2176
|
+
// After toggling off, title should be hidden
|
|
2177
|
+
return before.isVisible && !after.isVisible
|
|
2178
|
+
}
|
|
2179
|
+
)
|
|
2180
|
+
|
|
2181
|
+
await performAndAssert(
|
|
2182
|
+
'Show Title → Toggle back on',
|
|
2183
|
+
() => {
|
|
2184
|
+
const title = canvasElement.querySelector('.map-title')
|
|
2185
|
+
const isVisible = title?.classList.contains('visible')
|
|
2186
|
+
return {
|
|
2187
|
+
isVisible: Boolean(isVisible)
|
|
2188
|
+
}
|
|
2189
|
+
},
|
|
2190
|
+
async () => {
|
|
2191
|
+
await userEvent.click(showTitleCheckbox)
|
|
2192
|
+
},
|
|
2193
|
+
(before, after) => {
|
|
2194
|
+
// After toggling back on, title should be visible
|
|
2195
|
+
return !before.isVisible && after.isVisible
|
|
2196
|
+
}
|
|
2197
|
+
)
|
|
2198
|
+
|
|
2199
|
+
// ==========================================================================
|
|
2200
|
+
// TEST: Geo Border Color
|
|
2201
|
+
// ==========================================================================
|
|
2202
|
+
const geoBorderColorSelect = Array.from(canvasElement.querySelectorAll('select')).find(select => {
|
|
2203
|
+
const label = select.closest('label')
|
|
2204
|
+
return label?.textContent?.includes('Geo Border Color')
|
|
2205
|
+
}) as HTMLSelectElement
|
|
2206
|
+
|
|
2207
|
+
await performAndAssert(
|
|
2208
|
+
'Geo Border Color → Change to White',
|
|
2209
|
+
() => {
|
|
2210
|
+
// Check the stroke attribute on SVG paths with class 'single-geo'
|
|
2211
|
+
const geoPaths = canvasElement.querySelectorAll('path.single-geo')
|
|
2212
|
+
const firstPath = geoPaths[0] as SVGPathElement
|
|
2213
|
+
// Get the actual stroke value - could be in attribute, style, or computed
|
|
2214
|
+
const strokeAttr = firstPath?.getAttribute('stroke') || ''
|
|
2215
|
+
const strokeStyle = firstPath?.style?.stroke || ''
|
|
2216
|
+
const computedStroke = firstPath ? window.getComputedStyle(firstPath).stroke : ''
|
|
2217
|
+
return {
|
|
2218
|
+
strokeAttr,
|
|
2219
|
+
strokeStyle,
|
|
2220
|
+
computedStroke,
|
|
2221
|
+
pathCount: geoPaths.length
|
|
2222
|
+
}
|
|
2223
|
+
},
|
|
2224
|
+
async () => {
|
|
2225
|
+
await userEvent.selectOptions(geoBorderColorSelect, 'sameAsBackground')
|
|
2226
|
+
},
|
|
2227
|
+
(before, after) => {
|
|
2228
|
+
// After changing to white, one of the stroke values should change
|
|
2229
|
+
const beforeStroke = before.strokeAttr || before.strokeStyle || before.computedStroke
|
|
2230
|
+
const afterStroke = after.strokeAttr || after.strokeStyle || after.computedStroke
|
|
2231
|
+
return beforeStroke !== '' && afterStroke !== '' && beforeStroke !== afterStroke
|
|
2232
|
+
}
|
|
2233
|
+
)
|
|
2234
|
+
|
|
2235
|
+
// ==========================================================================
|
|
2236
|
+
// TEST: Map Color Palette - Sequential Palette Selection
|
|
2237
|
+
// ==========================================================================
|
|
2238
|
+
await performAndAssert(
|
|
2239
|
+
'Map Color Palette → Select different sequential palette',
|
|
2240
|
+
() => {
|
|
2241
|
+
// Check the fill colors of actual map path elements (same as we use for stroke)
|
|
2242
|
+
const geoPaths = canvasElement.querySelectorAll('path.single-geo')
|
|
2243
|
+
const firstPath = geoPaths[0] as SVGPathElement
|
|
2244
|
+
// Get the fill value from any available source
|
|
2245
|
+
const fillAttr = firstPath?.getAttribute('fill') || ''
|
|
2246
|
+
const fillStyle = firstPath?.style?.fill || ''
|
|
2247
|
+
const computedFill = firstPath ? window.getComputedStyle(firstPath).fill : ''
|
|
2248
|
+
const firstColor = fillAttr || fillStyle || computedFill
|
|
2249
|
+
return {
|
|
2250
|
+
firstColor,
|
|
2251
|
+
pathCount: geoPaths.length
|
|
2252
|
+
}
|
|
2253
|
+
},
|
|
2254
|
+
async () => {
|
|
2255
|
+
// Find the Sequential palette section
|
|
2256
|
+
const spans = Array.from(canvasElement.querySelectorAll('span'))
|
|
2257
|
+
const sequentialLabel = spans.find(span => span.textContent?.trim() === 'Sequential')
|
|
2258
|
+
const paletteContainer = sequentialLabel?.nextElementSibling
|
|
2259
|
+
|
|
2260
|
+
// Try both li and button elements for palette selection
|
|
2261
|
+
const palettes = Array.from(paletteContainer?.querySelectorAll('li, button') || []) as HTMLElement[]
|
|
2262
|
+
|
|
2263
|
+
// Select a different palette (skip the first one as it might be selected)
|
|
2264
|
+
const alternativePalette = palettes.find((element, idx) => idx > 0 && !element.classList.contains('selected'))
|
|
2265
|
+
if (alternativePalette) {
|
|
2266
|
+
await userEvent.click(alternativePalette)
|
|
2267
|
+
|
|
2268
|
+
// Wait for changes to apply, then check for modal
|
|
2269
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
2270
|
+
|
|
2271
|
+
// Check if modal appeared and handle it
|
|
2272
|
+
const modal = canvasElement.querySelector('.modal-overlay')
|
|
2273
|
+
if (modal) {
|
|
2274
|
+
const confirmButton = Array.from(canvasElement.querySelectorAll('button')).find(btn =>
|
|
2275
|
+
btn.textContent?.includes('Convert to New Palette')
|
|
2276
|
+
)
|
|
2277
|
+
if (confirmButton) {
|
|
2278
|
+
await userEvent.click(confirmButton)
|
|
2279
|
+
// Wait for modal to close and changes to apply
|
|
2280
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
},
|
|
2285
|
+
(before, after) => {
|
|
2286
|
+
// After selecting a different palette, the map fill colors should change
|
|
2287
|
+
return before.firstColor !== '' && after.firstColor !== '' && before.firstColor !== after.firstColor
|
|
2288
|
+
}
|
|
2289
|
+
)
|
|
2290
|
+
|
|
2291
|
+
// ==========================================================================
|
|
2292
|
+
// TEST: Map Color Palette - Reverse Order
|
|
2293
|
+
// ==========================================================================
|
|
2294
|
+
// The reverse toggle is an InputToggle component which renders as a div slider
|
|
2295
|
+
// Find it via the hidden checkbox with name containing "isReversed"
|
|
2296
|
+
const reversePaletteCheckbox = canvasElement.querySelector('input[name*="isReversed"]') as HTMLInputElement
|
|
2297
|
+
const reversePaletteToggle = reversePaletteCheckbox?.parentElement as HTMLElement
|
|
2298
|
+
|
|
2299
|
+
expect(reversePaletteToggle).toBeTruthy()
|
|
2300
|
+
|
|
2301
|
+
await performAndAssert(
|
|
2302
|
+
'Map Color Palette → Reverse palette order',
|
|
2303
|
+
() => {
|
|
2304
|
+
// Check the fill colors of map paths
|
|
2305
|
+
const geoPaths = canvasElement.querySelectorAll('path.single-geo')
|
|
2306
|
+
const firstPath = geoPaths[0] as SVGPathElement
|
|
2307
|
+
const fillAttr = firstPath?.getAttribute('fill') || ''
|
|
2308
|
+
const fillStyle = firstPath?.style?.fill || ''
|
|
2309
|
+
const computedFill = firstPath ? window.getComputedStyle(firstPath).fill : ''
|
|
2310
|
+
const firstColor = fillAttr || fillStyle || computedFill
|
|
2311
|
+
return {
|
|
2312
|
+
firstColor,
|
|
2313
|
+
pathCount: geoPaths.length
|
|
2314
|
+
}
|
|
2315
|
+
},
|
|
2316
|
+
async () => {
|
|
2317
|
+
await userEvent.click(reversePaletteToggle)
|
|
2318
|
+
// No modal should appear - reversing doesn't trigger conversion modal
|
|
2319
|
+
},
|
|
2320
|
+
(before, after) => {
|
|
2321
|
+
// After reversing, the first color should be different
|
|
2322
|
+
return before.firstColor !== '' && after.firstColor !== '' && before.firstColor !== after.firstColor
|
|
2323
|
+
}
|
|
2324
|
+
)
|
|
2325
|
+
|
|
2326
|
+
// ==========================================================================
|
|
2327
|
+
// TEST: Geocode Circle Size
|
|
2328
|
+
// ==========================================================================
|
|
2329
|
+
const geocodeCircleSizeInput = Array.from(canvasElement.querySelectorAll('input[type="number"]')).find(input => {
|
|
2330
|
+
const label = input.closest('label')
|
|
2331
|
+
return label?.textContent?.includes('Geocode Circle Size')
|
|
2332
|
+
}) as HTMLInputElement
|
|
2333
|
+
|
|
2334
|
+
expect(geocodeCircleSizeInput).toBeTruthy()
|
|
2335
|
+
|
|
2336
|
+
await performAndAssert(
|
|
2337
|
+
'Geocode Circle Size → Set to 15',
|
|
2338
|
+
() => {
|
|
2339
|
+
// Check the actual visualization - DC appears as a glyph on the map
|
|
2340
|
+
// The glyph class element is a path or circle, look for actual circle element
|
|
2341
|
+
const geoPoints = canvasElement.querySelectorAll('g.geo-point')
|
|
2342
|
+
const firstGeoPoint = geoPoints[0]
|
|
2343
|
+
const circle = firstGeoPoint?.querySelector('circle')
|
|
2344
|
+
const r = circle?.getAttribute('r') || ''
|
|
2345
|
+
// Also check path elements that might have size in their d attribute
|
|
2346
|
+
const path = firstGeoPoint?.querySelector('path')
|
|
2347
|
+
const d = path?.getAttribute('d') || ''
|
|
2348
|
+
return {
|
|
2349
|
+
r,
|
|
2350
|
+
d,
|
|
2351
|
+
inputValue: geocodeCircleSizeInput.value
|
|
2352
|
+
}
|
|
2353
|
+
},
|
|
2354
|
+
async () => {
|
|
2355
|
+
await userEvent.clear(geocodeCircleSizeInput)
|
|
2356
|
+
await userEvent.type(geocodeCircleSizeInput, '15')
|
|
2357
|
+
await userEvent.tab() // Trigger blur/change event
|
|
2358
|
+
},
|
|
2359
|
+
(before, after) => {
|
|
2360
|
+
// Verify the input value changed to 15
|
|
2361
|
+
expect(after.inputValue).toBe('15')
|
|
2362
|
+
// After changing the size, either the radius or path data should change
|
|
2363
|
+
return (
|
|
2364
|
+
(before.r !== '' && after.r !== '' && before.r !== after.r) ||
|
|
2365
|
+
(before.d !== '' && after.d !== '' && before.d !== after.d)
|
|
2366
|
+
)
|
|
2367
|
+
}
|
|
2368
|
+
)
|
|
2369
|
+
|
|
2370
|
+
// ==========================================================================
|
|
2371
|
+
// TEST: Default City Style
|
|
2372
|
+
// ==========================================================================
|
|
2373
|
+
const cityStyleSelect = Array.from(canvasElement.querySelectorAll('select')).find(select => {
|
|
2374
|
+
const label = select.closest('label')
|
|
2375
|
+
return label?.textContent?.includes('Default City Style')
|
|
2376
|
+
}) as HTMLSelectElement
|
|
2377
|
+
|
|
2378
|
+
if (cityStyleSelect) {
|
|
2379
|
+
await performAndAssert(
|
|
2380
|
+
'Default City Style → Change to Triangle',
|
|
2381
|
+
() => {
|
|
2382
|
+
// Check the actual visualization - look for glyph shapes
|
|
2383
|
+
const circles = canvasElement.querySelectorAll('.visx-glyph-circle')
|
|
2384
|
+
const triangles = canvasElement.querySelectorAll('.visx-glyph-triangle')
|
|
2385
|
+
return {
|
|
2386
|
+
circleCount: circles.length,
|
|
2387
|
+
triangleCount: triangles.length
|
|
2388
|
+
}
|
|
2389
|
+
},
|
|
2390
|
+
async () => {
|
|
2391
|
+
await userEvent.selectOptions(cityStyleSelect, 'triangle')
|
|
2392
|
+
},
|
|
2393
|
+
(before, after) => {
|
|
2394
|
+
// After changing to triangle, should have triangles instead of circles
|
|
2395
|
+
return before.triangleCount !== after.triangleCount || before.circleCount !== after.circleCount
|
|
2396
|
+
}
|
|
2397
|
+
)
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
// ==========================================================================
|
|
2401
|
+
// TEST: Label (Optional)
|
|
2402
|
+
// ==========================================================================
|
|
2403
|
+
const cityStyleLabelInput = Array.from(canvasElement.querySelectorAll('input[type="text"]')).find(input => {
|
|
2404
|
+
const label = input.closest('label')
|
|
2405
|
+
return label?.textContent?.includes('Label (Optional)')
|
|
2406
|
+
}) as HTMLInputElement
|
|
2407
|
+
|
|
2408
|
+
if (cityStyleLabelInput) {
|
|
2409
|
+
await performAndAssert(
|
|
2410
|
+
'Label (Optional) → Set custom label',
|
|
2411
|
+
() => {
|
|
2412
|
+
// Check if the label appears in the legend
|
|
2413
|
+
const legend = canvasElement.querySelector('.legends')
|
|
2414
|
+
const legendText = legend?.textContent || ''
|
|
2415
|
+
return {
|
|
2416
|
+
legendText
|
|
2417
|
+
}
|
|
2418
|
+
},
|
|
2419
|
+
async () => {
|
|
2420
|
+
await userEvent.clear(cityStyleLabelInput)
|
|
2421
|
+
await userEvent.type(cityStyleLabelInput, 'City Locations')
|
|
2422
|
+
},
|
|
2423
|
+
(before, after) => {
|
|
2424
|
+
// After setting the label, it should appear in the legend
|
|
2425
|
+
return after.legendText.includes('City Locations')
|
|
2426
|
+
}
|
|
2427
|
+
)
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
/**
|
|
2433
|
+
* Pattern Settings Section Tests
|
|
2434
|
+
* Tests controls in the Pattern Settings accordion (only visible for US maps)
|
|
2435
|
+
*/
|
|
2436
|
+
export const PatternSettingsSectionTests: Story = {
|
|
2437
|
+
args: {
|
|
2438
|
+
config: usaStateGradientConfig,
|
|
2439
|
+
isEditor: true
|
|
2440
|
+
},
|
|
2441
|
+
play: async ({ canvasElement }) => {
|
|
2442
|
+
const canvas = within(canvasElement)
|
|
2443
|
+
|
|
2444
|
+
// Wait for editor to load
|
|
2445
|
+
await waitForEditor(canvas)
|
|
2446
|
+
|
|
2447
|
+
// ==========================================================================
|
|
2448
|
+
// TEST: Wait for Pattern Settings accordion to load (US topo JSON)
|
|
2449
|
+
// ==========================================================================
|
|
2450
|
+
let patternSettingsButton: HTMLElement | null = null
|
|
2451
|
+
|
|
2452
|
+
await performAndAssert(
|
|
2453
|
+
'Pattern Settings → Wait for accordion to appear',
|
|
2454
|
+
() => {
|
|
2455
|
+
// Check if Pattern Settings accordion exists
|
|
2456
|
+
const buttons = Array.from(canvasElement.querySelectorAll('.accordion__button'))
|
|
2457
|
+
const button = buttons.find(btn => btn.textContent?.includes('Pattern Settings')) as HTMLElement
|
|
2458
|
+
if (button) {
|
|
2459
|
+
patternSettingsButton = button
|
|
2460
|
+
}
|
|
2461
|
+
return {
|
|
2462
|
+
accordionExists: !!button
|
|
2463
|
+
}
|
|
2464
|
+
},
|
|
2465
|
+
async () => {
|
|
2466
|
+
// No action needed - just let performAndAssert's built-in polling wait for the accordion
|
|
2467
|
+
},
|
|
2468
|
+
(before, after) => {
|
|
2469
|
+
// After waiting, the accordion should exist
|
|
2470
|
+
return after.accordionExists
|
|
2471
|
+
}
|
|
2472
|
+
)
|
|
2473
|
+
|
|
2474
|
+
// ==========================================================================
|
|
2475
|
+
// TEST: Open Pattern Settings accordion and verify controls appear
|
|
2476
|
+
// ==========================================================================
|
|
2477
|
+
await performAndAssert(
|
|
2478
|
+
'Pattern Settings → Open accordion',
|
|
2479
|
+
() => {
|
|
2480
|
+
// Check if the accordion panel is expanded (aria-expanded attribute)
|
|
2481
|
+
const isExpanded = patternSettingsButton.getAttribute('aria-expanded') === 'true'
|
|
2482
|
+
// Also check if the Add Geo Pattern button is visible
|
|
2483
|
+
const buttons = Array.from(canvasElement.querySelectorAll('button'))
|
|
2484
|
+
const addPatternButton = buttons.find(btn => btn.textContent?.includes('Add Geo Pattern'))
|
|
2485
|
+
return {
|
|
2486
|
+
isExpanded,
|
|
2487
|
+
hasAddButton: !!addPatternButton
|
|
2488
|
+
}
|
|
2489
|
+
},
|
|
2490
|
+
async () => {
|
|
2491
|
+
await userEvent.click(patternSettingsButton)
|
|
2492
|
+
},
|
|
2493
|
+
(before, after) => {
|
|
2494
|
+
// After clicking, accordion should be expanded and button should be visible
|
|
2495
|
+
return !before.isExpanded && after.isExpanded && after.hasAddButton
|
|
2496
|
+
}
|
|
2497
|
+
)
|
|
2498
|
+
|
|
2499
|
+
// ==========================================================================
|
|
2500
|
+
// TEST: Add a geo pattern and verify pattern appears on map
|
|
2501
|
+
// ==========================================================================
|
|
2502
|
+
await performAndAssert(
|
|
2503
|
+
'Pattern Settings → Add geo pattern',
|
|
2504
|
+
() => {
|
|
2505
|
+
// Look for pattern elements in ALL SVGs (there might be multiple)
|
|
2506
|
+
// Pattern definitions are <pattern> elements, and they're used by paths with fill="url(#...)"
|
|
2507
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
2508
|
+
let patternDefCount = 0
|
|
2509
|
+
let patternPathCount = 0
|
|
2510
|
+
|
|
2511
|
+
allSvgs.forEach(svg => {
|
|
2512
|
+
const patternDefs = svg.querySelectorAll('pattern')
|
|
2513
|
+
patternDefCount += patternDefs.length
|
|
2514
|
+
|
|
2515
|
+
const allPaths = Array.from(svg.querySelectorAll('path'))
|
|
2516
|
+
const patternPaths = allPaths.filter(path => {
|
|
2517
|
+
const fill = path.getAttribute('fill')
|
|
2518
|
+
return fill && fill.startsWith('url(#')
|
|
2519
|
+
})
|
|
2520
|
+
patternPathCount += patternPaths.length
|
|
2521
|
+
})
|
|
2522
|
+
|
|
2523
|
+
return {
|
|
2524
|
+
patternDefCount,
|
|
2525
|
+
patternPathCount,
|
|
2526
|
+
svgCount: allSvgs.length
|
|
2527
|
+
}
|
|
2528
|
+
},
|
|
2529
|
+
async () => {
|
|
2530
|
+
// Click "Add Geo Pattern" button
|
|
2531
|
+
const buttons = Array.from(canvasElement.querySelectorAll('button'))
|
|
2532
|
+
const addPatternButton = buttons.find(btn => btn.textContent?.includes('Add Geo Pattern'))
|
|
2533
|
+
if (!addPatternButton) {
|
|
2534
|
+
throw new Error('Add Geo Pattern button not found')
|
|
2535
|
+
}
|
|
2536
|
+
await userEvent.click(addPatternButton)
|
|
2537
|
+
},
|
|
2538
|
+
(before, after) => {
|
|
2539
|
+
// After clicking, pattern definitions and paths should appear on all states
|
|
2540
|
+
// Default pattern is 'circles' with empty dataKey, which matches all states (undefined === undefined)
|
|
2541
|
+
return after.patternDefCount > before.patternDefCount && after.patternPathCount > before.patternPathCount
|
|
2542
|
+
}
|
|
2543
|
+
)
|
|
2544
|
+
|
|
2545
|
+
// ==========================================================================
|
|
2546
|
+
// TEST: Change pattern color to #111
|
|
2547
|
+
// ==========================================================================
|
|
2548
|
+
// First, open the pattern accordion
|
|
2549
|
+
let patternAccordionButton: HTMLElement | null = null
|
|
2550
|
+
await performAndAssert(
|
|
2551
|
+
'Pattern Settings → Open pattern accordion',
|
|
2552
|
+
() => {
|
|
2553
|
+
const allButtons = Array.from(canvasElement.querySelectorAll('.accordion__button'))
|
|
2554
|
+
const button = allButtons.find(btn => btn.textContent?.includes('Select Column')) as HTMLElement
|
|
2555
|
+
if (button) {
|
|
2556
|
+
patternAccordionButton = button
|
|
2557
|
+
}
|
|
2558
|
+
return {
|
|
2559
|
+
isExpanded: button ? button.getAttribute('aria-expanded') === 'true' : false
|
|
2560
|
+
}
|
|
2561
|
+
},
|
|
2562
|
+
async () => {
|
|
2563
|
+
if (!patternAccordionButton) {
|
|
2564
|
+
throw new Error('Pattern accordion not found')
|
|
2565
|
+
}
|
|
2566
|
+
await userEvent.click(patternAccordionButton)
|
|
2567
|
+
},
|
|
2568
|
+
(before, after) => {
|
|
2569
|
+
return !before.isExpanded && after.isExpanded
|
|
2570
|
+
}
|
|
2571
|
+
)
|
|
2572
|
+
|
|
2573
|
+
// Now change the pattern color
|
|
2574
|
+
await performAndAssert(
|
|
2575
|
+
'Pattern Settings → Change pattern color to #111',
|
|
2576
|
+
() => {
|
|
2577
|
+
// Check pattern color in the SVG - look for circle elements inside pattern definitions
|
|
2578
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
2579
|
+
let patternCircleColors: string[] = []
|
|
2580
|
+
|
|
2581
|
+
allSvgs.forEach(svg => {
|
|
2582
|
+
const patterns = svg.querySelectorAll('pattern')
|
|
2583
|
+
patterns.forEach(pattern => {
|
|
2584
|
+
const circles = pattern.querySelectorAll('circle')
|
|
2585
|
+
circles.forEach(circle => {
|
|
2586
|
+
const fill = circle.getAttribute('fill')
|
|
2587
|
+
if (fill) patternCircleColors.push(fill)
|
|
2588
|
+
})
|
|
2589
|
+
})
|
|
2590
|
+
})
|
|
2591
|
+
|
|
2592
|
+
return {
|
|
2593
|
+
patternColors: patternCircleColors
|
|
2594
|
+
}
|
|
2595
|
+
},
|
|
2596
|
+
async () => {
|
|
2597
|
+
// Find the pattern color input and change it to #111
|
|
2598
|
+
const input = canvasElement.querySelector('input[name="patternColor"]') as HTMLInputElement
|
|
2599
|
+
if (!input) {
|
|
2600
|
+
throw new Error('Pattern color input not found')
|
|
2601
|
+
}
|
|
2602
|
+
await userEvent.clear(input)
|
|
2603
|
+
// Use paste to enter the entire value at once, avoiding intermediate contrast check warnings
|
|
2604
|
+
await userEvent.paste('#111')
|
|
2605
|
+
// Blur to commit the change
|
|
2606
|
+
await userEvent.tab()
|
|
2607
|
+
},
|
|
2608
|
+
(before, after) => {
|
|
2609
|
+
// Pattern circles should now have fill color #111
|
|
2610
|
+
return after.patternColors.some(color => color === '#111')
|
|
2611
|
+
}
|
|
2612
|
+
)
|
|
2613
|
+
|
|
2614
|
+
// ==========================================================================
|
|
2615
|
+
// TEST: Set dataKey to STATE and dataValue to Colorado
|
|
2616
|
+
// ==========================================================================
|
|
2617
|
+
await performAndAssert(
|
|
2618
|
+
'Pattern Settings → Set dataKey/dataValue to STATE/Colorado',
|
|
2619
|
+
() => {
|
|
2620
|
+
// Count pattern paths (paths with fill="url(#...)")
|
|
2621
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
2622
|
+
let patternPathCount = 0
|
|
2623
|
+
|
|
2624
|
+
allSvgs.forEach(svg => {
|
|
2625
|
+
const allPaths = Array.from(svg.querySelectorAll('path'))
|
|
2626
|
+
const patternPaths = allPaths.filter(path => {
|
|
2627
|
+
const fill = path.getAttribute('fill')
|
|
2628
|
+
return fill && fill.startsWith('url(#')
|
|
2629
|
+
})
|
|
2630
|
+
patternPathCount += patternPaths.length
|
|
2631
|
+
})
|
|
2632
|
+
|
|
2633
|
+
return {
|
|
2634
|
+
patternPathCount
|
|
2635
|
+
}
|
|
2636
|
+
},
|
|
2637
|
+
async () => {
|
|
2638
|
+
// Select "STATE" from the dataKey dropdown
|
|
2639
|
+
const dataKeySelect = canvasElement.querySelector('select[name^="pattern-dataKey--"]') as HTMLSelectElement
|
|
2640
|
+
if (!dataKeySelect) {
|
|
2641
|
+
throw new Error('DataKey select not found')
|
|
2642
|
+
}
|
|
2643
|
+
await userEvent.selectOptions(dataKeySelect, 'STATE')
|
|
2644
|
+
|
|
2645
|
+
// Type "Colorado" in the dataValue input
|
|
2646
|
+
const dataValueInput = canvasElement.querySelector('input[id^="pattern-dataValue--"]') as HTMLInputElement
|
|
2647
|
+
if (!dataValueInput) {
|
|
2648
|
+
throw new Error('DataValue input not found')
|
|
2649
|
+
}
|
|
2650
|
+
await userEvent.clear(dataValueInput)
|
|
2651
|
+
await userEvent.type(dataValueInput, 'Colorado')
|
|
2652
|
+
// Tab to commit the change
|
|
2653
|
+
await userEvent.tab()
|
|
2654
|
+
},
|
|
2655
|
+
(before, after) => {
|
|
2656
|
+
// After setting to Colorado only, pattern paths should decrease significantly
|
|
2657
|
+
// Before: all states (~50+), After: just Colorado (should be 1-2)
|
|
2658
|
+
return after.patternPathCount < before.patternPathCount && after.patternPathCount > 0
|
|
2659
|
+
}
|
|
2660
|
+
)
|
|
2661
|
+
|
|
2662
|
+
// ==========================================================================
|
|
2663
|
+
// TEST: Set label to "Colorado Pattern"
|
|
2664
|
+
// ==========================================================================
|
|
2665
|
+
await performAndAssert(
|
|
2666
|
+
'Pattern Settings → Set label to "Colorado Pattern"',
|
|
2667
|
+
() => {
|
|
2668
|
+
// Check if the label appears in the legend
|
|
2669
|
+
const legendText = canvasElement.textContent || ''
|
|
2670
|
+
return {
|
|
2671
|
+
hasLabelInLegend: legendText.includes('Colorado Pattern')
|
|
2672
|
+
}
|
|
2673
|
+
},
|
|
2674
|
+
async () => {
|
|
2675
|
+
// Find the label input - it's in a label element containing "Label (optional)"
|
|
2676
|
+
const labels = Array.from(canvasElement.querySelectorAll('label'))
|
|
2677
|
+
const labelElement = labels.find(l => l.textContent?.includes('Label (optional)'))
|
|
2678
|
+
const labelInput = labelElement?.querySelector('input[type="text"]') as HTMLInputElement
|
|
2679
|
+
|
|
2680
|
+
if (!labelInput) {
|
|
2681
|
+
throw new Error('Label input not found')
|
|
2682
|
+
}
|
|
2683
|
+
await userEvent.clear(labelInput)
|
|
2684
|
+
await userEvent.type(labelInput, 'Colorado Pattern')
|
|
2685
|
+
await userEvent.tab()
|
|
2686
|
+
},
|
|
2687
|
+
(before, after) => {
|
|
2688
|
+
// After setting the label, it should appear in the legend
|
|
2689
|
+
return !before.hasLabelInLegend && after.hasLabelInLegend
|
|
2690
|
+
}
|
|
2691
|
+
)
|
|
2692
|
+
|
|
2693
|
+
// ==========================================================================
|
|
2694
|
+
// TEST: Change pattern type to "lines"
|
|
2695
|
+
// ==========================================================================
|
|
2696
|
+
await performAndAssert(
|
|
2697
|
+
'Pattern Settings → Change pattern type to lines',
|
|
2698
|
+
() => {
|
|
2699
|
+
// Check what type of pattern elements exist - lines patterns have <line> elements
|
|
2700
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
2701
|
+
let hasCirclePattern = false
|
|
2702
|
+
let hasLinePattern = false
|
|
2703
|
+
|
|
2704
|
+
allSvgs.forEach(svg => {
|
|
2705
|
+
const patterns = svg.querySelectorAll('pattern')
|
|
2706
|
+
patterns.forEach(pattern => {
|
|
2707
|
+
if (pattern.querySelector('circle')) hasCirclePattern = true
|
|
2708
|
+
if (pattern.querySelector('line')) hasLinePattern = true
|
|
2709
|
+
})
|
|
2710
|
+
})
|
|
2711
|
+
|
|
2712
|
+
return {
|
|
2713
|
+
hasCirclePattern,
|
|
2714
|
+
hasLinePattern
|
|
2715
|
+
}
|
|
2716
|
+
},
|
|
2717
|
+
async () => {
|
|
2718
|
+
// Find the pattern type dropdown and select "lines"
|
|
2719
|
+
const patternTypeSelect = canvasElement.querySelector('select[name^="pattern-type--"]') as HTMLSelectElement
|
|
2720
|
+
if (!patternTypeSelect) {
|
|
2721
|
+
throw new Error('Pattern type select not found')
|
|
2722
|
+
}
|
|
2723
|
+
await userEvent.selectOptions(patternTypeSelect, 'lines')
|
|
2724
|
+
},
|
|
2725
|
+
(before, after) => {
|
|
2726
|
+
// Before: has circle pattern, no line pattern
|
|
2727
|
+
// After: has line pattern (may or may not still have circle pattern from other elements)
|
|
2728
|
+
return before.hasCirclePattern && !before.hasLinePattern && after.hasLinePattern
|
|
2729
|
+
}
|
|
2730
|
+
)
|
|
2731
|
+
|
|
2732
|
+
// ==========================================================================
|
|
2733
|
+
// TEST: Change pattern size to "large"
|
|
2734
|
+
// ==========================================================================
|
|
2735
|
+
await performAndAssert(
|
|
2736
|
+
'Pattern Settings → Change pattern size to large',
|
|
2737
|
+
() => {
|
|
2738
|
+
// Check the pattern element's width/height attributes
|
|
2739
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
2740
|
+
let patternSizes: number[] = []
|
|
2741
|
+
|
|
2742
|
+
allSvgs.forEach(svg => {
|
|
2743
|
+
const patterns = svg.querySelectorAll('pattern')
|
|
2744
|
+
patterns.forEach(pattern => {
|
|
2745
|
+
const width = pattern.getAttribute('width')
|
|
2746
|
+
if (width) patternSizes.push(parseFloat(width))
|
|
2747
|
+
})
|
|
2748
|
+
})
|
|
2749
|
+
|
|
2750
|
+
return {
|
|
2751
|
+
patternSizes
|
|
2752
|
+
}
|
|
2753
|
+
},
|
|
2754
|
+
async () => {
|
|
2755
|
+
// Find the pattern size dropdown and select "large"
|
|
2756
|
+
const patternSizeSelect = canvasElement.querySelector('select[name^="pattern-size--"]') as HTMLSelectElement
|
|
2757
|
+
if (!patternSizeSelect) {
|
|
2758
|
+
throw new Error('Pattern size select not found')
|
|
2759
|
+
}
|
|
2760
|
+
await userEvent.selectOptions(patternSizeSelect, 'large')
|
|
2761
|
+
},
|
|
2762
|
+
(before, after) => {
|
|
2763
|
+
// After changing to large, pattern sizes should increase
|
|
2764
|
+
const beforeMax = Math.max(...before.patternSizes)
|
|
2765
|
+
const afterMax = Math.max(...after.patternSizes)
|
|
2766
|
+
return afterMax > beforeMax
|
|
2767
|
+
}
|
|
2768
|
+
)
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
/**
|
|
2773
|
+
* Text Annotations Section Tests
|
|
2774
|
+
* Tests controls in the Text Annotations accordion
|
|
2775
|
+
*/
|
|
2776
|
+
export const TextAnnotationsSectionTests: Story = {
|
|
2777
|
+
args: {
|
|
2778
|
+
config: usaStateGradientConfig,
|
|
2779
|
+
isEditor: true
|
|
2780
|
+
},
|
|
2781
|
+
play: async ({ canvasElement }) => {
|
|
2782
|
+
const canvas = within(canvasElement)
|
|
2783
|
+
|
|
2784
|
+
// Wait for editor to load
|
|
2785
|
+
await waitForEditor(canvas)
|
|
2786
|
+
|
|
2787
|
+
// Open the Text Annotations accordion
|
|
2788
|
+
await openAccordion(canvas, 'Text Annotations')
|
|
2789
|
+
|
|
2790
|
+
// ==========================================================================
|
|
2791
|
+
// TEST: Add Annotation
|
|
2792
|
+
// ==========================================================================
|
|
2793
|
+
const addAnnotationButton = Array.from(canvasElement.querySelectorAll('button')).find(btn => {
|
|
2794
|
+
return btn.textContent?.includes('Add Annotation')
|
|
2795
|
+
}) as HTMLButtonElement
|
|
2796
|
+
|
|
2797
|
+
expect(addAnnotationButton).toBeTruthy()
|
|
2798
|
+
|
|
2799
|
+
await performAndAssert(
|
|
2800
|
+
'Add Annotation → Click to add default annotation',
|
|
2801
|
+
() => {
|
|
2802
|
+
// Check the actual visualization - annotations appear as divs with aria-label
|
|
2803
|
+
// Default annotation text is "New Annotation"
|
|
2804
|
+
const annotationElements = canvasElement.querySelectorAll('div[aria-label*="Annotation text"]')
|
|
2805
|
+
return {
|
|
2806
|
+
annotationCount: annotationElements.length
|
|
2807
|
+
}
|
|
2808
|
+
},
|
|
2809
|
+
async () => {
|
|
2810
|
+
await userEvent.click(addAnnotationButton)
|
|
2811
|
+
},
|
|
2812
|
+
(before, after) => {
|
|
2813
|
+
// After clicking, a new annotation should appear in the visualization
|
|
2814
|
+
return after.annotationCount > before.annotationCount
|
|
2815
|
+
}
|
|
2816
|
+
)
|
|
2817
|
+
|
|
2818
|
+
// ==========================================================================
|
|
2819
|
+
// TEST: Change Annotation Text
|
|
2820
|
+
// ==========================================================================
|
|
2821
|
+
// Wait for the annotation sub-accordion to appear and find the button with "New Annotation" or "Select Column"
|
|
2822
|
+
await waitForPresence('.cove-accordion__button', canvasElement)
|
|
2823
|
+
const annotationAccordionButtons = canvasElement.querySelectorAll('.cove-accordion__button')
|
|
2824
|
+
const annotationAccordionButton = Array.from(annotationAccordionButtons).find(
|
|
2825
|
+
btn => btn.textContent?.includes('New Annotation') || btn.textContent?.includes('Select Column')
|
|
2826
|
+
) as HTMLElement
|
|
2827
|
+
|
|
2828
|
+
expect(annotationAccordionButton).toBeTruthy()
|
|
2829
|
+
|
|
2830
|
+
// Open the annotation's sub-accordion
|
|
2831
|
+
await userEvent.click(annotationAccordionButton)
|
|
2832
|
+
|
|
2833
|
+
// Find the annotation text textarea
|
|
2834
|
+
const annotationTextarea = Array.from(canvasElement.querySelectorAll('textarea')).find(textarea => {
|
|
2835
|
+
const label = textarea.closest('label')
|
|
2836
|
+
return label?.textContent?.includes('Annotation Text')
|
|
2837
|
+
}) as HTMLTextAreaElement
|
|
2838
|
+
|
|
2839
|
+
expect(annotationTextarea).toBeTruthy()
|
|
2840
|
+
|
|
2841
|
+
await performAndAssert(
|
|
2842
|
+
'Annotation Text → Change to "Important Note"',
|
|
2843
|
+
() => {
|
|
2844
|
+
// Check the actual visualization - the annotation div should contain the text
|
|
2845
|
+
const annotationDivs = canvasElement.querySelectorAll('div[aria-label*="Annotation text"]')
|
|
2846
|
+
const texts = Array.from(annotationDivs).map(div => div.innerHTML)
|
|
2847
|
+
return {
|
|
2848
|
+
annotationTexts: texts.join('|')
|
|
2849
|
+
}
|
|
2850
|
+
},
|
|
2851
|
+
async () => {
|
|
2852
|
+
// Select all text and replace it
|
|
2853
|
+
await userEvent.click(annotationTextarea)
|
|
2854
|
+
await userEvent.keyboard('{Control>}a{/Control}')
|
|
2855
|
+
await userEvent.type(annotationTextarea, 'Important Note')
|
|
2856
|
+
await userEvent.tab() // Trigger blur to commit the change
|
|
2857
|
+
},
|
|
2858
|
+
(before, after) => {
|
|
2859
|
+
// After changing the text, the annotation should display the new text
|
|
2860
|
+
return after.annotationTexts.includes('Important Note')
|
|
2861
|
+
}
|
|
2862
|
+
)
|
|
2863
|
+
|
|
2864
|
+
// ==========================================================================
|
|
2865
|
+
// TEST: Delete Annotation
|
|
2866
|
+
// ==========================================================================
|
|
2867
|
+
const deleteButton = Array.from(canvasElement.querySelectorAll('button')).find(btn => {
|
|
2868
|
+
return btn.textContent?.includes('Delete Annotation')
|
|
2869
|
+
}) as HTMLButtonElement
|
|
2870
|
+
|
|
2871
|
+
expect(deleteButton).toBeTruthy()
|
|
2872
|
+
|
|
2873
|
+
await performAndAssert(
|
|
2874
|
+
'Delete Annotation → Remove annotation from map',
|
|
2875
|
+
() => {
|
|
2876
|
+
// Check the visualization - count the annotations
|
|
2877
|
+
const annotationDivs = canvasElement.querySelectorAll('div[aria-label*="Annotation text"]')
|
|
2878
|
+
return {
|
|
2879
|
+
annotationCount: annotationDivs.length
|
|
2880
|
+
}
|
|
2881
|
+
},
|
|
2882
|
+
async () => {
|
|
2883
|
+
await userEvent.click(deleteButton)
|
|
2884
|
+
},
|
|
2885
|
+
(before, after) => {
|
|
2886
|
+
// After deleting, annotation count should decrease
|
|
2887
|
+
return after.annotationCount < before.annotationCount
|
|
2888
|
+
}
|
|
2889
|
+
)
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
// ============================================================================
|
|
2894
|
+
// SMALL MULTIPLES SECTION TESTS
|
|
2895
|
+
// ============================================================================
|
|
2896
|
+
|
|
2897
|
+
export const SmallMultiplesSectionTests: Story = {
|
|
2898
|
+
name: 'Small Multiples Section Tests',
|
|
2899
|
+
parameters: {},
|
|
2900
|
+
args: {
|
|
2901
|
+
config: {
|
|
2902
|
+
...wastewaterMapSmallMultiplesConfig,
|
|
2903
|
+
general: {
|
|
2904
|
+
...wastewaterMapSmallMultiplesConfig.general,
|
|
2905
|
+
title: 'Map Small Multiples Test'
|
|
2906
|
+
},
|
|
2907
|
+
smallMultiples: {
|
|
2908
|
+
...wastewaterMapSmallMultiplesConfig.smallMultiples,
|
|
2909
|
+
mode: ''
|
|
2910
|
+
}
|
|
2911
|
+
},
|
|
2912
|
+
isEditor: true
|
|
2913
|
+
},
|
|
2914
|
+
play: async ({ canvasElement }) => {
|
|
2915
|
+
const canvas = within(canvasElement)
|
|
2916
|
+
|
|
2917
|
+
await waitForEditor(canvas)
|
|
2918
|
+
await waitForPresence('.map-container', canvasElement)
|
|
2919
|
+
|
|
2920
|
+
await openAccordion(canvas, 'Small Multiples')
|
|
2921
|
+
|
|
2922
|
+
// ============================================================================
|
|
2923
|
+
// TEST: Enable Small Multiples Mode
|
|
2924
|
+
// Verifies: Map visualization changes from single map to multiple maps
|
|
2925
|
+
// ============================================================================
|
|
2926
|
+
|
|
2927
|
+
const getMapTileCount = () => {
|
|
2928
|
+
const smallMultiplesContainer = canvasElement.querySelector('.small-multiples-container')
|
|
2929
|
+
const tiles = canvasElement.querySelectorAll('.small-multiples-grid > .small-multiple-tile')
|
|
2930
|
+
|
|
2931
|
+
return {
|
|
2932
|
+
hasSmallMultiplesContainer: !!smallMultiplesContainer,
|
|
2933
|
+
tileCount: tiles.length
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
const tileByColumnSelect = canvas.getByLabelText(/tile by column/i) as HTMLSelectElement
|
|
2938
|
+
|
|
2939
|
+
await performAndAssert(
|
|
2940
|
+
'Enable Small Multiples Mode - Map splits into multiple tiles',
|
|
2941
|
+
getMapTileCount,
|
|
2942
|
+
async () => {
|
|
2943
|
+
await userEvent.selectOptions(tileByColumnSelect, 'pathogen')
|
|
2944
|
+
},
|
|
2945
|
+
(before, after) => {
|
|
2946
|
+
return before.tileCount === 0 && after.tileCount === 3
|
|
2947
|
+
}
|
|
2948
|
+
)
|
|
2949
|
+
|
|
2950
|
+
// ============================================================================
|
|
2951
|
+
// TEST: Tiles Per Row Desktop
|
|
2952
|
+
// Verifies: Grid layout changes from 3 tiles per row to 2 tiles per row
|
|
2953
|
+
// ============================================================================
|
|
2954
|
+
|
|
2955
|
+
const getTilesInFirstRow = () => {
|
|
2956
|
+
const tiles = Array.from(canvasElement.querySelectorAll('.small-multiples-grid > .small-multiple-tile'))
|
|
2957
|
+
if (tiles.length === 0) return { tilesInFirstRow: 0 }
|
|
2958
|
+
|
|
2959
|
+
const firstTileTop = tiles[0].getBoundingClientRect().top
|
|
2960
|
+
const tilesInFirstRow = tiles.filter(tile => {
|
|
2961
|
+
const tileTop = tile.getBoundingClientRect().top
|
|
2962
|
+
return Math.abs(tileTop - firstTileTop) < 5
|
|
2963
|
+
}).length
|
|
2964
|
+
|
|
2965
|
+
return { tilesInFirstRow }
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
const tilesPerRowInput = canvas.getByLabelText(/tiles per row \(desktop\)/i) as HTMLInputElement
|
|
2969
|
+
|
|
2970
|
+
await performAndAssert(
|
|
2971
|
+
'Tiles Per Row Desktop - Layout changes from 3 to 2 tiles per row',
|
|
2972
|
+
getTilesInFirstRow,
|
|
2973
|
+
async () => {
|
|
2974
|
+
await userEvent.clear(tilesPerRowInput)
|
|
2975
|
+
await userEvent.type(tilesPerRowInput, '2')
|
|
2976
|
+
tilesPerRowInput.blur()
|
|
2977
|
+
},
|
|
2978
|
+
(before, after) => {
|
|
2979
|
+
return before.tilesInFirstRow === 3 && after.tilesInFirstRow === 2
|
|
2980
|
+
}
|
|
2981
|
+
)
|
|
2982
|
+
|
|
2983
|
+
// ============================================================================
|
|
2984
|
+
// TEST: Tile Order
|
|
2985
|
+
// Verifies: Changing tile order from custom to descending reverses tiles
|
|
2986
|
+
// ============================================================================
|
|
2987
|
+
|
|
2988
|
+
const getTileTitles = () => {
|
|
2989
|
+
const tiles = canvasElement.querySelectorAll('.small-multiples-grid > .small-multiple-tile')
|
|
2990
|
+
const titles = Array.from(tiles).map(tile => {
|
|
2991
|
+
const titleElement = tile.querySelector('.tile-title')
|
|
2992
|
+
return titleElement?.textContent?.trim() || ''
|
|
2993
|
+
})
|
|
2994
|
+
return { titles }
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
const tileOrderSelect = canvas.getByLabelText(/tile order/i) as HTMLSelectElement
|
|
2998
|
+
|
|
2999
|
+
await performAndAssert(
|
|
3000
|
+
'Tile Order - Descending sorts tiles alphabetically descending',
|
|
3001
|
+
getTileTitles,
|
|
3002
|
+
async () => {
|
|
3003
|
+
await userEvent.selectOptions(tileOrderSelect, 'desc')
|
|
3004
|
+
},
|
|
3005
|
+
(before, after) => {
|
|
3006
|
+
return before.titles[0] === 'Influenza AAA' && after.titles[0] === 'RSV' && after.titles[1] === 'Influenza AAA'
|
|
3007
|
+
}
|
|
3008
|
+
)
|
|
3009
|
+
|
|
3010
|
+
// ============================================================================
|
|
3011
|
+
// TEST: Custom Tile Title
|
|
3012
|
+
// Verifies: Custom tile title appears in visualization
|
|
3013
|
+
// ============================================================================
|
|
3014
|
+
|
|
3015
|
+
const covid19TitleInput = canvasElement.querySelector('input[placeholder="COVID-19"]') as HTMLInputElement
|
|
3016
|
+
|
|
3017
|
+
await performAndAssert(
|
|
3018
|
+
'Custom Tile Title - Changes tile display name',
|
|
3019
|
+
getTileTitles,
|
|
3020
|
+
async () => {
|
|
3021
|
+
if (covid19TitleInput) {
|
|
3022
|
+
await userEvent.clear(covid19TitleInput)
|
|
3023
|
+
await userEvent.type(covid19TitleInput, 'Custom COVID Title')
|
|
3024
|
+
covid19TitleInput.blur()
|
|
3025
|
+
}
|
|
3026
|
+
},
|
|
3027
|
+
(before, after) => {
|
|
3028
|
+
return after.titles.includes('Custom COVID Title') && !before.titles.includes('Custom COVID Title')
|
|
3029
|
+
}
|
|
3030
|
+
)
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
// ==========================================================================
|
|
3035
|
+
// MULTI-COUNTRY MAP TESTS
|
|
3036
|
+
// ==========================================================================
|
|
3037
|
+
|
|
3038
|
+
export const MultiCountryWorldMapTests: Story = {
|
|
3039
|
+
args: {
|
|
3040
|
+
...DEFAULT_ARGS,
|
|
3041
|
+
config: {
|
|
3042
|
+
...multiCountryConfig,
|
|
3043
|
+
general: {
|
|
3044
|
+
...multiCountryConfig.general,
|
|
3045
|
+
countriesPicked: [] // Start with no countries selected for testing
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
},
|
|
3049
|
+
play: async ({ canvasElement }) => {
|
|
3050
|
+
const canvas = within(canvasElement)
|
|
3051
|
+
|
|
3052
|
+
await waitForEditor(canvas)
|
|
3053
|
+
await waitForPresence('.map-container', canvasElement)
|
|
3054
|
+
|
|
3055
|
+
// Ensure we're testing a world map - Type accordion should be open by default
|
|
3056
|
+
await openAccordion(canvas, 'Type')
|
|
3057
|
+
|
|
3058
|
+
// Wait for world map to load (topology loads asynchronously)
|
|
3059
|
+
// The config already starts with geoType: 'world', so we just need to wait for it to render
|
|
3060
|
+
await waitForPresence('path[data-country-code]', canvasElement)
|
|
3061
|
+
|
|
3062
|
+
// Wait for the country selector to appear (only shows for world maps)
|
|
3063
|
+
await waitForPresence('.cove-multiselect', canvasElement)
|
|
3064
|
+
|
|
3065
|
+
// Stay in Type accordion section - this is where multi-country controls are located
|
|
3066
|
+
|
|
3067
|
+
// ==========================================================================
|
|
3068
|
+
// Helper functions to capture visual state
|
|
3069
|
+
// ==========================================================================
|
|
3070
|
+
const getCountryVisualState = () => {
|
|
3071
|
+
const mapContainer = canvasElement.querySelector('.map-container')
|
|
3072
|
+
const allCountryPaths = canvasElement.querySelectorAll('path[data-country-code]')
|
|
3073
|
+
const visibleCountries = Array.from(allCountryPaths).filter(
|
|
3074
|
+
path => !path.classList.contains('hidden') && !path.classList.contains('grayed-out')
|
|
3075
|
+
)
|
|
3076
|
+
const grayedCountries = Array.from(allCountryPaths).filter(path => path.classList.contains('grayed-out'))
|
|
3077
|
+
const hiddenCountries = Array.from(allCountryPaths).filter(path => {
|
|
3078
|
+
const hasHiddenClass = path.classList.contains('hidden')
|
|
3079
|
+
const computedStyle = window.getComputedStyle(path as Element)
|
|
3080
|
+
const isDisplayNone = computedStyle.display === 'none'
|
|
3081
|
+
return hasHiddenClass || isDisplayNone
|
|
3082
|
+
})
|
|
3083
|
+
|
|
3084
|
+
return {
|
|
3085
|
+
totalCountries: allCountryPaths.length,
|
|
3086
|
+
visibleCountries: visibleCountries.length,
|
|
3087
|
+
grayedCountries: grayedCountries.length,
|
|
3088
|
+
hiddenCountries: hiddenCountries.length,
|
|
3089
|
+
mapClasses: mapContainer ? Array.from(mapContainer.classList) : [],
|
|
3090
|
+
hasMultiCountryClass: mapContainer?.classList.contains('multi-country-selected') || false,
|
|
3091
|
+
selectedCountryCodes: Array.from(
|
|
3092
|
+
new Set(
|
|
3093
|
+
Array.from(visibleCountries)
|
|
3094
|
+
.map(path => path.getAttribute('data-country-code'))
|
|
3095
|
+
.filter(Boolean)
|
|
3096
|
+
)
|
|
3097
|
+
)
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
// ==========================================================================
|
|
3102
|
+
// TEST: Country Multi-Select Dropdown Interaction
|
|
3103
|
+
// ==========================================================================
|
|
3104
|
+
const countryMultiSelect = Array.from(canvasElement.querySelectorAll('.cove-multiselect')).find(ms => {
|
|
3105
|
+
const parentLabel = ms.closest('label')
|
|
3106
|
+
const labelSpan = parentLabel?.querySelector('span')
|
|
3107
|
+
return labelSpan?.textContent?.includes('Countries Selector')
|
|
3108
|
+
}) as HTMLElement
|
|
3109
|
+
expect(countryMultiSelect).toBeTruthy()
|
|
3110
|
+
|
|
3111
|
+
// Get the expand button to open the dropdown
|
|
3112
|
+
const expandButton = countryMultiSelect.querySelector('button[aria-label="Expand"]') as HTMLButtonElement
|
|
3113
|
+
expect(expandButton).toBeTruthy()
|
|
3114
|
+
|
|
3115
|
+
// Test selecting first country (France)
|
|
3116
|
+
await performAndAssert(
|
|
3117
|
+
'Multi-Country → Select France',
|
|
3118
|
+
getCountryVisualState,
|
|
3119
|
+
async () => {
|
|
3120
|
+
// Open the dropdown
|
|
3121
|
+
await userEvent.click(expandButton)
|
|
3122
|
+
// Find and click France option
|
|
3123
|
+
const franceOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
3124
|
+
li.textContent?.includes('France')
|
|
3125
|
+
) as HTMLElement
|
|
3126
|
+
expect(franceOption).toBeTruthy()
|
|
3127
|
+
await userEvent.click(franceOption)
|
|
3128
|
+
},
|
|
3129
|
+
(before, after) => {
|
|
3130
|
+
// When countries are selected, map should show multi-country state
|
|
3131
|
+
return (
|
|
3132
|
+
after.hasMultiCountryClass &&
|
|
3133
|
+
after.selectedCountryCodes.includes('FRA') &&
|
|
3134
|
+
after.visibleCountries < before.totalCountries // Some countries should be filtered
|
|
3135
|
+
)
|
|
3136
|
+
}
|
|
3137
|
+
)
|
|
3138
|
+
|
|
3139
|
+
// Test selecting second country (Germany)
|
|
3140
|
+
await performAndAssert(
|
|
3141
|
+
'Multi-Country → Add Germany to selection',
|
|
3142
|
+
getCountryVisualState,
|
|
3143
|
+
async () => {
|
|
3144
|
+
// Expand dropdown again
|
|
3145
|
+
const expandBtn = countryMultiSelect.querySelector('button[aria-label="Expand"]') as HTMLButtonElement
|
|
3146
|
+
await userEvent.click(expandBtn)
|
|
3147
|
+
// Find and click Germany option
|
|
3148
|
+
const germanyOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
3149
|
+
li.textContent?.includes('Germany')
|
|
3150
|
+
) as HTMLElement
|
|
3151
|
+
expect(germanyOption).toBeTruthy()
|
|
3152
|
+
await userEvent.click(germanyOption)
|
|
3153
|
+
},
|
|
3154
|
+
(before, after) => {
|
|
3155
|
+
// Both France and Germany should be selected
|
|
3156
|
+
return (
|
|
3157
|
+
after.selectedCountryCodes.includes('FRA') &&
|
|
3158
|
+
after.selectedCountryCodes.includes('DEU') &&
|
|
3159
|
+
after.selectedCountryCodes.length === 2
|
|
3160
|
+
)
|
|
3161
|
+
}
|
|
3162
|
+
)
|
|
3163
|
+
|
|
3164
|
+
// Test adding a third country (Italy)
|
|
3165
|
+
await performAndAssert(
|
|
3166
|
+
'Multi-Country → Add Italy to selection',
|
|
3167
|
+
getCountryVisualState,
|
|
3168
|
+
async () => {
|
|
3169
|
+
// Expand dropdown again
|
|
3170
|
+
const expandBtn = countryMultiSelect.querySelector('button[aria-label="Expand"]') as HTMLButtonElement
|
|
3171
|
+
await userEvent.click(expandBtn)
|
|
3172
|
+
// Find and click Italy option
|
|
3173
|
+
const italyOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
3174
|
+
li.textContent?.includes('Italy')
|
|
3175
|
+
) as HTMLElement
|
|
3176
|
+
expect(italyOption).toBeTruthy()
|
|
3177
|
+
await userEvent.click(italyOption)
|
|
3178
|
+
},
|
|
3179
|
+
(before, after) => {
|
|
3180
|
+
// All three countries should be selected
|
|
3181
|
+
return (
|
|
3182
|
+
after.selectedCountryCodes.includes('FRA') &&
|
|
3183
|
+
after.selectedCountryCodes.includes('DEU') &&
|
|
3184
|
+
after.selectedCountryCodes.includes('ITA') &&
|
|
3185
|
+
after.selectedCountryCodes.length === 3
|
|
3186
|
+
)
|
|
3187
|
+
}
|
|
3188
|
+
)
|
|
3189
|
+
|
|
3190
|
+
// ==========================================================================
|
|
3191
|
+
// TEST: Hide Unselected Countries Toggle (Grey-out vs Hide behavior)
|
|
3192
|
+
// ==========================================================================
|
|
3193
|
+
// Ensure map has fully rendered with country paths before testing toggle
|
|
3194
|
+
await waitForPresence('path[data-country-code]', canvasElement)
|
|
3195
|
+
|
|
3196
|
+
// InputToggle renders as a div with a hidden checkbox
|
|
3197
|
+
const hideUnselectedToggle = Array.from(
|
|
3198
|
+
canvasElement.querySelectorAll('input[name*="hideUnselectedCountries"]')
|
|
3199
|
+
).find(input => {
|
|
3200
|
+
const label = input.closest('label') || input.parentElement?.querySelector('label')
|
|
3201
|
+
return label?.textContent?.includes('Hide Unselected Countries')
|
|
3202
|
+
}) as HTMLInputElement
|
|
3203
|
+
expect(hideUnselectedToggle).toBeTruthy()
|
|
3204
|
+
|
|
3205
|
+
// By default (unchecked), unselected countries should be grayed out
|
|
3206
|
+
// First, ensure the toggle is unchecked (hideUnselectedCountries = false, so countries are grayed)
|
|
3207
|
+
if (hideUnselectedToggle.checked) {
|
|
3208
|
+
await userEvent.click(hideUnselectedToggle)
|
|
3209
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
const beforeState = getCountryVisualState()
|
|
3213
|
+
|
|
3214
|
+
// Now check it (should hide unselected countries)
|
|
3215
|
+
await userEvent.click(hideUnselectedToggle)
|
|
3216
|
+
await new Promise(resolve => setTimeout(resolve, 100)) // Give time for re-render
|
|
3217
|
+
|
|
3218
|
+
const afterState = getCountryVisualState()
|
|
3219
|
+
|
|
3220
|
+
const testResult =
|
|
3221
|
+
afterState.hiddenCountries > beforeState.hiddenCountries &&
|
|
3222
|
+
afterState.grayedCountries === 0 && // No grayed countries when hiding
|
|
3223
|
+
afterState.selectedCountryCodes.length === 3 // Only our 3 selected countries (by unique ISO codes)
|
|
3224
|
+
|
|
3225
|
+
expect(testResult).toBe(true)
|
|
3226
|
+
|
|
3227
|
+
// Test unchecking "Hide Unselected Countries" again (should grey-out unselected countries)
|
|
3228
|
+
await performAndAssert(
|
|
3229
|
+
'Hide Unselected Countries → Grey-out unselected countries',
|
|
3230
|
+
getCountryVisualState,
|
|
3231
|
+
async () => {
|
|
3232
|
+
await userEvent.click(hideUnselectedToggle)
|
|
3233
|
+
},
|
|
3234
|
+
(before, after) => {
|
|
3235
|
+
// When not hiding (default), unselected countries should be grayed out
|
|
3236
|
+
return (
|
|
3237
|
+
after.grayedCountries > before.grayedCountries &&
|
|
3238
|
+
after.hiddenCountries === 0 && // No hidden countries when showing grayed
|
|
3239
|
+
after.selectedCountryCodes.length === 3 // Our 3 selected countries remain visible (by unique ISO codes)
|
|
3240
|
+
)
|
|
3241
|
+
}
|
|
3242
|
+
)
|
|
3243
|
+
|
|
3244
|
+
// ==========================================================================
|
|
3245
|
+
// TEST: Map Centering and Bounds Changes
|
|
3246
|
+
// ==========================================================================
|
|
3247
|
+
await performAndAssert(
|
|
3248
|
+
'Multi-Country Selection → Map centers on selected countries',
|
|
3249
|
+
getCountryVisualState,
|
|
3250
|
+
async () => {
|
|
3251
|
+
// Clear selection and select different countries (Japan, Australia) to test centering
|
|
3252
|
+
// Click expand button
|
|
3253
|
+
const expandBtn = countryMultiSelect.querySelector('button[aria-label="Expand"]') as HTMLButtonElement
|
|
3254
|
+
await userEvent.click(expandBtn)
|
|
3255
|
+
|
|
3256
|
+
// Clear existing selections first by clicking remove buttons
|
|
3257
|
+
const removeButtons = countryMultiSelect.querySelectorAll('button[aria-label="Remove"]')
|
|
3258
|
+
for (const button of Array.from(removeButtons)) {
|
|
3259
|
+
await userEvent.click(button as HTMLButtonElement)
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
// Select Japan
|
|
3263
|
+
await userEvent.click(expandBtn) // Reopen dropdown
|
|
3264
|
+
const japanOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
3265
|
+
li.textContent?.includes('Japan')
|
|
3266
|
+
) as HTMLElement
|
|
3267
|
+
expect(japanOption).toBeTruthy()
|
|
3268
|
+
await userEvent.click(japanOption)
|
|
3269
|
+
|
|
3270
|
+
// Select Australia
|
|
3271
|
+
await userEvent.click(expandBtn) // Reopen dropdown
|
|
3272
|
+
const australiaOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
3273
|
+
li.textContent?.includes('Australia')
|
|
3274
|
+
) as HTMLElement
|
|
3275
|
+
expect(australiaOption).toBeTruthy()
|
|
3276
|
+
await userEvent.click(australiaOption)
|
|
3277
|
+
|
|
3278
|
+
// Wait for map to re-render with new selection
|
|
3279
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
3280
|
+
},
|
|
3281
|
+
(before, after) => {
|
|
3282
|
+
// Verify that the selected countries changed (Japan and Australia instead of France, Germany, Italy)
|
|
3283
|
+
// The centering happens automatically via useCountryZoom hook
|
|
3284
|
+
return (
|
|
3285
|
+
after.selectedCountryCodes.includes('JPN') &&
|
|
3286
|
+
after.selectedCountryCodes.includes('AUS') &&
|
|
3287
|
+
!after.selectedCountryCodes.includes('FRA') &&
|
|
3288
|
+
after.selectedCountryCodes.length === 2
|
|
3289
|
+
)
|
|
3290
|
+
}
|
|
3291
|
+
)
|
|
3292
|
+
|
|
3293
|
+
// ==========================================================================
|
|
3294
|
+
// TEST: Clear Country Selection (Reset to full world map)
|
|
3295
|
+
// ==========================================================================
|
|
3296
|
+
await performAndAssert(
|
|
3297
|
+
'Multi-Country → Clear all selections',
|
|
3298
|
+
getCountryVisualState,
|
|
3299
|
+
async () => {
|
|
3300
|
+
// Remove all selected countries using the remove buttons
|
|
3301
|
+
const removeButtons = countryMultiSelect.querySelectorAll('button[aria-label="Remove"]')
|
|
3302
|
+
for (const button of Array.from(removeButtons)) {
|
|
3303
|
+
await userEvent.click(button as HTMLButtonElement)
|
|
3304
|
+
}
|
|
3305
|
+
},
|
|
3306
|
+
(before, after) => {
|
|
3307
|
+
// When all countries are cleared, should return to normal world map state
|
|
3308
|
+
// Note: selectedCountryCodes will contain ALL countries since all are visible (not grayed/hidden)
|
|
3309
|
+
return (
|
|
3310
|
+
!after.hasMultiCountryClass &&
|
|
3311
|
+
after.grayedCountries === 0 &&
|
|
3312
|
+
after.hiddenCountries === 0 &&
|
|
3313
|
+
after.visibleCountries === after.totalCountries // All countries visible
|
|
3314
|
+
)
|
|
3315
|
+
}
|
|
3316
|
+
)
|
|
3317
|
+
|
|
3318
|
+
// ==========================================================================
|
|
3319
|
+
// TEST: Country Selection with Data Integration
|
|
3320
|
+
// ==========================================================================
|
|
3321
|
+
await performAndAssert(
|
|
3322
|
+
'Multi-Country Data → Selected countries show data values',
|
|
3323
|
+
getCountryVisualState,
|
|
3324
|
+
async () => {
|
|
3325
|
+
// Select countries that should have data (France, Germany, Italy - these have data in the config)
|
|
3326
|
+
const expandBtn = countryMultiSelect.querySelector('button[aria-label="Expand"]') as HTMLButtonElement
|
|
3327
|
+
|
|
3328
|
+
// Select France
|
|
3329
|
+
await userEvent.click(expandBtn)
|
|
3330
|
+
const franceOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
3331
|
+
li.textContent?.includes('France')
|
|
3332
|
+
) as HTMLElement
|
|
3333
|
+
|
|
3334
|
+
expect(franceOption).toBeTruthy()
|
|
3335
|
+
await userEvent.click(franceOption)
|
|
3336
|
+
|
|
3337
|
+
// Select Germany
|
|
3338
|
+
await userEvent.click(expandBtn)
|
|
3339
|
+
const germanyOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
3340
|
+
li.textContent?.includes('Germany')
|
|
3341
|
+
) as HTMLElement
|
|
3342
|
+
|
|
3343
|
+
expect(germanyOption).toBeTruthy()
|
|
3344
|
+
await userEvent.click(germanyOption)
|
|
3345
|
+
|
|
3346
|
+
// Select Italy
|
|
3347
|
+
await userEvent.click(expandBtn)
|
|
3348
|
+
const italyOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
3349
|
+
li.textContent?.includes('Italy')
|
|
3350
|
+
) as HTMLElement
|
|
3351
|
+
|
|
3352
|
+
expect(italyOption).toBeTruthy()
|
|
3353
|
+
await userEvent.click(italyOption)
|
|
3354
|
+
|
|
3355
|
+
// Wait for data to be applied
|
|
3356
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
3357
|
+
},
|
|
3358
|
+
(before, after) => {
|
|
3359
|
+
// Verify that the countries with data are correctly selected
|
|
3360
|
+
// (Data rendering and legend display are separate from multi-country selection functionality)
|
|
3361
|
+
return (
|
|
3362
|
+
after.selectedCountryCodes.includes('FRA') &&
|
|
3363
|
+
after.selectedCountryCodes.includes('DEU') &&
|
|
3364
|
+
after.selectedCountryCodes.includes('ITA') &&
|
|
3365
|
+
after.hasMultiCountryClass &&
|
|
3366
|
+
after.selectedCountryCodes.length === 3
|
|
3367
|
+
)
|
|
3368
|
+
}
|
|
3369
|
+
)
|
|
3370
|
+
}
|
|
3371
|
+
}
|