@cdc/map 4.26.3 → 4.26.5
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 +268 -0
- package/README.md +74 -24
- package/dist/cdcmap-CY9IcPSi.es.js +6 -0
- package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
- package/dist/cdcmap.js +29168 -27482
- 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 +6 -3
- package/examples/minimal-example.json +73 -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 +107 -14
- package/src/_stories/CdcMap.AltText.stories.tsx +122 -0
- package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +600 -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.FocusVisibility.stories.tsx +87 -0
- package/src/_stories/CdcMap.HiddenMount.stories.tsx +69 -0
- package/src/_stories/CdcMap.ResetBehavior.stories.tsx +32 -0
- package/src/_stories/CdcMap.Zoom.stories.tsx +111 -0
- package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +60 -0
- package/src/_stories/_mock/alt_text_metadata.json +65 -0
- package/src/_stories/_mock/legends/legend-tests.json +3 -3
- package/src/_stories/_mock/world-bubble-reset.json +138 -0
- package/src/_stories/_mock/world-data-zoom-filters.json +166 -0
- package/src/components/Annotation/AnnotationList.tsx +1 -1
- package/src/components/BubbleList.tsx +13 -0
- package/src/components/EditorPanel/components/EditorPanel.tsx +637 -382
- 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/FilterControls.tsx +21 -0
- package/src/components/Legend/components/Legend.tsx +3 -3
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
- package/src/components/SmallMultiples/SmallMultiples.tsx +2 -2
- package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
- package/src/components/UsaMap/components/UsaMap.County.tsx +309 -108
- package/src/components/UsaMap/components/UsaMap.Region.tsx +5 -2
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +33 -10
- package/src/components/UsaMap/components/UsaMap.State.tsx +10 -3
- 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 +37 -4
- package/src/components/WorldMap/data/world-topo.json +1 -1
- package/src/components/ZoomableGroup.tsx +23 -3
- package/src/components/filterControls.styles.css +6 -0
- package/src/data/initial-state.js +3 -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/generateRuntimeFilters.ts +2 -1
- package/src/helpers/handleMapAriaLabels.ts +45 -30
- package/src/helpers/shouldAutoResetSingleStateZoom.ts +22 -0
- package/src/helpers/tests/countyTerritories.test.ts +87 -0
- package/src/helpers/tests/handleMapAriaLabels.test.ts +71 -0
- package/src/helpers/tests/shouldAutoResetSingleStateZoom.test.ts +71 -0
- package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
- package/src/hooks/useGeoClickHandler.ts +13 -1
- package/src/hooks/useMapLayers.tsx +1 -1
- package/src/hooks/useStateZoom.tsx +39 -20
- package/src/hooks/useTooltip.test.tsx +2 -16
- package/src/hooks/useTooltip.ts +18 -7
- package/src/index.jsx +5 -2
- package/src/scss/main.scss +6 -21
- package/src/scss/map.scss +20 -0
- 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 +11 -0
- package/src/types/MapContext.ts +6 -1
- package/topojson-updater/README.txt +1 -1
- 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,359 @@
|
|
|
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 VisualSectionTests: StoryObj<typeof CdcMap> = {
|
|
27
|
+
name: 'Visual Section Tests',
|
|
28
|
+
args: {
|
|
29
|
+
...DEFAULT_ARGS
|
|
30
|
+
},
|
|
31
|
+
play: async ({ canvasElement }) => {
|
|
32
|
+
const canvas = within(canvasElement)
|
|
33
|
+
|
|
34
|
+
// Wait for the editor to load
|
|
35
|
+
await waitForEditor(canvas)
|
|
36
|
+
|
|
37
|
+
// Open the Visual accordion
|
|
38
|
+
await openAccordion(canvas, 'Visual')
|
|
39
|
+
await waitForPresence('path.single-geo', canvasElement)
|
|
40
|
+
|
|
41
|
+
// ==========================================================================
|
|
42
|
+
// TEST: Header Theme
|
|
43
|
+
// ==========================================================================
|
|
44
|
+
await performAndAssert(
|
|
45
|
+
'Header Theme → Select purple theme',
|
|
46
|
+
() => {
|
|
47
|
+
// Check via the HeaderThemeSelector's selected button state
|
|
48
|
+
const colorPalettes = canvasElement.querySelectorAll('.color-palette')
|
|
49
|
+
const themeColorPalette = Array.from(colorPalettes).find(palette =>
|
|
50
|
+
Array.from(palette.querySelectorAll('button')).some(btn => btn.classList.contains('theme-purple'))
|
|
51
|
+
)
|
|
52
|
+
const selectedThemeBtn = themeColorPalette?.querySelector('button.selected') as HTMLElement | null
|
|
53
|
+
return {
|
|
54
|
+
currentTheme: selectedThemeBtn
|
|
55
|
+
? Array.from(selectedThemeBtn.classList).find(cls => cls.startsWith('theme-')) || ''
|
|
56
|
+
: ''
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
async () => {
|
|
60
|
+
// Find the header theme selector and click purple
|
|
61
|
+
const colorPalettes = canvasElement.querySelectorAll('.color-palette')
|
|
62
|
+
const themeColorPalette = Array.from(colorPalettes).find(palette =>
|
|
63
|
+
Array.from(palette.querySelectorAll('button')).some(btn => btn.classList.contains('theme-purple'))
|
|
64
|
+
)
|
|
65
|
+
const purpleTheme = themeColorPalette?.querySelector('button.theme-purple') as HTMLElement
|
|
66
|
+
await userEvent.click(purpleTheme)
|
|
67
|
+
},
|
|
68
|
+
(before, after) => {
|
|
69
|
+
// After clicking, the purple theme button should be selected
|
|
70
|
+
return after.currentTheme === 'theme-purple'
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
// ==========================================================================
|
|
75
|
+
// TEST: Show Title
|
|
76
|
+
// ==========================================================================
|
|
77
|
+
const showTitleCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(checkbox => {
|
|
78
|
+
const label = checkbox.closest('label')
|
|
79
|
+
return label?.textContent?.includes('Show Title')
|
|
80
|
+
}) as HTMLInputElement
|
|
81
|
+
|
|
82
|
+
await performAndAssert(
|
|
83
|
+
'Show Title → Toggle off',
|
|
84
|
+
() => {
|
|
85
|
+
const titleElement = canvasElement.querySelector('.cove-title, header.cove-visualization__header')
|
|
86
|
+
return {
|
|
87
|
+
isPresent: Boolean(titleElement)
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async () => {
|
|
91
|
+
await userEvent.click(showTitleCheckbox)
|
|
92
|
+
},
|
|
93
|
+
(before, after) => {
|
|
94
|
+
// After toggling off, title should be removed from DOM
|
|
95
|
+
return before.isPresent && !after.isPresent
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
await performAndAssert(
|
|
100
|
+
'Show Title → Toggle back on',
|
|
101
|
+
() => {
|
|
102
|
+
const titleElement = canvasElement.querySelector('.cove-title, header.cove-visualization__header')
|
|
103
|
+
return {
|
|
104
|
+
isPresent: Boolean(titleElement)
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async () => {
|
|
108
|
+
await userEvent.click(showTitleCheckbox)
|
|
109
|
+
},
|
|
110
|
+
(before, after) => {
|
|
111
|
+
// After toggling back on, title should be present in DOM
|
|
112
|
+
return !before.isPresent && after.isPresent
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// ==========================================================================
|
|
117
|
+
// TEST: Geo Border Color
|
|
118
|
+
// ==========================================================================
|
|
119
|
+
const geoBorderColorSelect = Array.from(canvasElement.querySelectorAll('select')).find(select => {
|
|
120
|
+
const label = select.closest('label')
|
|
121
|
+
return label?.textContent?.includes('Geo Border Color')
|
|
122
|
+
}) as HTMLSelectElement
|
|
123
|
+
|
|
124
|
+
await performAndAssert(
|
|
125
|
+
'Geo Border Color → Change to White',
|
|
126
|
+
() => {
|
|
127
|
+
// Check the stroke attribute on SVG paths with class 'single-geo'
|
|
128
|
+
const geoPaths = canvasElement.querySelectorAll('path.single-geo')
|
|
129
|
+
const firstPath = geoPaths[0] as SVGPathElement
|
|
130
|
+
// Get the actual stroke value - could be in attribute, style, or computed
|
|
131
|
+
const strokeAttr = firstPath?.getAttribute('stroke') || ''
|
|
132
|
+
const strokeStyle = firstPath?.style?.stroke || ''
|
|
133
|
+
const computedStroke = firstPath ? window.getComputedStyle(firstPath).stroke : ''
|
|
134
|
+
return {
|
|
135
|
+
strokeAttr,
|
|
136
|
+
strokeStyle,
|
|
137
|
+
computedStroke,
|
|
138
|
+
pathCount: geoPaths.length
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async () => {
|
|
142
|
+
await userEvent.selectOptions(geoBorderColorSelect, 'sameAsBackground')
|
|
143
|
+
},
|
|
144
|
+
(before, after) => {
|
|
145
|
+
// After changing to white, geo paths should still exist and their rendered stroke should become white.
|
|
146
|
+
const beforeStroke = before.strokeAttr || before.strokeStyle || before.computedStroke
|
|
147
|
+
const afterStroke = after.strokeAttr || after.strokeStyle || after.computedStroke
|
|
148
|
+
return (
|
|
149
|
+
before.pathCount > 0 &&
|
|
150
|
+
after.pathCount > 0 &&
|
|
151
|
+
beforeStroke !== '' &&
|
|
152
|
+
afterStroke !== '' &&
|
|
153
|
+
beforeStroke !== afterStroke &&
|
|
154
|
+
after.computedStroke === 'rgb(255, 255, 255)'
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
// ==========================================================================
|
|
160
|
+
// TEST: Map Color Palette - Sequential Palette Selection
|
|
161
|
+
// ==========================================================================
|
|
162
|
+
await performAndAssert(
|
|
163
|
+
'Map Color Palette → Select different sequential palette',
|
|
164
|
+
() => {
|
|
165
|
+
// Check the fill colors of actual map path elements (same as we use for stroke)
|
|
166
|
+
const geoPaths = canvasElement.querySelectorAll('path.single-geo')
|
|
167
|
+
const firstPath = geoPaths[0] as SVGPathElement
|
|
168
|
+
// Get the fill value from any available source
|
|
169
|
+
const fillAttr = firstPath?.getAttribute('fill') || ''
|
|
170
|
+
const fillStyle = firstPath?.style?.fill || ''
|
|
171
|
+
const computedFill = firstPath ? window.getComputedStyle(firstPath).fill : ''
|
|
172
|
+
const firstColor = fillAttr || fillStyle || computedFill
|
|
173
|
+
return {
|
|
174
|
+
firstColor,
|
|
175
|
+
pathCount: geoPaths.length
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
async () => {
|
|
179
|
+
// Find the Sequential palette section
|
|
180
|
+
const spans = Array.from(canvasElement.querySelectorAll('span'))
|
|
181
|
+
const sequentialLabel = spans.find(span => span.textContent?.trim() === 'Sequential')
|
|
182
|
+
const paletteContainer = sequentialLabel?.nextElementSibling
|
|
183
|
+
|
|
184
|
+
// Try both li and button elements for palette selection
|
|
185
|
+
const palettes = Array.from(paletteContainer?.querySelectorAll('li, button') || []) as HTMLElement[]
|
|
186
|
+
|
|
187
|
+
// Select a different palette (skip the first one as it might be selected)
|
|
188
|
+
const alternativePalette = palettes.find((element, idx) => idx > 0 && !element.classList.contains('selected'))
|
|
189
|
+
if (alternativePalette) {
|
|
190
|
+
await userEvent.click(alternativePalette)
|
|
191
|
+
|
|
192
|
+
// Wait for changes to apply, then check for modal
|
|
193
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
194
|
+
|
|
195
|
+
// Check if modal appeared and handle it
|
|
196
|
+
const modal = canvasElement.querySelector('.modal-overlay')
|
|
197
|
+
if (modal) {
|
|
198
|
+
const confirmButton = Array.from(canvasElement.querySelectorAll('button')).find(btn =>
|
|
199
|
+
btn.textContent?.includes('Convert to New Palette')
|
|
200
|
+
)
|
|
201
|
+
if (confirmButton) {
|
|
202
|
+
await userEvent.click(confirmButton)
|
|
203
|
+
// Wait for modal to close and changes to apply
|
|
204
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
(before, after) => {
|
|
210
|
+
// After selecting a different palette, the map fill colors should change
|
|
211
|
+
return before.firstColor !== '' && after.firstColor !== '' && before.firstColor !== after.firstColor
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
// ==========================================================================
|
|
216
|
+
// TEST: Map Color Palette - Reverse Order
|
|
217
|
+
// ==========================================================================
|
|
218
|
+
// The reverse toggle is an InputToggle component which renders as a div slider
|
|
219
|
+
// Find it via the hidden checkbox with name containing "isReversed"
|
|
220
|
+
const reversePaletteCheckbox = canvasElement.querySelector('input[name*="isReversed"]') as HTMLInputElement
|
|
221
|
+
const reversePaletteToggle = reversePaletteCheckbox?.parentElement as HTMLElement
|
|
222
|
+
|
|
223
|
+
expect(reversePaletteToggle).toBeTruthy()
|
|
224
|
+
|
|
225
|
+
await performAndAssert(
|
|
226
|
+
'Map Color Palette → Reverse palette order',
|
|
227
|
+
() => {
|
|
228
|
+
// Check the fill colors of map paths
|
|
229
|
+
const geoPaths = canvasElement.querySelectorAll('path.single-geo')
|
|
230
|
+
const firstPath = geoPaths[0] as SVGPathElement
|
|
231
|
+
const fillAttr = firstPath?.getAttribute('fill') || ''
|
|
232
|
+
const fillStyle = firstPath?.style?.fill || ''
|
|
233
|
+
const computedFill = firstPath ? window.getComputedStyle(firstPath).fill : ''
|
|
234
|
+
const firstColor = fillAttr || fillStyle || computedFill
|
|
235
|
+
return {
|
|
236
|
+
firstColor,
|
|
237
|
+
pathCount: geoPaths.length
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
async () => {
|
|
241
|
+
await userEvent.click(reversePaletteToggle)
|
|
242
|
+
// No modal should appear - reversing doesn't trigger conversion modal
|
|
243
|
+
},
|
|
244
|
+
(before, after) => {
|
|
245
|
+
// After reversing, the first color should be different
|
|
246
|
+
return before.firstColor !== '' && after.firstColor !== '' && before.firstColor !== after.firstColor
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
// ==========================================================================
|
|
251
|
+
// TEST: Geocode Circle Size
|
|
252
|
+
// ==========================================================================
|
|
253
|
+
const geocodeCircleSizeInput = Array.from(canvasElement.querySelectorAll('input[type="number"]')).find(input => {
|
|
254
|
+
const label = input.closest('label')
|
|
255
|
+
return label?.textContent?.includes('Geocode Circle Size')
|
|
256
|
+
}) as HTMLInputElement
|
|
257
|
+
|
|
258
|
+
expect(geocodeCircleSizeInput).toBeTruthy()
|
|
259
|
+
|
|
260
|
+
await performAndAssert(
|
|
261
|
+
'Geocode Circle Size → Set to 15',
|
|
262
|
+
() => {
|
|
263
|
+
// Check the actual visualization - DC appears as a glyph on the map
|
|
264
|
+
// The glyph class element is a path or circle, look for actual circle element
|
|
265
|
+
const geoPoints = canvasElement.querySelectorAll('g.geo-point')
|
|
266
|
+
const firstGeoPoint = geoPoints[0]
|
|
267
|
+
const circle = firstGeoPoint?.querySelector('circle')
|
|
268
|
+
const r = circle?.getAttribute('r') || ''
|
|
269
|
+
// Also check path elements that might have size in their d attribute
|
|
270
|
+
const path = firstGeoPoint?.querySelector('path')
|
|
271
|
+
const d = path?.getAttribute('d') || ''
|
|
272
|
+
return {
|
|
273
|
+
r,
|
|
274
|
+
d,
|
|
275
|
+
inputValue: geocodeCircleSizeInput.value
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
async () => {
|
|
279
|
+
await userEvent.clear(geocodeCircleSizeInput)
|
|
280
|
+
await userEvent.type(geocodeCircleSizeInput, '15')
|
|
281
|
+
await userEvent.tab() // Trigger blur/change event
|
|
282
|
+
},
|
|
283
|
+
(before, after) => {
|
|
284
|
+
// Verify the input value changed to 15
|
|
285
|
+
expect(after.inputValue).toBe('15')
|
|
286
|
+
// After changing the size, either the radius or path data should change
|
|
287
|
+
return (
|
|
288
|
+
(before.r !== '' && after.r !== '' && before.r !== after.r) ||
|
|
289
|
+
(before.d !== '' && after.d !== '' && before.d !== after.d)
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
// ==========================================================================
|
|
295
|
+
// TEST: Default City Style
|
|
296
|
+
// ==========================================================================
|
|
297
|
+
const cityStyleSelect = Array.from(canvasElement.querySelectorAll('select')).find(select => {
|
|
298
|
+
const label = select.closest('label')
|
|
299
|
+
return label?.textContent?.includes('Default City Style')
|
|
300
|
+
}) as HTMLSelectElement
|
|
301
|
+
|
|
302
|
+
if (cityStyleSelect) {
|
|
303
|
+
await performAndAssert(
|
|
304
|
+
'Default City Style → Change to Triangle',
|
|
305
|
+
() => {
|
|
306
|
+
// Check the actual visualization - look for glyph shapes
|
|
307
|
+
const circles = canvasElement.querySelectorAll('.visx-glyph-circle')
|
|
308
|
+
const triangles = canvasElement.querySelectorAll('.visx-glyph-triangle')
|
|
309
|
+
return {
|
|
310
|
+
circleCount: circles.length,
|
|
311
|
+
triangleCount: triangles.length
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
async () => {
|
|
315
|
+
await userEvent.selectOptions(cityStyleSelect, 'triangle')
|
|
316
|
+
},
|
|
317
|
+
(before, after) => {
|
|
318
|
+
// After changing to triangle, should have triangles instead of circles
|
|
319
|
+
return before.triangleCount !== after.triangleCount || before.circleCount !== after.circleCount
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ==========================================================================
|
|
325
|
+
// TEST: Label (Optional)
|
|
326
|
+
// ==========================================================================
|
|
327
|
+
const cityStyleLabelInput = Array.from(canvasElement.querySelectorAll('input[type="text"]')).find(input => {
|
|
328
|
+
const label = input.closest('label')
|
|
329
|
+
return label?.textContent?.includes('Label (Optional)')
|
|
330
|
+
}) as HTMLInputElement
|
|
331
|
+
|
|
332
|
+
if (cityStyleLabelInput) {
|
|
333
|
+
await performAndAssert(
|
|
334
|
+
'Label (Optional) → Set custom label',
|
|
335
|
+
() => {
|
|
336
|
+
// Check if the label appears in the legend
|
|
337
|
+
const legend = canvasElement.querySelector('.legends')
|
|
338
|
+
const legendText = legend?.textContent || ''
|
|
339
|
+
return {
|
|
340
|
+
legendText
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
async () => {
|
|
344
|
+
await userEvent.clear(cityStyleLabelInput)
|
|
345
|
+
await userEvent.type(cityStyleLabelInput, 'City Locations')
|
|
346
|
+
},
|
|
347
|
+
(before, after) => {
|
|
348
|
+
// After setting the label, it should appear in the legend
|
|
349
|
+
return after.legendText.includes('City Locations')
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Pattern Settings Section Tests
|
|
358
|
+
* Tests controls in the Pattern Settings accordion (only visible for US maps)
|
|
359
|
+
*/
|
|
@@ -0,0 +1,88 @@
|
|
|
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 { performAndAssert, waitForEditor, waitForPresence, openAccordion } from '@cdc/core/helpers/testing'
|
|
6
|
+
|
|
7
|
+
type Story = StoryObj<typeof CdcMap>
|
|
8
|
+
|
|
9
|
+
const mapMeta: Meta<typeof CdcMap> = {
|
|
10
|
+
title: 'Components/Templates/Map/Editor Tests',
|
|
11
|
+
component: CdcMap,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'fullscreen'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default mapMeta
|
|
18
|
+
|
|
19
|
+
const DEFAULT_ARGS = {
|
|
20
|
+
isEditor: true,
|
|
21
|
+
config: usaStateGradientConfig
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ZoomControlsTest: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
...DEFAULT_ARGS
|
|
27
|
+
},
|
|
28
|
+
play: async ({ canvasElement }) => {
|
|
29
|
+
const canvas = within(canvasElement)
|
|
30
|
+
|
|
31
|
+
await waitForEditor(canvas)
|
|
32
|
+
await waitForPresence('.map-container', canvasElement)
|
|
33
|
+
await openAccordion(canvas, 'Type')
|
|
34
|
+
|
|
35
|
+
const getZoomControlsState = () => {
|
|
36
|
+
const zoomControls = canvasElement.querySelector('.zoom-controls')
|
|
37
|
+
const zoomInButton = canvasElement.querySelector('button[aria-label="Zoom In"]')
|
|
38
|
+
const zoomOutButton = canvasElement.querySelector('button[aria-label="Zoom Out"]')
|
|
39
|
+
const mapContainer = canvasElement.querySelector('.map-container')
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
hasZoomControls: Boolean(zoomControls),
|
|
43
|
+
hasZoomInButton: Boolean(zoomInButton),
|
|
44
|
+
hasZoomOutButton: Boolean(zoomOutButton),
|
|
45
|
+
mapClasses: mapContainer ? Array.from(mapContainer.classList) : []
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const worldButton = Array.from(canvasElement.querySelectorAll('.geo-buttons button')).find(button =>
|
|
50
|
+
button.textContent?.trim().toLowerCase().includes('world')
|
|
51
|
+
) as HTMLButtonElement
|
|
52
|
+
expect(worldButton).toBeTruthy()
|
|
53
|
+
|
|
54
|
+
await performAndAssert(
|
|
55
|
+
'World Data Map → Switch geo type to world',
|
|
56
|
+
getZoomControlsState,
|
|
57
|
+
async () => {
|
|
58
|
+
await userEvent.click(worldButton)
|
|
59
|
+
},
|
|
60
|
+
(before, after) => !before.mapClasses.includes('world') && after.mapClasses.includes('world')
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const mapTypeSelect = canvas.getByLabelText(/Map Type/i) as HTMLSelectElement
|
|
64
|
+
await performAndAssert(
|
|
65
|
+
'World Data Map → Switch type to data',
|
|
66
|
+
getZoomControlsState,
|
|
67
|
+
async () => {
|
|
68
|
+
await userEvent.selectOptions(mapTypeSelect, 'data')
|
|
69
|
+
},
|
|
70
|
+
(_before, after) => after.mapClasses.includes('world')
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const allowMapZoomingCheckbox = canvas.getByLabelText(/Allow Map Zooming/i) as HTMLInputElement
|
|
74
|
+
expect(allowMapZoomingCheckbox.checked)
|
|
75
|
+
|
|
76
|
+
await userEvent.click(allowMapZoomingCheckbox)
|
|
77
|
+
expect(!allowMapZoomingCheckbox.checked)
|
|
78
|
+
|
|
79
|
+
await performAndAssert(
|
|
80
|
+
'World Data Map → Enable map zooming',
|
|
81
|
+
getZoomControlsState,
|
|
82
|
+
async () => {
|
|
83
|
+
await userEvent.click(allowMapZoomingCheckbox)
|
|
84
|
+
},
|
|
85
|
+
(before, after) => after.hasZoomControls && after.hasZoomInButton && after.hasZoomOutButton
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { within, expect, userEvent } from 'storybook/test'
|
|
3
|
+
import CdcMap from '../CdcMap'
|
|
4
|
+
import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
|
|
5
|
+
import singleStateConfig from './_mock/DEV-8942.json'
|
|
6
|
+
import worldBubbleReset from './_mock/world-bubble-reset.json'
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof CdcMap> = {
|
|
9
|
+
title: 'Components/Templates/Map/Focus Visibility',
|
|
10
|
+
component: CdcMap,
|
|
11
|
+
parameters: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
'Regression coverage for map focus treatment so single-state maps suppress pointer-only outlines while keeping keyboard-visible focus.'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
|
|
23
|
+
type Story = StoryObj<typeof CdcMap>
|
|
24
|
+
|
|
25
|
+
export const SingleStateFocusVisibility: Story = {
|
|
26
|
+
args: {
|
|
27
|
+
config: singleStateConfig,
|
|
28
|
+
isEditor: false
|
|
29
|
+
},
|
|
30
|
+
play: async ({ canvasElement }) => {
|
|
31
|
+
await assertVisualizationRendered(canvasElement)
|
|
32
|
+
await waitForPresence('path.county', canvasElement)
|
|
33
|
+
|
|
34
|
+
const mapRegion = canvasElement.querySelector('.map-container[role="region"]') as HTMLElement
|
|
35
|
+
const geographyContainer = canvasElement.querySelector('.geography-container') as HTMLElement
|
|
36
|
+
const countyPath = canvasElement.querySelector('path.county') as SVGPathElement
|
|
37
|
+
|
|
38
|
+
expect(mapRegion).toBeTruthy()
|
|
39
|
+
expect(geographyContainer).toBeTruthy()
|
|
40
|
+
expect(countyPath).toBeTruthy()
|
|
41
|
+
|
|
42
|
+
await userEvent.click(countyPath)
|
|
43
|
+
expect(geographyContainer.matches(':focus-visible')).toBe(false)
|
|
44
|
+
expect(geographyContainer).not.toHaveFocus()
|
|
45
|
+
expect(mapRegion.matches(':focus-visible')).toBe(false)
|
|
46
|
+
expect(mapRegion).not.toHaveFocus()
|
|
47
|
+
|
|
48
|
+
await userEvent.tab()
|
|
49
|
+
|
|
50
|
+
const focusedElement = document.activeElement as HTMLElement | null
|
|
51
|
+
expect(focusedElement).toBeTruthy()
|
|
52
|
+
expect(canvasElement.contains(focusedElement)).toBe(true)
|
|
53
|
+
expect(focusedElement?.matches(':focus-visible')).toBe(true)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const WorldBubbleFocusVisibility: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
config: worldBubbleReset,
|
|
60
|
+
isEditor: false
|
|
61
|
+
},
|
|
62
|
+
play: async ({ canvasElement }) => {
|
|
63
|
+
await assertVisualizationRendered(canvasElement)
|
|
64
|
+
await waitForPresence('circle.bubble.country--France', canvasElement)
|
|
65
|
+
|
|
66
|
+
const mapRegion = canvasElement.querySelector('.map-container[role="region"]') as HTMLElement
|
|
67
|
+
const geographyContainer = canvasElement.querySelector('.geography-container') as HTMLElement
|
|
68
|
+
const bubble = canvasElement.querySelector('circle.bubble.country--France') as SVGCircleElement
|
|
69
|
+
|
|
70
|
+
expect(mapRegion).toBeTruthy()
|
|
71
|
+
expect(geographyContainer).toBeTruthy()
|
|
72
|
+
expect(bubble).toBeTruthy()
|
|
73
|
+
|
|
74
|
+
await userEvent.click(bubble)
|
|
75
|
+
expect(bubble.matches(':focus-visible')).toBe(false)
|
|
76
|
+
expect(bubble).not.toHaveFocus()
|
|
77
|
+
expect(geographyContainer.matches(':focus-visible')).toBe(false)
|
|
78
|
+
expect(mapRegion.matches(':focus-visible')).toBe(false)
|
|
79
|
+
|
|
80
|
+
await userEvent.tab()
|
|
81
|
+
|
|
82
|
+
const focusedElement = document.activeElement as HTMLElement | null
|
|
83
|
+
expect(focusedElement).toBeTruthy()
|
|
84
|
+
expect(canvasElement.contains(focusedElement)).toBe(true)
|
|
85
|
+
expect(focusedElement?.matches(':focus-visible')).toBe(true)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
3
|
+
import CdcMap from '../CdcMap'
|
|
4
|
+
import CountyPatterns from './_mock/county-patterns.json'
|
|
5
|
+
import { within, expect, userEvent } from 'storybook/test'
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof CdcMap> = {
|
|
8
|
+
title: 'Components/Templates/Map/Hidden Mount',
|
|
9
|
+
component: CdcMap,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'fullscreen'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default meta
|
|
16
|
+
|
|
17
|
+
type Story = StoryObj<typeof CdcMap>
|
|
18
|
+
|
|
19
|
+
const HiddenMountHarness = () => {
|
|
20
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div style={{ padding: 24 }}>
|
|
24
|
+
<div style={{ maxWidth: 960, marginBottom: 16 }}>
|
|
25
|
+
<p style={{ marginBottom: 12 }}>
|
|
26
|
+
This story mounts the county map while hidden, mimicking host-page CSS or JS that reveals it later.
|
|
27
|
+
</p>
|
|
28
|
+
<button type='button' onClick={() => setIsVisible(true)}>
|
|
29
|
+
Reveal county map
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div
|
|
34
|
+
data-testid='county-hidden-mount-wrapper'
|
|
35
|
+
style={{
|
|
36
|
+
display: isVisible ? 'block' : 'none',
|
|
37
|
+
width: 960
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<CdcMap config={CountyPatterns} />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const County_Map_Revealed_After_Hidden_Mount: Story = {
|
|
47
|
+
render: () => <HiddenMountHarness />,
|
|
48
|
+
parameters: {
|
|
49
|
+
docs: {
|
|
50
|
+
description: {
|
|
51
|
+
story:
|
|
52
|
+
'Demonstrates the original county-map hidden-mount scenario: the map is mounted while hidden and only shown after host-page interaction.'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
play: async ({ canvasElement }) => {
|
|
57
|
+
const canvas = within(canvasElement)
|
|
58
|
+
const revealButton = canvas.getByRole('button', { name: 'Reveal county map' })
|
|
59
|
+
|
|
60
|
+
await userEvent.click(revealButton)
|
|
61
|
+
|
|
62
|
+
const renderedCanvas = await canvas.findByRole('img', { hidden: true }, { timeout: 10000 })
|
|
63
|
+
expect(renderedCanvas).toBeInTheDocument()
|
|
64
|
+
|
|
65
|
+
const mapCanvas = canvasElement.querySelector('canvas') as HTMLCanvasElement | null
|
|
66
|
+
expect(mapCanvas?.width).toBeGreaterThan(0)
|
|
67
|
+
expect(mapCanvas?.height).toBeGreaterThan(0)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import CdcMap from '../CdcMap'
|
|
3
|
+
import worldDataZoomFilters from './_mock/world-data-zoom-filters.json'
|
|
4
|
+
import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof CdcMap> = {
|
|
7
|
+
title: 'Components/Templates/Map/Reset Behavior',
|
|
8
|
+
component: CdcMap,
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'Manual repro story for world data maps that combine active filters with zoom so reset behavior can be validated directly in Storybook.'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default meta
|
|
20
|
+
|
|
21
|
+
type Story = StoryObj<typeof CdcMap>
|
|
22
|
+
|
|
23
|
+
export const WorldDataZoomWithFilters: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
config: worldDataZoomFilters,
|
|
26
|
+
isEditor: true
|
|
27
|
+
},
|
|
28
|
+
play: async ({ canvasElement }) => {
|
|
29
|
+
await assertVisualizationRendered(canvasElement)
|
|
30
|
+
await waitForPresence('path[data-country-code]', canvasElement)
|
|
31
|
+
}
|
|
32
|
+
}
|