@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 MultiCountryWorldMapTests: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
...DEFAULT_ARGS,
|
|
29
|
+
config: {
|
|
30
|
+
...multiCountryConfig,
|
|
31
|
+
general: {
|
|
32
|
+
...multiCountryConfig.general,
|
|
33
|
+
countriesPicked: [] // Start with no countries selected for testing
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
play: async ({ canvasElement }) => {
|
|
38
|
+
const canvas = within(canvasElement)
|
|
39
|
+
|
|
40
|
+
await waitForEditor(canvas)
|
|
41
|
+
await waitForPresence('.map-container', canvasElement)
|
|
42
|
+
|
|
43
|
+
// Ensure we're testing a world map - Type accordion should be open by default
|
|
44
|
+
await openAccordion(canvas, 'Type')
|
|
45
|
+
|
|
46
|
+
// Wait for world map to load (topology loads asynchronously)
|
|
47
|
+
// The config already starts with geoType: 'world', so we just need to wait for it to render
|
|
48
|
+
await waitForPresence('path[data-country-code]', canvasElement)
|
|
49
|
+
|
|
50
|
+
// Wait for the country selector to appear (only shows for world maps)
|
|
51
|
+
await waitForPresence('.cove-multiselect', canvasElement)
|
|
52
|
+
|
|
53
|
+
// Stay in Type accordion section - this is where multi-country controls are located
|
|
54
|
+
|
|
55
|
+
// ==========================================================================
|
|
56
|
+
// Helper functions to capture visual state
|
|
57
|
+
// ==========================================================================
|
|
58
|
+
const getCountryVisualState = () => {
|
|
59
|
+
const mapContainer = canvasElement.querySelector('.map-container')
|
|
60
|
+
const allCountryPaths = canvasElement.querySelectorAll('path[data-country-code]')
|
|
61
|
+
const visibleCountries = Array.from(allCountryPaths).filter(
|
|
62
|
+
path => !path.classList.contains('hidden') && !path.classList.contains('grayed-out')
|
|
63
|
+
)
|
|
64
|
+
const grayedCountries = Array.from(allCountryPaths).filter(path => path.classList.contains('grayed-out'))
|
|
65
|
+
const hiddenCountries = Array.from(allCountryPaths).filter(path => {
|
|
66
|
+
const hasHiddenClass = path.classList.contains('hidden')
|
|
67
|
+
const computedStyle = window.getComputedStyle(path as Element)
|
|
68
|
+
const isDisplayNone = computedStyle.display === 'none'
|
|
69
|
+
return hasHiddenClass || isDisplayNone
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
totalCountries: allCountryPaths.length,
|
|
74
|
+
visibleCountries: visibleCountries.length,
|
|
75
|
+
grayedCountries: grayedCountries.length,
|
|
76
|
+
hiddenCountries: hiddenCountries.length,
|
|
77
|
+
mapClasses: mapContainer ? Array.from(mapContainer.classList) : [],
|
|
78
|
+
hasMultiCountryClass: mapContainer?.classList.contains('multi-country-selected') || false,
|
|
79
|
+
selectedCountryCodes: Array.from(
|
|
80
|
+
new Set(
|
|
81
|
+
Array.from(visibleCountries)
|
|
82
|
+
.map(path => path.getAttribute('data-country-code'))
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ==========================================================================
|
|
90
|
+
// TEST: Country Multi-Select Dropdown Interaction
|
|
91
|
+
// ==========================================================================
|
|
92
|
+
const countryMultiSelect = Array.from(canvasElement.querySelectorAll('.cove-multiselect')).find(ms => {
|
|
93
|
+
const parentLabel = ms.closest('label')
|
|
94
|
+
const labelSpan = parentLabel?.querySelector('span')
|
|
95
|
+
return labelSpan?.textContent?.includes('Countries Selector')
|
|
96
|
+
}) as HTMLElement
|
|
97
|
+
expect(countryMultiSelect).toBeTruthy()
|
|
98
|
+
|
|
99
|
+
// Get the expand button to open the dropdown
|
|
100
|
+
const expandButton = countryMultiSelect.querySelector('button[aria-label="Expand"]') as HTMLButtonElement
|
|
101
|
+
expect(expandButton).toBeTruthy()
|
|
102
|
+
|
|
103
|
+
// Test selecting first country (France)
|
|
104
|
+
await performAndAssert(
|
|
105
|
+
'Multi-Country → Select France',
|
|
106
|
+
getCountryVisualState,
|
|
107
|
+
async () => {
|
|
108
|
+
// Open the dropdown
|
|
109
|
+
await userEvent.click(expandButton)
|
|
110
|
+
// Find and click France option
|
|
111
|
+
const franceOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
112
|
+
li.textContent?.includes('France')
|
|
113
|
+
) as HTMLElement
|
|
114
|
+
expect(franceOption).toBeTruthy()
|
|
115
|
+
await userEvent.click(franceOption)
|
|
116
|
+
},
|
|
117
|
+
(before, after) => {
|
|
118
|
+
// When countries are selected, map should show multi-country state
|
|
119
|
+
return (
|
|
120
|
+
after.hasMultiCountryClass &&
|
|
121
|
+
after.selectedCountryCodes.includes('FRA') &&
|
|
122
|
+
after.visibleCountries < before.totalCountries // Some countries should be filtered
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// Test selecting second country (Germany)
|
|
128
|
+
await performAndAssert(
|
|
129
|
+
'Multi-Country → Add Germany to selection',
|
|
130
|
+
getCountryVisualState,
|
|
131
|
+
async () => {
|
|
132
|
+
// Expand dropdown again
|
|
133
|
+
const expandBtn = countryMultiSelect.querySelector('button[aria-label="Expand"]') as HTMLButtonElement
|
|
134
|
+
await userEvent.click(expandBtn)
|
|
135
|
+
// Find and click Germany option
|
|
136
|
+
const germanyOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
137
|
+
li.textContent?.includes('Germany')
|
|
138
|
+
) as HTMLElement
|
|
139
|
+
expect(germanyOption).toBeTruthy()
|
|
140
|
+
await userEvent.click(germanyOption)
|
|
141
|
+
},
|
|
142
|
+
(before, after) => {
|
|
143
|
+
// Both France and Germany should be selected
|
|
144
|
+
return (
|
|
145
|
+
after.selectedCountryCodes.includes('FRA') &&
|
|
146
|
+
after.selectedCountryCodes.includes('DEU') &&
|
|
147
|
+
after.selectedCountryCodes.length === 2
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
// Test adding a third country (Italy)
|
|
153
|
+
await performAndAssert(
|
|
154
|
+
'Multi-Country → Add Italy to selection',
|
|
155
|
+
getCountryVisualState,
|
|
156
|
+
async () => {
|
|
157
|
+
// Expand dropdown again
|
|
158
|
+
const expandBtn = countryMultiSelect.querySelector('button[aria-label="Expand"]') as HTMLButtonElement
|
|
159
|
+
await userEvent.click(expandBtn)
|
|
160
|
+
// Find and click Italy option
|
|
161
|
+
const italyOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
162
|
+
li.textContent?.includes('Italy')
|
|
163
|
+
) as HTMLElement
|
|
164
|
+
expect(italyOption).toBeTruthy()
|
|
165
|
+
await userEvent.click(italyOption)
|
|
166
|
+
},
|
|
167
|
+
(before, after) => {
|
|
168
|
+
// All three countries should be selected
|
|
169
|
+
return (
|
|
170
|
+
after.selectedCountryCodes.includes('FRA') &&
|
|
171
|
+
after.selectedCountryCodes.includes('DEU') &&
|
|
172
|
+
after.selectedCountryCodes.includes('ITA') &&
|
|
173
|
+
after.selectedCountryCodes.length === 3
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
// ==========================================================================
|
|
179
|
+
// TEST: Hide Unselected Countries Toggle (Grey-out vs Hide behavior)
|
|
180
|
+
// ==========================================================================
|
|
181
|
+
// Ensure map has fully rendered with country paths before testing toggle
|
|
182
|
+
await waitForPresence('path[data-country-code]', canvasElement)
|
|
183
|
+
|
|
184
|
+
// InputToggle renders as a div with a hidden checkbox
|
|
185
|
+
const hideUnselectedToggle = Array.from(
|
|
186
|
+
canvasElement.querySelectorAll('input[name*="hideUnselectedCountries"]')
|
|
187
|
+
).find(input => {
|
|
188
|
+
const label = input.closest('label') || input.parentElement?.querySelector('label')
|
|
189
|
+
return label?.textContent?.includes('Hide Unselected Countries')
|
|
190
|
+
}) as HTMLInputElement
|
|
191
|
+
expect(hideUnselectedToggle).toBeTruthy()
|
|
192
|
+
|
|
193
|
+
// By default (unchecked), unselected countries should be grayed out
|
|
194
|
+
// First, ensure the toggle is unchecked (hideUnselectedCountries = false, so countries are grayed)
|
|
195
|
+
if (hideUnselectedToggle.checked) {
|
|
196
|
+
await userEvent.click(hideUnselectedToggle)
|
|
197
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const beforeState = getCountryVisualState()
|
|
201
|
+
|
|
202
|
+
// Now check it (should hide unselected countries)
|
|
203
|
+
await userEvent.click(hideUnselectedToggle)
|
|
204
|
+
await new Promise(resolve => setTimeout(resolve, 100)) // Give time for re-render
|
|
205
|
+
|
|
206
|
+
const afterState = getCountryVisualState()
|
|
207
|
+
|
|
208
|
+
const testResult =
|
|
209
|
+
afterState.hiddenCountries > beforeState.hiddenCountries &&
|
|
210
|
+
afterState.grayedCountries === 0 && // No grayed countries when hiding
|
|
211
|
+
afterState.selectedCountryCodes.length === 3 // Only our 3 selected countries (by unique ISO codes)
|
|
212
|
+
|
|
213
|
+
expect(testResult).toBe(true)
|
|
214
|
+
|
|
215
|
+
// Test unchecking "Hide Unselected Countries" again (should grey-out unselected countries)
|
|
216
|
+
await performAndAssert(
|
|
217
|
+
'Hide Unselected Countries → Grey-out unselected countries',
|
|
218
|
+
getCountryVisualState,
|
|
219
|
+
async () => {
|
|
220
|
+
await userEvent.click(hideUnselectedToggle)
|
|
221
|
+
},
|
|
222
|
+
(before, after) => {
|
|
223
|
+
// When not hiding (default), unselected countries should be grayed out
|
|
224
|
+
return (
|
|
225
|
+
after.grayedCountries > before.grayedCountries &&
|
|
226
|
+
after.hiddenCountries === 0 && // No hidden countries when showing grayed
|
|
227
|
+
after.selectedCountryCodes.length === 3 // Our 3 selected countries remain visible (by unique ISO codes)
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
// ==========================================================================
|
|
233
|
+
// TEST: Map Centering and Bounds Changes
|
|
234
|
+
// ==========================================================================
|
|
235
|
+
await performAndAssert(
|
|
236
|
+
'Multi-Country Selection → Map centers on selected countries',
|
|
237
|
+
getCountryVisualState,
|
|
238
|
+
async () => {
|
|
239
|
+
// Clear selection and select different countries (Japan, Australia) to test centering
|
|
240
|
+
// Click expand button
|
|
241
|
+
const expandBtn = countryMultiSelect.querySelector('button[aria-label="Expand"]') as HTMLButtonElement
|
|
242
|
+
await userEvent.click(expandBtn)
|
|
243
|
+
|
|
244
|
+
// Clear existing selections first by clicking remove buttons
|
|
245
|
+
const removeButtons = countryMultiSelect.querySelectorAll('button[aria-label="Remove"]')
|
|
246
|
+
for (const button of Array.from(removeButtons)) {
|
|
247
|
+
await userEvent.click(button as HTMLButtonElement)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Select Japan
|
|
251
|
+
await userEvent.click(expandBtn) // Reopen dropdown
|
|
252
|
+
const japanOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
253
|
+
li.textContent?.includes('Japan')
|
|
254
|
+
) as HTMLElement
|
|
255
|
+
expect(japanOption).toBeTruthy()
|
|
256
|
+
await userEvent.click(japanOption)
|
|
257
|
+
|
|
258
|
+
// Select Australia
|
|
259
|
+
await userEvent.click(expandBtn) // Reopen dropdown
|
|
260
|
+
const australiaOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
261
|
+
li.textContent?.includes('Australia')
|
|
262
|
+
) as HTMLElement
|
|
263
|
+
expect(australiaOption).toBeTruthy()
|
|
264
|
+
await userEvent.click(australiaOption)
|
|
265
|
+
|
|
266
|
+
// Wait for map to re-render with new selection
|
|
267
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
268
|
+
},
|
|
269
|
+
(before, after) => {
|
|
270
|
+
// Verify that the selected countries changed (Japan and Australia instead of France, Germany, Italy)
|
|
271
|
+
// The centering happens automatically via useCountryZoom hook
|
|
272
|
+
return (
|
|
273
|
+
after.selectedCountryCodes.includes('JPN') &&
|
|
274
|
+
after.selectedCountryCodes.includes('AUS') &&
|
|
275
|
+
!after.selectedCountryCodes.includes('FRA') &&
|
|
276
|
+
after.selectedCountryCodes.length === 2
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
// ==========================================================================
|
|
282
|
+
// TEST: Clear Country Selection (Reset to full world map)
|
|
283
|
+
// ==========================================================================
|
|
284
|
+
await performAndAssert(
|
|
285
|
+
'Multi-Country → Clear all selections',
|
|
286
|
+
getCountryVisualState,
|
|
287
|
+
async () => {
|
|
288
|
+
// Remove all selected countries using the remove buttons
|
|
289
|
+
const removeButtons = countryMultiSelect.querySelectorAll('button[aria-label="Remove"]')
|
|
290
|
+
for (const button of Array.from(removeButtons)) {
|
|
291
|
+
await userEvent.click(button as HTMLButtonElement)
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
(before, after) => {
|
|
295
|
+
// When all countries are cleared, should return to normal world map state
|
|
296
|
+
// Note: selectedCountryCodes will contain ALL countries since all are visible (not grayed/hidden)
|
|
297
|
+
return (
|
|
298
|
+
!after.hasMultiCountryClass &&
|
|
299
|
+
after.grayedCountries === 0 &&
|
|
300
|
+
after.hiddenCountries === 0 &&
|
|
301
|
+
after.visibleCountries === after.totalCountries // All countries visible
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
// ==========================================================================
|
|
307
|
+
// TEST: Country Selection with Data Integration
|
|
308
|
+
// ==========================================================================
|
|
309
|
+
await performAndAssert(
|
|
310
|
+
'Multi-Country Data → Selected countries show data values',
|
|
311
|
+
getCountryVisualState,
|
|
312
|
+
async () => {
|
|
313
|
+
// Select countries that should have data (France, Germany, Italy - these have data in the config)
|
|
314
|
+
const expandBtn = countryMultiSelect.querySelector('button[aria-label="Expand"]') as HTMLButtonElement
|
|
315
|
+
|
|
316
|
+
// Select France
|
|
317
|
+
await userEvent.click(expandBtn)
|
|
318
|
+
const franceOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
319
|
+
li.textContent?.includes('France')
|
|
320
|
+
) as HTMLElement
|
|
321
|
+
|
|
322
|
+
expect(franceOption).toBeTruthy()
|
|
323
|
+
await userEvent.click(franceOption)
|
|
324
|
+
|
|
325
|
+
// Select Germany
|
|
326
|
+
await userEvent.click(expandBtn)
|
|
327
|
+
const germanyOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
328
|
+
li.textContent?.includes('Germany')
|
|
329
|
+
) as HTMLElement
|
|
330
|
+
|
|
331
|
+
expect(germanyOption).toBeTruthy()
|
|
332
|
+
await userEvent.click(germanyOption)
|
|
333
|
+
|
|
334
|
+
// Select Italy
|
|
335
|
+
await userEvent.click(expandBtn)
|
|
336
|
+
const italyOption = Array.from(countryMultiSelect.querySelectorAll('li[role="option"]')).find(li =>
|
|
337
|
+
li.textContent?.includes('Italy')
|
|
338
|
+
) as HTMLElement
|
|
339
|
+
|
|
340
|
+
expect(italyOption).toBeTruthy()
|
|
341
|
+
await userEvent.click(italyOption)
|
|
342
|
+
|
|
343
|
+
// Wait for data to be applied
|
|
344
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
345
|
+
},
|
|
346
|
+
(before, after) => {
|
|
347
|
+
// Verify that the countries with data are correctly selected
|
|
348
|
+
// (Data rendering and legend display are separate from multi-country selection functionality)
|
|
349
|
+
return (
|
|
350
|
+
after.selectedCountryCodes.includes('FRA') &&
|
|
351
|
+
after.selectedCountryCodes.includes('DEU') &&
|
|
352
|
+
after.selectedCountryCodes.includes('ITA') &&
|
|
353
|
+
after.hasMultiCountryClass &&
|
|
354
|
+
after.selectedCountryCodes.length === 3
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
}
|