@cdc/map 4.26.3 → 4.26.4
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/CONFIG.md +235 -0
- package/README.md +70 -24
- package/dist/cdcmap-CY9IcPSi.es.js +6 -0
- package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
- package/dist/cdcmap.js +27405 -26257
- package/examples/{testing-layer-2.json → __data__/testing-layer-2.json} +1 -1
- package/examples/{testing-layer.json → __data__/testing-layer.json} +1 -1
- package/examples/county-hsa-toggle.json +51993 -0
- package/examples/custom-map-layers.json +2 -2
- package/examples/default-county.json +3 -3
- package/examples/minimal-example.json +69 -0
- package/examples/private/annotation-bug.json +2 -2
- package/examples/private/css-issue.json +314 -0
- package/examples/private/region-breaking.json +1639 -0
- package/examples/private/test1.json +27247 -0
- package/package.json +4 -4
- package/src/CdcMapComponent.tsx +96 -13
- package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +601 -0
- package/src/_stories/CdcMap.Editor.DataTableSectionTests.stories.tsx +404 -0
- package/src/_stories/CdcMap.Editor.FiltersSectionTests.stories.tsx +229 -0
- package/src/_stories/CdcMap.Editor.GeneralSectionTests.stories.tsx +262 -0
- package/src/_stories/CdcMap.Editor.LegendSectionTests.stories.tsx +541 -0
- package/src/_stories/CdcMap.Editor.MultiCountryWorldMapTests.stories.tsx +359 -0
- package/src/_stories/CdcMap.Editor.PatternSettingsSectionTests.stories.tsx +516 -0
- package/src/_stories/CdcMap.Editor.SmallMultiplesSectionTests.stories.tsx +165 -0
- package/src/_stories/CdcMap.Editor.TextAnnotationsSectionTests.stories.tsx +145 -0
- package/src/_stories/CdcMap.Editor.TypeSectionTests.stories.tsx +312 -0
- package/src/_stories/CdcMap.Editor.VisualSectionTests.stories.tsx +359 -0
- package/src/_stories/CdcMap.Editor.ZoomControlsTests.stories.tsx +88 -0
- package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +12 -0
- package/src/_stories/_mock/legends/legend-tests.json +3 -3
- package/src/components/Annotation/AnnotationList.tsx +1 -1
- package/src/components/EditorPanel/components/EditorPanel.tsx +504 -383
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +112 -117
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +26 -13
- package/src/components/EditorPanel/components/editorPanel.styles.css +22 -2
- package/src/components/Legend/components/Legend.tsx +3 -3
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
- package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
- package/src/components/UsaMap/components/UsaMap.County.tsx +271 -100
- package/src/components/UsaMap/components/UsaMap.State.tsx +1 -1
- package/src/components/UsaMap/data/cb_2019_us_county_20m.json +75817 -1
- package/src/components/UsaMap/data/hsa_fips_mapping.json +3144 -0
- package/src/components/WorldMap/data/world-topo.json +1 -1
- package/src/data/initial-state.js +1 -0
- package/src/data/supported-counties.json +1 -1
- package/src/helpers/countyTerritories.ts +38 -0
- package/src/helpers/dataTableHelpers.ts +35 -6
- package/src/helpers/tests/countyTerritories.test.ts +87 -0
- package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
- package/src/hooks/useMapLayers.tsx +1 -1
- package/src/hooks/useTooltip.ts +18 -7
- package/src/store/map.actions.ts +5 -2
- package/src/store/map.reducer.ts +12 -3
- package/src/test/CdcMap.test.jsx +24 -0
- package/src/types/MapConfig.ts +6 -0
- package/src/types/MapContext.ts +3 -1
- package/topojson-updater/README.txt +1 -1
- package/LICENSE +0 -201
- package/dist/cdcmap-vr9HZwRt.es.js +0 -6
- package/examples/__data__/city-state-data.json +0 -668
- package/examples/city-state.json +0 -434
- package/examples/default-world-data.json +0 -1450
- package/examples/new-cities.json +0 -656
- package/src/_stories/CdcMap.Editor.stories.tsx +0 -3648
- package/topojson-updater/package-lock.json +0 -223
- /package/src/_stories/{CdcMap.ColumnWrap.stories.tsx → CdcMap.ColumnWrap.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Defaults.stories.tsx → CdcMap.Defaults.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.DistrictOfColumbia.stories.tsx → CdcMap.DistrictOfColumbia.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Filters.stories.tsx → CdcMap.Filters.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Legend.Gradient.stories.tsx → CdcMap.Legend.Gradient.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Legend.stories.tsx → CdcMap.Legend.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Patterns.stories.tsx → CdcMap.Patterns.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.SmallMultiples.stories.tsx → CdcMap.SmallMultiples.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Table.stories.tsx → CdcMap.Table.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.ZeroColor.stories.tsx → CdcMap.ZeroColor.smoke.stories.tsx} +0 -0
- /package/src/_stories/{GoogleMap.stories.tsx → GoogleMap.smoke.stories.tsx} +0 -0
- /package/src/_stories/{UsaMap.NoData.stories.tsx → UsaMap.NoData.smoke.stories.tsx} +0 -0
|
@@ -0,0 +1,601 @@
|
|
|
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 ColumnsSectionTests: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
...DEFAULT_ARGS
|
|
29
|
+
},
|
|
30
|
+
play: async ({ canvasElement }) => {
|
|
31
|
+
const setTextFieldValue = (input: HTMLInputElement | HTMLTextAreaElement, value: string) => {
|
|
32
|
+
const prototype =
|
|
33
|
+
input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype
|
|
34
|
+
const valueSetter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set
|
|
35
|
+
valueSetter?.call(input, value)
|
|
36
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
37
|
+
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
38
|
+
}
|
|
39
|
+
const canvas = within(canvasElement)
|
|
40
|
+
|
|
41
|
+
await waitForEditor(canvas)
|
|
42
|
+
await waitForPresence('.map-container', canvasElement)
|
|
43
|
+
|
|
44
|
+
await openAccordion(canvas, 'Columns')
|
|
45
|
+
|
|
46
|
+
// ==========================================================================
|
|
47
|
+
// TEST: Geography column select
|
|
48
|
+
// Verifies: Changing geo column to invalid data makes map gray and legend shows "No data"
|
|
49
|
+
// ==========================================================================
|
|
50
|
+
const columnsAccordionButton = canvas
|
|
51
|
+
.getAllByRole('button', { name: /^Columns$/i })
|
|
52
|
+
.find(button => button.closest('[data-accordion-component="AccordionItem"], .accordion__item'))
|
|
53
|
+
const columnsAccordion = columnsAccordionButton?.closest(
|
|
54
|
+
'[data-accordion-component="AccordionItem"], .accordion__item'
|
|
55
|
+
)
|
|
56
|
+
expect(columnsAccordion).toBeTruthy()
|
|
57
|
+
const geoColumnLabel = Array.from(columnsAccordion?.querySelectorAll('label') || []).find(label => {
|
|
58
|
+
const columnHeading = label.querySelector('.edit-label.column-heading')
|
|
59
|
+
return columnHeading?.textContent?.includes('Geography')
|
|
60
|
+
})
|
|
61
|
+
const geoSelect = geoColumnLabel?.querySelector('select') as HTMLSelectElement
|
|
62
|
+
expect(geoSelect).toBeTruthy()
|
|
63
|
+
|
|
64
|
+
const getMapAndLegendState = () => {
|
|
65
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
66
|
+
const legendSvg = legendContainer?.querySelector('svg')
|
|
67
|
+
const gradientStops = legendSvg?.querySelectorAll('linearGradient stop')
|
|
68
|
+
|
|
69
|
+
// Gradient legend has color stops when data is valid
|
|
70
|
+
const hasGradientStops = (gradientStops?.length || 0) > 0
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
legendHTML: legendContainer?.innerHTML || '',
|
|
74
|
+
hasGradientStops: hasGradientStops,
|
|
75
|
+
gradientStopCount: gradientStops?.length || 0
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Change to a column that's NOT valid geography data (Rate instead of STATE)
|
|
80
|
+
await performAndAssert(
|
|
81
|
+
'Geography Column → Change to invalid column',
|
|
82
|
+
getMapAndLegendState,
|
|
83
|
+
async () => {
|
|
84
|
+
await userEvent.selectOptions(geoSelect, 'Rate')
|
|
85
|
+
},
|
|
86
|
+
(before, after) => {
|
|
87
|
+
// Legend gradient should lose color stops when geo column is invalid
|
|
88
|
+
return before.hasGradientStops && !after.hasGradientStops
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// Change back to valid geography column
|
|
93
|
+
await performAndAssert(
|
|
94
|
+
'Geography Column → Change back to valid column',
|
|
95
|
+
getMapAndLegendState,
|
|
96
|
+
async () => {
|
|
97
|
+
await userEvent.selectOptions(geoSelect, 'STATE')
|
|
98
|
+
},
|
|
99
|
+
(before, after) => {
|
|
100
|
+
// Legend gradient should regain color stops when geo column is valid
|
|
101
|
+
return !before.hasGradientStops && after.hasGradientStops
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
// ==========================================================================
|
|
106
|
+
// TEST: Hide Geography Column Name in Tooltip checkbox
|
|
107
|
+
// Verifies: Tooltip content changes when geography label is hidden/shown
|
|
108
|
+
// ==========================================================================
|
|
109
|
+
const hideGeoLabel = Array.from(columnsAccordion?.querySelectorAll('label') || []).find(label =>
|
|
110
|
+
label.textContent?.includes('Hide Geography Column Name in Tooltip')
|
|
111
|
+
)
|
|
112
|
+
const hideGeoCheckbox = hideGeoLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
|
113
|
+
expect(hideGeoCheckbox).toBeTruthy()
|
|
114
|
+
|
|
115
|
+
const getTooltipContent = () => {
|
|
116
|
+
const geoGroups = Array.from(canvasElement.querySelectorAll('g.geo-group')) as SVGGElement[]
|
|
117
|
+
const tooltipHtml =
|
|
118
|
+
geoGroups.map(group => group.getAttribute('data-tooltip-html') || '').find(html => html.trim().length > 0) || ''
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
tooltipContent: tooltipHtml,
|
|
122
|
+
hasTooltipHeading: tooltipHtml.includes('tooltip-heading'),
|
|
123
|
+
hasStatePrefix: tooltipHtml.includes('State:'),
|
|
124
|
+
usesStrongGeoName: tooltipHtml.startsWith('<strong>')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await performAndAssert(
|
|
129
|
+
'Wait for geography tooltip to populate',
|
|
130
|
+
getTooltipContent,
|
|
131
|
+
async () => {},
|
|
132
|
+
(_before, after) => after.tooltipContent.length > 0
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
await performAndAssert(
|
|
136
|
+
'Hide Geography Column Name → Enable',
|
|
137
|
+
getTooltipContent,
|
|
138
|
+
async () => {
|
|
139
|
+
await userEvent.click(hideGeoCheckbox)
|
|
140
|
+
},
|
|
141
|
+
(before, after) => {
|
|
142
|
+
// Hidden geography labels now render as a standalone <strong> name instead of a tooltip heading.
|
|
143
|
+
return before.tooltipContent.length > 0 && after.usesStrongGeoName && !after.hasTooltipHeading
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
await performAndAssert(
|
|
148
|
+
'Hide Geography Column Name → Disable',
|
|
149
|
+
getTooltipContent,
|
|
150
|
+
async () => {
|
|
151
|
+
await userEvent.click(hideGeoCheckbox)
|
|
152
|
+
},
|
|
153
|
+
(before, after) => {
|
|
154
|
+
// Re-enabling restores the heading-based tooltip format.
|
|
155
|
+
return (
|
|
156
|
+
before.usesStrongGeoName &&
|
|
157
|
+
after.hasTooltipHeading &&
|
|
158
|
+
(after.hasStatePrefix || after.tooltipContent.length > 0)
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
// ==========================================================================
|
|
164
|
+
// TEST: Geography Label field
|
|
165
|
+
// Verifies: Custom geography label appears in tooltip instead of default "State:"
|
|
166
|
+
// ==========================================================================
|
|
167
|
+
const geoLabelInput = Array.from(columnsAccordion?.querySelectorAll('input[type="text"]') || []).find(input => {
|
|
168
|
+
const label = input.closest('label')
|
|
169
|
+
return label?.textContent?.includes('Geography Label')
|
|
170
|
+
}) as HTMLInputElement
|
|
171
|
+
expect(geoLabelInput).toBeTruthy()
|
|
172
|
+
|
|
173
|
+
await performAndAssert(
|
|
174
|
+
'Geography Label → Add custom label',
|
|
175
|
+
getTooltipContent,
|
|
176
|
+
async () => {
|
|
177
|
+
geoLabelInput.scrollIntoView({ block: 'center' })
|
|
178
|
+
setTextFieldValue(geoLabelInput, 'Region')
|
|
179
|
+
},
|
|
180
|
+
(before, after) => {
|
|
181
|
+
// Custom label "Region:" should appear in tooltip
|
|
182
|
+
return !before.tooltipContent.includes('Region:') && after.tooltipContent.includes('Region:')
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
// ==========================================================================
|
|
187
|
+
// TEST: Data Column select
|
|
188
|
+
// Verifies: Changing data column changes tooltip data values
|
|
189
|
+
// ==========================================================================
|
|
190
|
+
const dataColumnLabel = Array.from(columnsAccordion?.querySelectorAll('label') || []).find(label =>
|
|
191
|
+
label.textContent?.includes('Data Column')
|
|
192
|
+
)
|
|
193
|
+
const dataColumnSelect = dataColumnLabel?.querySelector('select') as HTMLSelectElement
|
|
194
|
+
expect(dataColumnSelect).toBeTruthy()
|
|
195
|
+
|
|
196
|
+
const getDataTooltipContent = () => {
|
|
197
|
+
const geoGroup = canvasElement.querySelector('g.geo-group') as SVGGElement
|
|
198
|
+
const tooltipHtml = geoGroup?.getAttribute('data-tooltip-html') || ''
|
|
199
|
+
return {
|
|
200
|
+
tooltipContent: tooltipHtml
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await performAndAssert(
|
|
205
|
+
'Data Column → Change to STATE column',
|
|
206
|
+
getDataTooltipContent,
|
|
207
|
+
async () => {
|
|
208
|
+
await userEvent.selectOptions(dataColumnSelect, 'STATE')
|
|
209
|
+
},
|
|
210
|
+
(before, after) => {
|
|
211
|
+
// Tooltip should show STATE values (e.g., state names) instead of Rate values (numbers)
|
|
212
|
+
const beforeHasNumbers = /\d+/.test(before.tooltipContent)
|
|
213
|
+
const afterHasStateNames = /Alabama|Alaska|Arizona|Arkansas|California/.test(after.tooltipContent)
|
|
214
|
+
return beforeHasNumbers && afterHasStateNames
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
// Change back to Rate
|
|
219
|
+
await performAndAssert(
|
|
220
|
+
'Data Column → Change back to Rate',
|
|
221
|
+
getDataTooltipContent,
|
|
222
|
+
async () => {
|
|
223
|
+
await userEvent.selectOptions(dataColumnSelect, 'Rate')
|
|
224
|
+
},
|
|
225
|
+
(before, after) => {
|
|
226
|
+
// Tooltip should show numeric Rate values again
|
|
227
|
+
const beforeHasStateNames = /Alabama|Alaska|Arizona|Arkansas|California/.test(before.tooltipContent)
|
|
228
|
+
const afterHasNumbers = /\d+/.test(after.tooltipContent)
|
|
229
|
+
return beforeHasStateNames && afterHasNumbers
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
// ==========================================================================
|
|
234
|
+
// TEST: Hide Data Column Name in Tooltip
|
|
235
|
+
// Verifies: Data column label disappears from tooltip when hidden
|
|
236
|
+
// ==========================================================================
|
|
237
|
+
const hideDataLabel = Array.from(columnsAccordion?.querySelectorAll('label') || []).find(label =>
|
|
238
|
+
label.textContent?.includes('Hide Data Column Name in Tooltip')
|
|
239
|
+
)
|
|
240
|
+
const hideDataCheckbox = hideDataLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
|
241
|
+
expect(hideDataCheckbox).toBeTruthy()
|
|
242
|
+
|
|
243
|
+
await performAndAssert(
|
|
244
|
+
'Hide Data Column Name → Enable',
|
|
245
|
+
getDataTooltipContent,
|
|
246
|
+
async () => {
|
|
247
|
+
await userEvent.click(hideDataCheckbox)
|
|
248
|
+
},
|
|
249
|
+
(before, after) => {
|
|
250
|
+
// The label prefix (e.g., "Rate:" or similar) should be removed
|
|
251
|
+
// Before should have a colon pattern indicating a label, after should have less structure
|
|
252
|
+
const beforeHasLabelStructure = before.tooltipContent.includes(':</li>') || before.tooltipContent.includes(': ')
|
|
253
|
+
const afterHasLessStructure = after.tooltipContent.length < before.tooltipContent.length
|
|
254
|
+
return beforeHasLabelStructure && afterHasLessStructure
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
await performAndAssert(
|
|
259
|
+
'Hide Data Column Name → Disable',
|
|
260
|
+
getDataTooltipContent,
|
|
261
|
+
async () => {
|
|
262
|
+
await userEvent.click(hideDataCheckbox)
|
|
263
|
+
},
|
|
264
|
+
(before, after) => {
|
|
265
|
+
// The label prefix should reappear
|
|
266
|
+
const beforeHasLessStructure = before.tooltipContent.length < after.tooltipContent.length
|
|
267
|
+
const afterHasLabelStructure = after.tooltipContent.includes(':</li>') || after.tooltipContent.includes(': ')
|
|
268
|
+
return beforeHasLessStructure && afterHasLabelStructure
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
// ==========================================================================
|
|
273
|
+
// TEST: Data Label field
|
|
274
|
+
// Verifies: Custom data label appears in tooltip
|
|
275
|
+
// ==========================================================================
|
|
276
|
+
const dataLabelInputs = Array.from(columnsAccordion?.querySelectorAll('input') || [])
|
|
277
|
+
const dataLabelInput = dataLabelInputs.find(input => {
|
|
278
|
+
const label = input.closest('label')
|
|
279
|
+
return label?.textContent?.includes('Data Label')
|
|
280
|
+
}) as HTMLInputElement
|
|
281
|
+
expect(dataLabelInput).toBeTruthy()
|
|
282
|
+
|
|
283
|
+
await performAndAssert(
|
|
284
|
+
'Data Label → Add custom label',
|
|
285
|
+
getDataTooltipContent,
|
|
286
|
+
async () => {
|
|
287
|
+
dataLabelInput.scrollIntoView({ block: 'center' })
|
|
288
|
+
setTextFieldValue(dataLabelInput, 'Value')
|
|
289
|
+
},
|
|
290
|
+
(before, after) => {
|
|
291
|
+
// Custom label "Value:" should appear in tooltip
|
|
292
|
+
return !before.tooltipContent.includes('Value:') && after.tooltipContent.includes('Value:')
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
// ==========================================================================
|
|
297
|
+
// TEST: Prefix field
|
|
298
|
+
// Verifies: Prefix appears before data values in both tooltip and legend
|
|
299
|
+
// ==========================================================================
|
|
300
|
+
const prefixInputs = Array.from(columnsAccordion?.querySelectorAll('input') || [])
|
|
301
|
+
const prefixInput = prefixInputs.find(input => {
|
|
302
|
+
const label = input.closest('label')
|
|
303
|
+
return label?.textContent?.includes('Prefix')
|
|
304
|
+
}) as HTMLInputElement
|
|
305
|
+
expect(prefixInput).toBeTruthy()
|
|
306
|
+
|
|
307
|
+
const getPrefixVisualization = () => {
|
|
308
|
+
const geoGroup = canvasElement.querySelector('g.geo-group') as SVGGElement
|
|
309
|
+
const tooltipHtml = geoGroup?.getAttribute('data-tooltip-html') || ''
|
|
310
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
311
|
+
const legendSvg = legendContainer?.querySelector('svg')
|
|
312
|
+
const legendText = legendSvg?.textContent || ''
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
tooltipHasPrefix: tooltipHtml.includes('$'),
|
|
316
|
+
legendHasPrefix: legendText.includes('$'),
|
|
317
|
+
tooltipContent: tooltipHtml,
|
|
318
|
+
legendContent: legendText
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
await performAndAssert(
|
|
323
|
+
'Prefix → Add dollar sign',
|
|
324
|
+
getPrefixVisualization,
|
|
325
|
+
async () => {
|
|
326
|
+
prefixInput.scrollIntoView({ block: 'center' })
|
|
327
|
+
setTextFieldValue(prefixInput, '$')
|
|
328
|
+
},
|
|
329
|
+
(before, after) => {
|
|
330
|
+
// Dollar sign should appear in both tooltip and legend
|
|
331
|
+
return !before.tooltipHasPrefix && after.tooltipHasPrefix && !before.legendHasPrefix && after.legendHasPrefix
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
// ==========================================================================
|
|
336
|
+
// TEST: Suffix field
|
|
337
|
+
// Verifies: Suffix appears after data values in both tooltip and legend
|
|
338
|
+
// ==========================================================================
|
|
339
|
+
const suffixInputs = Array.from(columnsAccordion?.querySelectorAll('input') || [])
|
|
340
|
+
const suffixInput = suffixInputs.find(input => {
|
|
341
|
+
const label = input.closest('label')
|
|
342
|
+
return label?.textContent?.includes('Suffix')
|
|
343
|
+
}) as HTMLInputElement
|
|
344
|
+
expect(suffixInput).toBeTruthy()
|
|
345
|
+
|
|
346
|
+
const getSuffixVisualization = () => {
|
|
347
|
+
const geoGroup = canvasElement.querySelector('g.geo-group') as SVGGElement
|
|
348
|
+
const tooltipHtml = geoGroup?.getAttribute('data-tooltip-html') || ''
|
|
349
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
350
|
+
const legendSvg = legendContainer?.querySelector('svg')
|
|
351
|
+
const legendText = legendSvg?.textContent || ''
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
tooltipHasSuffix: tooltipHtml.includes('%'),
|
|
355
|
+
legendHasSuffix: legendText.includes('%'),
|
|
356
|
+
tooltipContent: tooltipHtml,
|
|
357
|
+
legendContent: legendText
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
await performAndAssert(
|
|
362
|
+
'Suffix → Add percent sign',
|
|
363
|
+
getSuffixVisualization,
|
|
364
|
+
async () => {
|
|
365
|
+
suffixInput.scrollIntoView({ block: 'center' })
|
|
366
|
+
setTextFieldValue(suffixInput, '%')
|
|
367
|
+
},
|
|
368
|
+
(before, after) => {
|
|
369
|
+
// Percent sign should appear in both tooltip and legend
|
|
370
|
+
return !before.tooltipHasSuffix && after.tooltipHasSuffix && !before.legendHasSuffix && after.legendHasSuffix
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
// ==========================================================================
|
|
375
|
+
// TEST: Round field
|
|
376
|
+
// Verifies: Changing rounding adds decimal places in both tooltip and legend
|
|
377
|
+
// ==========================================================================
|
|
378
|
+
const roundInputs = Array.from(columnsAccordion?.querySelectorAll('input') || [])
|
|
379
|
+
const roundInput = roundInputs.find(input => {
|
|
380
|
+
const label = input.closest('label')
|
|
381
|
+
return label?.textContent?.includes('Round')
|
|
382
|
+
}) as HTMLInputElement
|
|
383
|
+
expect(roundInput).toBeTruthy()
|
|
384
|
+
|
|
385
|
+
const getRoundingVisualization = () => {
|
|
386
|
+
const geoGroup = canvasElement.querySelector('g.geo-group') as SVGGElement
|
|
387
|
+
const tooltipHtml = geoGroup?.getAttribute('data-tooltip-html') || ''
|
|
388
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
389
|
+
const legendSvg = legendContainer?.querySelector('svg')
|
|
390
|
+
const legendText = legendSvg?.textContent || ''
|
|
391
|
+
|
|
392
|
+
// Check for 2 decimal places pattern (e.g., "12.34", "0.50", etc.)
|
|
393
|
+
const twoDecimalPattern = /\d+\.\d{2}/
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
tooltipHasTwoDecimals: twoDecimalPattern.test(tooltipHtml),
|
|
397
|
+
legendHasTwoDecimals: twoDecimalPattern.test(legendText),
|
|
398
|
+
tooltipContent: tooltipHtml,
|
|
399
|
+
legendContent: legendText
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await performAndAssert(
|
|
404
|
+
'Round → Set to 2 decimal places',
|
|
405
|
+
getRoundingVisualization,
|
|
406
|
+
async () => {
|
|
407
|
+
roundInput.scrollIntoView({ block: 'center' })
|
|
408
|
+
setTextFieldValue(roundInput, '2')
|
|
409
|
+
},
|
|
410
|
+
(before, after) => {
|
|
411
|
+
// Numbers should now show 2 decimal places in both tooltip and legend
|
|
412
|
+
return (
|
|
413
|
+
!before.tooltipHasTwoDecimals &&
|
|
414
|
+
after.tooltipHasTwoDecimals &&
|
|
415
|
+
!before.legendHasTwoDecimals &&
|
|
416
|
+
after.legendHasTwoDecimals
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
// ==========================================================================
|
|
422
|
+
// TEST: Show in Data Table checkbox
|
|
423
|
+
// Verifies: Primary column appears/disappears in the data table
|
|
424
|
+
// ==========================================================================
|
|
425
|
+
const showInTableLabel = Array.from(columnsAccordion?.querySelectorAll('label') || []).find(label =>
|
|
426
|
+
label.textContent?.includes('Show in Data Table')
|
|
427
|
+
)
|
|
428
|
+
const showInTableCheckbox = showInTableLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
|
429
|
+
expect(showInTableCheckbox).toBeTruthy()
|
|
430
|
+
|
|
431
|
+
const getDataTableState = () => {
|
|
432
|
+
const dataTable = canvasElement.querySelector('.data-table-container, table')
|
|
433
|
+
const tableHeaders = Array.from(dataTable?.querySelectorAll('th') || [])
|
|
434
|
+
const tableHeaderText = tableHeaders.map(th => th.textContent?.trim() || '')
|
|
435
|
+
|
|
436
|
+
// Check if "Rate" column header appears in data table
|
|
437
|
+
const hasRateColumn = tableHeaderText.some(text => text.includes('Rate') || text.includes('Value'))
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
hasDataTable: Boolean(dataTable),
|
|
441
|
+
tableHeaders: tableHeaderText,
|
|
442
|
+
hasRateColumn: hasRateColumn
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Test config has dataTable: true for primary column
|
|
447
|
+
await performAndAssert(
|
|
448
|
+
'Show in Data Table → Disable',
|
|
449
|
+
getDataTableState,
|
|
450
|
+
async () => {
|
|
451
|
+
await userEvent.click(showInTableCheckbox)
|
|
452
|
+
},
|
|
453
|
+
(before, after) => {
|
|
454
|
+
// Rate column should disappear from data table headers
|
|
455
|
+
return before.hasRateColumn && !after.hasRateColumn
|
|
456
|
+
}
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
await performAndAssert(
|
|
460
|
+
'Show in Data Table → Enable',
|
|
461
|
+
getDataTableState,
|
|
462
|
+
async () => {
|
|
463
|
+
await userEvent.click(showInTableCheckbox)
|
|
464
|
+
},
|
|
465
|
+
(before, after) => {
|
|
466
|
+
// Rate column should reappear in data table headers
|
|
467
|
+
return !before.hasRateColumn && after.hasRateColumn
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
// ==========================================================================
|
|
472
|
+
// TEST: Show in Tooltips checkbox
|
|
473
|
+
// Verifies: Primary column data appears/disappears in tooltips
|
|
474
|
+
// ==========================================================================
|
|
475
|
+
const showInTooltipsLabel = Array.from(columnsAccordion?.querySelectorAll('label') || []).find(label =>
|
|
476
|
+
label.textContent?.includes('Show in Tooltips')
|
|
477
|
+
)
|
|
478
|
+
const showInTooltipsCheckbox = showInTooltipsLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
|
|
479
|
+
expect(showInTooltipsCheckbox).toBeTruthy()
|
|
480
|
+
|
|
481
|
+
const getTooltipDataState = () => {
|
|
482
|
+
const geoGroup = canvasElement.querySelector('g.geo-group') as SVGGElement
|
|
483
|
+
const tooltipHtml = geoGroup?.getAttribute('data-tooltip-html') || ''
|
|
484
|
+
|
|
485
|
+
// Check if tooltip contains data values (numbers with potential $ and % from earlier tests)
|
|
486
|
+
// Look for patterns like list items with numeric values
|
|
487
|
+
const hasDataValues = tooltipHtml.includes('<li') && /\$?\d+\.\d{2}%?/.test(tooltipHtml)
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
tooltipContent: tooltipHtml,
|
|
491
|
+
hasDataValues: hasDataValues,
|
|
492
|
+
tooltipLength: tooltipHtml.length
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Test config has tooltip: true for primary column
|
|
497
|
+
await performAndAssert(
|
|
498
|
+
'Show in Tooltips → Disable',
|
|
499
|
+
getTooltipDataState,
|
|
500
|
+
async () => {
|
|
501
|
+
await userEvent.click(showInTooltipsCheckbox)
|
|
502
|
+
},
|
|
503
|
+
(before, after) => {
|
|
504
|
+
// Tooltip should no longer contain data values, should be shorter
|
|
505
|
+
return before.hasDataValues && !after.hasDataValues && after.tooltipLength < before.tooltipLength
|
|
506
|
+
}
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
await performAndAssert(
|
|
510
|
+
'Show in Tooltips → Enable',
|
|
511
|
+
getTooltipDataState,
|
|
512
|
+
async () => {
|
|
513
|
+
await userEvent.click(showInTooltipsCheckbox)
|
|
514
|
+
},
|
|
515
|
+
(before, after) => {
|
|
516
|
+
// Tooltip should contain data values again, should be longer
|
|
517
|
+
return !before.hasDataValues && after.hasDataValues && after.tooltipLength > before.tooltipLength
|
|
518
|
+
}
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
// ==========================================================================
|
|
522
|
+
// TEST: Add Special Class button
|
|
523
|
+
// Verifies: New special class item appears in legend
|
|
524
|
+
// ==========================================================================
|
|
525
|
+
const addSpecialClassButton = Array.from(columnsAccordion?.querySelectorAll('button') || []).find(button =>
|
|
526
|
+
button.textContent?.includes('Add Special Class')
|
|
527
|
+
) as HTMLButtonElement
|
|
528
|
+
expect(addSpecialClassButton).toBeTruthy()
|
|
529
|
+
|
|
530
|
+
const getSpecialClassVisualization = () => {
|
|
531
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
532
|
+
const legendSvg = legendContainer?.querySelector('svg')
|
|
533
|
+
const gradientStops = legendSvg?.querySelectorAll('linearGradient stop')
|
|
534
|
+
const gradientRects = legendSvg?.querySelectorAll('g.visx-group rect')
|
|
535
|
+
|
|
536
|
+
// Get all stop colors to detect changes
|
|
537
|
+
const stopColors = Array.from(gradientStops || []).map(stop => (stop as SVGStopElement).getAttribute('style'))
|
|
538
|
+
|
|
539
|
+
// Get all rect fills to detect new colors
|
|
540
|
+
const rectFills = Array.from(gradientRects || []).map(rect => (rect as SVGRectElement).getAttribute('fill'))
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
gradientStopCount: gradientStops?.length || 0,
|
|
544
|
+
gradientRectCount: gradientRects?.length || 0,
|
|
545
|
+
stopColors: stopColors,
|
|
546
|
+
rectFills: rectFills,
|
|
547
|
+
legendHTML: legendContainer?.innerHTML || ''
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
await performAndAssert(
|
|
552
|
+
'Add Special Class → Configure with STATE=Alabama',
|
|
553
|
+
getSpecialClassVisualization,
|
|
554
|
+
async () => {
|
|
555
|
+
// Click the Add Special Class button
|
|
556
|
+
await userEvent.click(addSpecialClassButton)
|
|
557
|
+
|
|
558
|
+
// Find the newly added special class fields
|
|
559
|
+
const specialClassSection = Array.from(columnsAccordion?.querySelectorAll('.edit-block') || []).find(block =>
|
|
560
|
+
block.textContent?.includes('Special Class 1')
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
// Get all selects in the special class section
|
|
564
|
+
// First select is Data Key, second is Value
|
|
565
|
+
const selects = Array.from(specialClassSection?.querySelectorAll('select') || [])
|
|
566
|
+
const dataKeySelect = selects[0] as HTMLSelectElement
|
|
567
|
+
const valueSelect = selects[1] as HTMLSelectElement
|
|
568
|
+
|
|
569
|
+
// Set Data Key to STATE
|
|
570
|
+
await userEvent.selectOptions(dataKeySelect, 'STATE')
|
|
571
|
+
|
|
572
|
+
// Wait for the Value select to populate with options based on STATE column
|
|
573
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
574
|
+
|
|
575
|
+
// Select "Alabama" from the Value select
|
|
576
|
+
await userEvent.selectOptions(valueSelect, 'Alabama')
|
|
577
|
+
|
|
578
|
+
// Find and fill in the Label field
|
|
579
|
+
const labelInput = Array.from(specialClassSection?.querySelectorAll('input') || []).find(input => {
|
|
580
|
+
const label = input.closest('label')
|
|
581
|
+
return label?.textContent?.includes('Label')
|
|
582
|
+
}) as HTMLInputElement
|
|
583
|
+
|
|
584
|
+
labelInput.scrollIntoView({ block: 'center' })
|
|
585
|
+
setTextFieldValue(labelInput, 'Alabama')
|
|
586
|
+
},
|
|
587
|
+
(before, after) => {
|
|
588
|
+
// Check that "Alabama" text appears in the legend
|
|
589
|
+
const alabamaInLegend = after.legendHTML.includes('Alabama')
|
|
590
|
+
|
|
591
|
+
// Check that the special class gray color appears
|
|
592
|
+
const hasGrayColor = after.rectFills.includes('#b4b4b4')
|
|
593
|
+
|
|
594
|
+
// Legend should change
|
|
595
|
+
const legendChanged = after.legendHTML !== before.legendHTML
|
|
596
|
+
|
|
597
|
+
return alabamaInLegend && hasGrayColor && legendChanged
|
|
598
|
+
}
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
}
|