@cdc/map 4.26.2 → 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 +31260 -27946
- 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 +642 -0
- 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/CdcMap.tsx +3 -14
- package/src/CdcMapComponent.tsx +302 -164
- package/src/_stories/CdcMap.Defaults.smoke.stories.tsx +76 -0
- 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} +23 -1
- package/src/_stories/Map.HTMLInDataTable.stories.tsx +385 -0
- package/src/_stories/_mock/legends/legend-tests.json +3 -3
- package/src/_stories/_mock/multi-state-show-unselected.json +82 -0
- package/src/cdcMapComponent.styles.css +2 -2
- package/src/components/Annotation/Annotation.Draggable.styles.css +4 -4
- package/src/components/Annotation/AnnotationDropdown.styles.css +1 -1
- package/src/components/Annotation/AnnotationList.styles.css +13 -13
- package/src/components/Annotation/AnnotationList.tsx +1 -1
- package/src/components/EditorPanel/components/EditorPanel.tsx +905 -416
- 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-style.css +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +31 -15
- package/src/components/EditorPanel/components/editorPanel.styles.css +55 -25
- package/src/components/Legend/components/Legend.tsx +12 -7
- package/src/components/Legend/components/LegendGroup/legend.group.css +5 -5
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
- package/src/components/Legend/components/index.scss +2 -3
- package/src/components/NavigationMenu.tsx +2 -1
- package/src/components/SmallMultiples/SmallMultiples.css +5 -5
- package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
- package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +32 -17
- package/src/components/UsaMap/components/TerritoriesSection.tsx +3 -2
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +13 -8
- package/src/components/UsaMap/components/UsaMap.County.tsx +629 -231
- package/src/components/UsaMap/components/UsaMap.Region.styles.css +1 -1
- package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +2 -2
- package/src/components/UsaMap/components/UsaMap.State.tsx +14 -9
- 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/WorldMap.tsx +10 -13
- package/src/components/WorldMap/data/world-topo-updated.json +1 -0
- package/src/components/WorldMap/data/world-topo.json +1 -1
- package/src/components/WorldMap/worldMap.styles.css +1 -1
- package/src/components/ZoomControls.tsx +49 -18
- package/src/components/zoomControls.styles.css +27 -11
- package/src/data/initial-state.js +15 -5
- package/src/data/legacy-defaults.ts +8 -0
- package/src/data/supported-counties.json +1 -1
- package/src/data/supported-geos.js +19 -0
- package/src/helpers/colors.ts +2 -1
- package/src/helpers/countyTerritories.ts +38 -0
- package/src/helpers/dataTableHelpers.ts +85 -0
- package/src/helpers/displayGeoName.ts +19 -11
- package/src/helpers/getMapContainerClasses.ts +8 -2
- package/src/helpers/getMatchingPatternForRow.ts +67 -0
- package/src/helpers/getPatternForRow.ts +11 -18
- package/src/helpers/tests/countyTerritories.test.ts +87 -0
- package/src/helpers/tests/dataTableHelpers.test.ts +78 -0
- package/src/helpers/tests/displayGeoName.test.ts +17 -0
- package/src/helpers/tests/getMatchingPatternForRow.test.ts +150 -0
- package/src/helpers/tests/getPatternForRow.test.ts +140 -2
- package/src/helpers/urlDataHelpers.ts +7 -1
- package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
- package/src/hooks/useMapLayers.tsx +1 -1
- package/src/hooks/useResizeObserver.ts +36 -22
- package/src/hooks/useTooltip.test.tsx +64 -0
- package/src/hooks/useTooltip.ts +46 -15
- package/src/scss/editor-panel.scss +1 -1
- package/src/scss/main.scss +140 -6
- package/src/scss/map.scss +9 -4
- package/src/store/map.actions.ts +5 -0
- package/src/store/map.reducer.ts +13 -0
- package/src/test/CdcMap.test.jsx +26 -2
- package/src/types/MapConfig.ts +28 -4
- package/src/types/MapContext.ts +5 -1
- package/topojson-updater/README.txt +1 -1
- package/dist/cdcmap-Cf9_fbQf.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 -3475
- package/src/helpers/componentHelpers.ts +0 -8
- 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.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,516 @@
|
|
|
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 PatternSettingsSectionTests: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
config: usaStateGradientConfig,
|
|
29
|
+
isEditor: true
|
|
30
|
+
},
|
|
31
|
+
play: async ({ canvasElement }) => {
|
|
32
|
+
const canvas = within(canvasElement)
|
|
33
|
+
|
|
34
|
+
// Wait for editor to load
|
|
35
|
+
await waitForEditor(canvas)
|
|
36
|
+
|
|
37
|
+
// ==========================================================================
|
|
38
|
+
// TEST: Wait for Pattern Settings accordion to load (US topo JSON)
|
|
39
|
+
// ==========================================================================
|
|
40
|
+
let patternSettingsButton: HTMLElement | null = null
|
|
41
|
+
|
|
42
|
+
await performAndAssert(
|
|
43
|
+
'Pattern Settings → Wait for accordion to appear',
|
|
44
|
+
() => {
|
|
45
|
+
// Check if Pattern Settings accordion exists
|
|
46
|
+
const buttons = Array.from(canvasElement.querySelectorAll('.accordion__button'))
|
|
47
|
+
const button = buttons.find(btn => btn.textContent?.includes('Pattern Settings')) as HTMLElement
|
|
48
|
+
if (button) {
|
|
49
|
+
patternSettingsButton = button
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
accordionExists: !!button
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async () => {
|
|
56
|
+
// No action needed - just let performAndAssert's built-in polling wait for the accordion
|
|
57
|
+
},
|
|
58
|
+
(before, after) => {
|
|
59
|
+
// After waiting, the accordion should exist
|
|
60
|
+
return after.accordionExists
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// ==========================================================================
|
|
65
|
+
// TEST: Open Pattern Settings accordion and verify controls appear
|
|
66
|
+
// ==========================================================================
|
|
67
|
+
await performAndAssert(
|
|
68
|
+
'Pattern Settings → Open accordion',
|
|
69
|
+
() => {
|
|
70
|
+
// Check if the accordion panel is expanded (aria-expanded attribute)
|
|
71
|
+
const isExpanded = patternSettingsButton.getAttribute('aria-expanded') === 'true'
|
|
72
|
+
// Also check if the Add Geo Pattern button is visible
|
|
73
|
+
const buttons = Array.from(canvasElement.querySelectorAll('button'))
|
|
74
|
+
const addPatternButton = buttons.find(btn => btn.textContent?.includes('Add Geo Pattern'))
|
|
75
|
+
return {
|
|
76
|
+
isExpanded,
|
|
77
|
+
hasAddButton: !!addPatternButton
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
async () => {
|
|
81
|
+
await userEvent.click(patternSettingsButton)
|
|
82
|
+
},
|
|
83
|
+
(before, after) => {
|
|
84
|
+
// After clicking, accordion should be expanded and button should be visible
|
|
85
|
+
return !before.isExpanded && after.isExpanded && after.hasAddButton
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
// ==========================================================================
|
|
90
|
+
// TEST: Add a geo pattern and verify pattern appears on map
|
|
91
|
+
// ==========================================================================
|
|
92
|
+
await performAndAssert(
|
|
93
|
+
'Pattern Settings → Add geo pattern',
|
|
94
|
+
() => {
|
|
95
|
+
// Look for pattern elements in ALL SVGs (there might be multiple)
|
|
96
|
+
// Pattern definitions are <pattern> elements, and they're used by paths with fill="url(#...)"
|
|
97
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
98
|
+
let patternDefCount = 0
|
|
99
|
+
let patternPathCount = 0
|
|
100
|
+
|
|
101
|
+
allSvgs.forEach(svg => {
|
|
102
|
+
const patternDefs = svg.querySelectorAll('pattern')
|
|
103
|
+
patternDefCount += patternDefs.length
|
|
104
|
+
|
|
105
|
+
const allPaths = Array.from(svg.querySelectorAll('path'))
|
|
106
|
+
const patternPaths = allPaths.filter(path => {
|
|
107
|
+
const fill = path.getAttribute('fill')
|
|
108
|
+
return fill && fill.startsWith('url(#')
|
|
109
|
+
})
|
|
110
|
+
patternPathCount += patternPaths.length
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
patternDefCount,
|
|
115
|
+
patternPathCount,
|
|
116
|
+
svgCount: allSvgs.length
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
async () => {
|
|
120
|
+
// Click "Add Geo Pattern" button
|
|
121
|
+
const buttons = Array.from(canvasElement.querySelectorAll('button'))
|
|
122
|
+
const addPatternButton = buttons.find(btn => btn.textContent?.includes('Add Geo Pattern'))
|
|
123
|
+
if (!addPatternButton) {
|
|
124
|
+
throw new Error('Add Geo Pattern button not found')
|
|
125
|
+
}
|
|
126
|
+
await userEvent.click(addPatternButton)
|
|
127
|
+
},
|
|
128
|
+
(before, after) => {
|
|
129
|
+
// After clicking, at minimum a new pattern definition should be created.
|
|
130
|
+
// Pattern paths may remain unchanged until dataKey/dataValue are configured.
|
|
131
|
+
return after.patternDefCount > before.patternDefCount
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
// ==========================================================================
|
|
136
|
+
// TEST: Change pattern color to #111
|
|
137
|
+
// ==========================================================================
|
|
138
|
+
// First, open the pattern accordion
|
|
139
|
+
let patternAccordionButton: HTMLElement | null = null
|
|
140
|
+
await performAndAssert(
|
|
141
|
+
'Pattern Settings → Open pattern accordion',
|
|
142
|
+
() => {
|
|
143
|
+
const allButtons = Array.from(canvasElement.querySelectorAll('.accordion__button'))
|
|
144
|
+
const button = allButtons.find(btn => btn.textContent?.includes('Select Column')) as HTMLElement
|
|
145
|
+
if (button) {
|
|
146
|
+
patternAccordionButton = button
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
isExpanded: button ? button.getAttribute('aria-expanded') === 'true' : false
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
async () => {
|
|
153
|
+
if (!patternAccordionButton) {
|
|
154
|
+
throw new Error('Pattern accordion not found')
|
|
155
|
+
}
|
|
156
|
+
await userEvent.click(patternAccordionButton)
|
|
157
|
+
},
|
|
158
|
+
(before, after) => {
|
|
159
|
+
return !before.isExpanded && after.isExpanded
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
// Now change the pattern color
|
|
164
|
+
await performAndAssert(
|
|
165
|
+
'Pattern Settings → Change pattern color to #111',
|
|
166
|
+
() => {
|
|
167
|
+
// Check pattern color in the SVG - look for circle elements inside pattern definitions
|
|
168
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
169
|
+
let patternCircleColors: string[] = []
|
|
170
|
+
|
|
171
|
+
allSvgs.forEach(svg => {
|
|
172
|
+
const patterns = svg.querySelectorAll('pattern')
|
|
173
|
+
patterns.forEach(pattern => {
|
|
174
|
+
const circles = pattern.querySelectorAll('circle')
|
|
175
|
+
circles.forEach(circle => {
|
|
176
|
+
const fill = circle.getAttribute('fill')
|
|
177
|
+
if (fill) patternCircleColors.push(fill)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
patternColors: patternCircleColors
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
async () => {
|
|
187
|
+
// Find the pattern color input and change it to #111
|
|
188
|
+
const input = canvasElement.querySelector('input[name="patternColor"]') as HTMLInputElement
|
|
189
|
+
if (!input) {
|
|
190
|
+
throw new Error('Pattern color input not found')
|
|
191
|
+
}
|
|
192
|
+
await userEvent.clear(input)
|
|
193
|
+
// Use paste to enter the entire value at once, avoiding intermediate contrast check warnings
|
|
194
|
+
await userEvent.paste('#111')
|
|
195
|
+
// Blur to commit the change
|
|
196
|
+
await userEvent.tab()
|
|
197
|
+
},
|
|
198
|
+
(before, after) => {
|
|
199
|
+
// Pattern circles should now have fill color #111
|
|
200
|
+
return after.patternColors.some(color => color === '#111')
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
// ==========================================================================
|
|
205
|
+
// TEST: Set dataKey to STATE and dataValue to Colorado
|
|
206
|
+
// ==========================================================================
|
|
207
|
+
await performAndAssert(
|
|
208
|
+
'Pattern Settings → Set dataKey/dataValue to STATE/Colorado',
|
|
209
|
+
() => {
|
|
210
|
+
// Count pattern paths (paths with fill="url(#...)")
|
|
211
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
212
|
+
let patternPathCount = 0
|
|
213
|
+
|
|
214
|
+
allSvgs.forEach(svg => {
|
|
215
|
+
const allPaths = Array.from(svg.querySelectorAll('path'))
|
|
216
|
+
const patternPaths = allPaths.filter(path => {
|
|
217
|
+
const fill = path.getAttribute('fill')
|
|
218
|
+
return fill && fill.startsWith('url(#')
|
|
219
|
+
})
|
|
220
|
+
patternPathCount += patternPaths.length
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
patternPathCount
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
async () => {
|
|
228
|
+
// Select "STATE" from the dataKey dropdown
|
|
229
|
+
const dataKeySelect = canvasElement.querySelector('select[name^="pattern-dataKey--"]') as HTMLSelectElement
|
|
230
|
+
if (!dataKeySelect) {
|
|
231
|
+
throw new Error('DataKey select not found')
|
|
232
|
+
}
|
|
233
|
+
await userEvent.selectOptions(dataKeySelect, 'STATE')
|
|
234
|
+
|
|
235
|
+
// Type "Colorado" in the dataValue input
|
|
236
|
+
const dataValueInput = canvasElement.querySelector('input[id^="pattern-dataValue--"]') as HTMLInputElement
|
|
237
|
+
if (!dataValueInput) {
|
|
238
|
+
throw new Error('DataValue input not found')
|
|
239
|
+
}
|
|
240
|
+
await userEvent.clear(dataValueInput)
|
|
241
|
+
await userEvent.type(dataValueInput, 'Colorado')
|
|
242
|
+
// Tab to commit the change
|
|
243
|
+
await userEvent.tab()
|
|
244
|
+
},
|
|
245
|
+
(before, after) => {
|
|
246
|
+
// After setting STATE/Colorado, at least one matching patterned path should appear.
|
|
247
|
+
return after.patternPathCount > before.patternPathCount && after.patternPathCount > 0
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
// ==========================================================================
|
|
252
|
+
// TEST: Numeric dataValue matching and hover persistence
|
|
253
|
+
// ==========================================================================
|
|
254
|
+
await performAndAssert(
|
|
255
|
+
'Pattern Settings → Set numeric dataKey/dataValue (Rate/55)',
|
|
256
|
+
() => {
|
|
257
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
258
|
+
let patternPathCount = 0
|
|
259
|
+
|
|
260
|
+
allSvgs.forEach(svg => {
|
|
261
|
+
const allPaths = Array.from(svg.querySelectorAll('path'))
|
|
262
|
+
patternPathCount += allPaths.filter(path => path.getAttribute('fill')?.startsWith('url(#')).length
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
return { patternPathCount }
|
|
266
|
+
},
|
|
267
|
+
async () => {
|
|
268
|
+
const dataKeySelect = canvasElement.querySelector('select[name^="pattern-dataKey--"]') as HTMLSelectElement
|
|
269
|
+
const dataValueInput = canvasElement.querySelector('input[id^="pattern-dataValue--"]') as HTMLInputElement
|
|
270
|
+
if (!dataKeySelect || !dataValueInput) throw new Error('Pattern data controls not found')
|
|
271
|
+
|
|
272
|
+
await userEvent.selectOptions(dataKeySelect, 'Rate')
|
|
273
|
+
await userEvent.clear(dataValueInput)
|
|
274
|
+
await userEvent.type(dataValueInput, '55')
|
|
275
|
+
await userEvent.tab()
|
|
276
|
+
},
|
|
277
|
+
(before, after) => after.patternPathCount > 0
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
await performAndAssert(
|
|
281
|
+
'Pattern Settings → Broad match with blank dataKey (value 55)',
|
|
282
|
+
() => {
|
|
283
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
284
|
+
let patternPathCount = 0
|
|
285
|
+
|
|
286
|
+
allSvgs.forEach(svg => {
|
|
287
|
+
const allPaths = Array.from(svg.querySelectorAll('path'))
|
|
288
|
+
patternPathCount += allPaths.filter(path => path.getAttribute('fill')?.startsWith('url(#')).length
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
return { patternPathCount }
|
|
292
|
+
},
|
|
293
|
+
async () => {
|
|
294
|
+
const dataKeySelect = canvasElement.querySelector('select[name^="pattern-dataKey--"]') as HTMLSelectElement
|
|
295
|
+
const dataValueInput = canvasElement.querySelector('input[id^="pattern-dataValue--"]') as HTMLInputElement
|
|
296
|
+
if (!dataKeySelect || !dataValueInput) throw new Error('Pattern data controls not found')
|
|
297
|
+
|
|
298
|
+
await userEvent.selectOptions(dataKeySelect, '')
|
|
299
|
+
await userEvent.clear(dataValueInput)
|
|
300
|
+
await userEvent.type(dataValueInput, '55')
|
|
301
|
+
await userEvent.tab()
|
|
302
|
+
},
|
|
303
|
+
(before, after) => after.patternPathCount > 0
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
await performAndAssert(
|
|
307
|
+
'Pattern Settings → Specific match beats broad match',
|
|
308
|
+
() => {
|
|
309
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
310
|
+
const patternTypeById: Record<string, 'lines' | 'circles' | 'waves' | 'unknown'> = {}
|
|
311
|
+
const appliedRatePatternTypes = new Set<string>()
|
|
312
|
+
|
|
313
|
+
allSvgs.forEach(svg => {
|
|
314
|
+
const patterns = svg.querySelectorAll('pattern')
|
|
315
|
+
patterns.forEach(pattern => {
|
|
316
|
+
const patternId = pattern.getAttribute('id')
|
|
317
|
+
if (!patternId) return
|
|
318
|
+
|
|
319
|
+
if (pattern.querySelector('circle')) patternTypeById[patternId] = 'circles'
|
|
320
|
+
else if (pattern.querySelector('line')) patternTypeById[patternId] = 'lines'
|
|
321
|
+
else if (pattern.querySelector('path')) patternTypeById[patternId] = 'waves'
|
|
322
|
+
else patternTypeById[patternId] = 'unknown'
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
const ratePatternNodes = Array.from(svg.querySelectorAll('path.pattern-geoKey--Rate')).filter(node =>
|
|
326
|
+
node.getAttribute('fill')?.startsWith('url(#')
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
ratePatternNodes.forEach(node => {
|
|
330
|
+
const fill = node.getAttribute('fill')
|
|
331
|
+
const match = fill?.match(/^url\(#(.+)\)$/)
|
|
332
|
+
const patternId = match?.[1]
|
|
333
|
+
if (!patternId) return
|
|
334
|
+
appliedRatePatternTypes.add(patternTypeById[patternId] || 'unknown')
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
hasRateCircle: appliedRatePatternTypes.has('circles'),
|
|
340
|
+
hasRateWave: appliedRatePatternTypes.has('waves')
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
async () => {
|
|
344
|
+
const firstDataKey = canvasElement.querySelector('select[name="pattern-dataKey--0"]') as HTMLSelectElement
|
|
345
|
+
const firstDataValue = canvasElement.querySelector('input[id="pattern-dataValue--0"]') as HTMLInputElement
|
|
346
|
+
const firstPatternType = canvasElement.querySelector('select[name="pattern-type--0"]') as HTMLSelectElement
|
|
347
|
+
if (!firstDataKey || !firstDataValue || !firstPatternType) {
|
|
348
|
+
throw new Error('First pattern controls not found')
|
|
349
|
+
}
|
|
350
|
+
await userEvent.selectOptions(firstDataKey, 'Rate')
|
|
351
|
+
await userEvent.clear(firstDataValue)
|
|
352
|
+
await userEvent.type(firstDataValue, '55')
|
|
353
|
+
await userEvent.selectOptions(firstPatternType, 'circles')
|
|
354
|
+
|
|
355
|
+
const buttons = Array.from(canvasElement.querySelectorAll('button'))
|
|
356
|
+
const addPatternButton = buttons.find(btn => btn.textContent?.includes('Add Geo Pattern'))
|
|
357
|
+
if (!addPatternButton) throw new Error('Add Geo Pattern button not found')
|
|
358
|
+
await userEvent.click(addPatternButton)
|
|
359
|
+
|
|
360
|
+
const accordionButtons = Array.from(canvasElement.querySelectorAll('.accordion__button'))
|
|
361
|
+
const selectColumnButtons = accordionButtons.filter(btn => btn.textContent?.includes('Select Column'))
|
|
362
|
+
const secondPatternAccordionButton = selectColumnButtons[selectColumnButtons.length - 1] as HTMLElement
|
|
363
|
+
if (!secondPatternAccordionButton) throw new Error('Second pattern accordion not found')
|
|
364
|
+
await userEvent.click(secondPatternAccordionButton)
|
|
365
|
+
|
|
366
|
+
const secondDataKey = canvasElement.querySelector('select[name="pattern-dataKey--1"]') as HTMLSelectElement
|
|
367
|
+
const secondDataValue = canvasElement.querySelector('input[id="pattern-dataValue--1"]') as HTMLInputElement
|
|
368
|
+
const secondPatternType = canvasElement.querySelector('select[name="pattern-type--1"]') as HTMLSelectElement
|
|
369
|
+
|
|
370
|
+
if (!secondDataKey || !secondDataValue || !secondPatternType) {
|
|
371
|
+
throw new Error('Second pattern controls not found')
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
await userEvent.selectOptions(secondDataKey, '')
|
|
375
|
+
await userEvent.clear(secondDataValue)
|
|
376
|
+
await userEvent.type(secondDataValue, '55')
|
|
377
|
+
await userEvent.selectOptions(secondPatternType, 'waves')
|
|
378
|
+
},
|
|
379
|
+
(before, after) => after.hasRateCircle && !after.hasRateWave
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
await performAndAssert(
|
|
383
|
+
'Pattern Settings → Pattern remains after hover',
|
|
384
|
+
() => {
|
|
385
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
386
|
+
let patternPathCount = 0
|
|
387
|
+
|
|
388
|
+
allSvgs.forEach(svg => {
|
|
389
|
+
const allPaths = Array.from(svg.querySelectorAll('path'))
|
|
390
|
+
patternPathCount += allPaths.filter(path => path.getAttribute('fill')?.startsWith('url(#')).length
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
return { patternPathCount }
|
|
394
|
+
},
|
|
395
|
+
async () => {
|
|
396
|
+
const geoGroup = canvasElement.querySelector('.geo-group') as HTMLElement
|
|
397
|
+
if (!geoGroup) throw new Error('Geo group not found for hover test')
|
|
398
|
+
await userEvent.hover(geoGroup)
|
|
399
|
+
},
|
|
400
|
+
(before, after) => after.patternPathCount === before.patternPathCount && after.patternPathCount > 0
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
// ==========================================================================
|
|
404
|
+
// TEST: Set label to "Colorado Pattern"
|
|
405
|
+
// ==========================================================================
|
|
406
|
+
await performAndAssert(
|
|
407
|
+
'Pattern Settings → Set label to "Colorado Pattern"',
|
|
408
|
+
() => {
|
|
409
|
+
// Check if the label appears in the legend
|
|
410
|
+
const legendText = canvasElement.textContent || ''
|
|
411
|
+
return {
|
|
412
|
+
hasLabelInLegend: legendText.includes('Colorado Pattern')
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
async () => {
|
|
416
|
+
// Find the label input - it's in a label element containing "Label (optional)"
|
|
417
|
+
const labels = Array.from(canvasElement.querySelectorAll('label'))
|
|
418
|
+
const labelElement = labels.find(l => l.textContent?.includes('Label (optional)'))
|
|
419
|
+
const labelInput = labelElement?.querySelector('input[type="text"]') as HTMLInputElement
|
|
420
|
+
|
|
421
|
+
if (!labelInput) {
|
|
422
|
+
throw new Error('Label input not found')
|
|
423
|
+
}
|
|
424
|
+
await userEvent.clear(labelInput)
|
|
425
|
+
await userEvent.type(labelInput, 'Colorado Pattern')
|
|
426
|
+
await userEvent.tab()
|
|
427
|
+
},
|
|
428
|
+
(before, after) => {
|
|
429
|
+
// After setting the label, it should appear in the legend
|
|
430
|
+
return !before.hasLabelInLegend && after.hasLabelInLegend
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
// ==========================================================================
|
|
435
|
+
// TEST: Change pattern type to "lines"
|
|
436
|
+
// ==========================================================================
|
|
437
|
+
await performAndAssert(
|
|
438
|
+
'Pattern Settings → Change pattern type to lines',
|
|
439
|
+
() => {
|
|
440
|
+
// Check what type of pattern elements exist - lines patterns have <line> elements
|
|
441
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
442
|
+
let hasCirclePattern = false
|
|
443
|
+
let hasLinePattern = false
|
|
444
|
+
|
|
445
|
+
allSvgs.forEach(svg => {
|
|
446
|
+
const patterns = svg.querySelectorAll('pattern')
|
|
447
|
+
patterns.forEach(pattern => {
|
|
448
|
+
if (pattern.querySelector('circle')) hasCirclePattern = true
|
|
449
|
+
if (pattern.querySelector('line')) hasLinePattern = true
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
hasCirclePattern,
|
|
455
|
+
hasLinePattern
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
async () => {
|
|
459
|
+
// Find the pattern type dropdown and select "lines"
|
|
460
|
+
const patternTypeSelect = canvasElement.querySelector('select[name^="pattern-type--"]') as HTMLSelectElement
|
|
461
|
+
if (!patternTypeSelect) {
|
|
462
|
+
throw new Error('Pattern type select not found')
|
|
463
|
+
}
|
|
464
|
+
await userEvent.selectOptions(patternTypeSelect, 'lines')
|
|
465
|
+
},
|
|
466
|
+
(before, after) => {
|
|
467
|
+
// Before: has circle pattern, no line pattern
|
|
468
|
+
// After: has line pattern (may or may not still have circle pattern from other elements)
|
|
469
|
+
return before.hasCirclePattern && !before.hasLinePattern && after.hasLinePattern
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
// ==========================================================================
|
|
474
|
+
// TEST: Change pattern size to "large"
|
|
475
|
+
// ==========================================================================
|
|
476
|
+
await performAndAssert(
|
|
477
|
+
'Pattern Settings → Change pattern size to large',
|
|
478
|
+
() => {
|
|
479
|
+
// Check the pattern element's width/height attributes
|
|
480
|
+
const allSvgs = canvasElement.querySelectorAll('svg')
|
|
481
|
+
let patternSizes: number[] = []
|
|
482
|
+
|
|
483
|
+
allSvgs.forEach(svg => {
|
|
484
|
+
const patterns = svg.querySelectorAll('pattern')
|
|
485
|
+
patterns.forEach(pattern => {
|
|
486
|
+
const width = pattern.getAttribute('width')
|
|
487
|
+
if (width) patternSizes.push(parseFloat(width))
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
patternSizes
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
async () => {
|
|
496
|
+
// Find the pattern size dropdown and select "large"
|
|
497
|
+
const patternSizeSelect = canvasElement.querySelector('select[name^="pattern-size--"]') as HTMLSelectElement
|
|
498
|
+
if (!patternSizeSelect) {
|
|
499
|
+
throw new Error('Pattern size select not found')
|
|
500
|
+
}
|
|
501
|
+
await userEvent.selectOptions(patternSizeSelect, 'large')
|
|
502
|
+
},
|
|
503
|
+
(before, after) => {
|
|
504
|
+
// After changing to large, pattern sizes should increase
|
|
505
|
+
const beforeMax = Math.max(...before.patternSizes)
|
|
506
|
+
const afterMax = Math.max(...after.patternSizes)
|
|
507
|
+
return afterMax > beforeMax
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Text Annotations Section Tests
|
|
515
|
+
* Tests controls in the Text Annotations accordion
|
|
516
|
+
*/
|
|
@@ -0,0 +1,165 @@
|
|
|
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 SmallMultiplesSectionTests: Story = {
|
|
27
|
+
name: 'Small Multiples Section Tests',
|
|
28
|
+
parameters: {},
|
|
29
|
+
args: {
|
|
30
|
+
config: {
|
|
31
|
+
...wastewaterMapSmallMultiplesConfig,
|
|
32
|
+
general: {
|
|
33
|
+
...wastewaterMapSmallMultiplesConfig.general,
|
|
34
|
+
title: 'Map Small Multiples Test'
|
|
35
|
+
},
|
|
36
|
+
smallMultiples: {
|
|
37
|
+
...wastewaterMapSmallMultiplesConfig.smallMultiples,
|
|
38
|
+
mode: ''
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
isEditor: true
|
|
42
|
+
},
|
|
43
|
+
play: async ({ canvasElement }) => {
|
|
44
|
+
const canvas = within(canvasElement)
|
|
45
|
+
|
|
46
|
+
await waitForEditor(canvas)
|
|
47
|
+
await waitForPresence('.map-container', canvasElement)
|
|
48
|
+
|
|
49
|
+
await openAccordion(canvas, 'Small Multiples')
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// TEST: Enable Small Multiples Mode
|
|
53
|
+
// Verifies: Map visualization changes from single map to multiple maps
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
const getMapTileCount = () => {
|
|
57
|
+
const smallMultiplesContainer = canvasElement.querySelector('.small-multiples-container')
|
|
58
|
+
const tiles = canvasElement.querySelectorAll('.small-multiples-grid > .small-multiple-tile')
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
hasSmallMultiplesContainer: !!smallMultiplesContainer,
|
|
62
|
+
tileCount: tiles.length
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const tileByColumnSelect = canvas.getByLabelText(/tile by column/i) as HTMLSelectElement
|
|
67
|
+
|
|
68
|
+
await performAndAssert(
|
|
69
|
+
'Enable Small Multiples Mode - Map splits into multiple tiles',
|
|
70
|
+
getMapTileCount,
|
|
71
|
+
async () => {
|
|
72
|
+
await userEvent.selectOptions(tileByColumnSelect, 'pathogen')
|
|
73
|
+
},
|
|
74
|
+
(before, after) => {
|
|
75
|
+
return before.tileCount === 0 && after.tileCount === 3
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// TEST: Tiles Per Row Desktop
|
|
81
|
+
// Verifies: Grid layout changes from 3 tiles per row to 2 tiles per row
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
const getTilesInFirstRow = () => {
|
|
85
|
+
const tiles = Array.from(canvasElement.querySelectorAll('.small-multiples-grid > .small-multiple-tile'))
|
|
86
|
+
if (tiles.length === 0) return { tilesInFirstRow: 0 }
|
|
87
|
+
|
|
88
|
+
const firstTileTop = tiles[0].getBoundingClientRect().top
|
|
89
|
+
const tilesInFirstRow = tiles.filter(tile => {
|
|
90
|
+
const tileTop = tile.getBoundingClientRect().top
|
|
91
|
+
return Math.abs(tileTop - firstTileTop) < 5
|
|
92
|
+
}).length
|
|
93
|
+
|
|
94
|
+
return { tilesInFirstRow }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const tilesPerRowInput = canvas.getByLabelText(/tiles per row \(desktop\)/i) as HTMLInputElement
|
|
98
|
+
|
|
99
|
+
await performAndAssert(
|
|
100
|
+
'Tiles Per Row Desktop - Layout changes from 3 to 2 tiles per row',
|
|
101
|
+
getTilesInFirstRow,
|
|
102
|
+
async () => {
|
|
103
|
+
await userEvent.clear(tilesPerRowInput)
|
|
104
|
+
await userEvent.type(tilesPerRowInput, '2')
|
|
105
|
+
tilesPerRowInput.blur()
|
|
106
|
+
},
|
|
107
|
+
(before, after) => {
|
|
108
|
+
return before.tilesInFirstRow === 3 && after.tilesInFirstRow === 2
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// TEST: Tile Order
|
|
114
|
+
// Verifies: Changing tile order from custom to descending reverses tiles
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
const getTileTitles = () => {
|
|
118
|
+
const tiles = canvasElement.querySelectorAll('.small-multiples-grid > .small-multiple-tile')
|
|
119
|
+
const titles = Array.from(tiles).map(tile => {
|
|
120
|
+
const titleElement = tile.querySelector('.tile-title')
|
|
121
|
+
return titleElement?.textContent?.trim() || ''
|
|
122
|
+
})
|
|
123
|
+
return { titles }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const tileOrderSelect = canvas.getByLabelText(/tile order/i) as HTMLSelectElement
|
|
127
|
+
|
|
128
|
+
await performAndAssert(
|
|
129
|
+
'Tile Order - Descending sorts tiles alphabetically descending',
|
|
130
|
+
getTileTitles,
|
|
131
|
+
async () => {
|
|
132
|
+
await userEvent.selectOptions(tileOrderSelect, 'desc')
|
|
133
|
+
},
|
|
134
|
+
(before, after) => {
|
|
135
|
+
return before.titles[0] === 'Influenza AAA' && after.titles[0] === 'RSV' && after.titles[1] === 'Influenza AAA'
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// TEST: Custom Tile Title
|
|
141
|
+
// Verifies: Custom tile title appears in visualization
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
const covid19TitleInput = canvasElement.querySelector('input[placeholder="COVID-19"]') as HTMLInputElement
|
|
145
|
+
|
|
146
|
+
await performAndAssert(
|
|
147
|
+
'Custom Tile Title - Changes tile display name',
|
|
148
|
+
getTileTitles,
|
|
149
|
+
async () => {
|
|
150
|
+
if (covid19TitleInput) {
|
|
151
|
+
await userEvent.clear(covid19TitleInput)
|
|
152
|
+
await userEvent.type(covid19TitleInput, 'Custom COVID Title')
|
|
153
|
+
covid19TitleInput.blur()
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
(before, after) => {
|
|
157
|
+
return after.titles.includes('Custom COVID Title') && !before.titles.includes('Custom COVID Title')
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ==========================================================================
|
|
164
|
+
// MULTI-COUNTRY MAP TESTS
|
|
165
|
+
// ==========================================================================
|