@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.
Files changed (105) hide show
  1. package/CONFIG.md +268 -0
  2. package/README.md +74 -24
  3. package/dist/cdcmap-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcmap.js +29168 -27482
  6. package/examples/{testing-layer-2.json → __data__/testing-layer-2.json} +1 -1
  7. package/examples/{testing-layer.json → __data__/testing-layer.json} +1 -1
  8. package/examples/county-hsa-toggle.json +51993 -0
  9. package/examples/custom-map-layers.json +2 -2
  10. package/examples/default-county.json +6 -3
  11. package/examples/minimal-example.json +73 -0
  12. package/examples/private/annotation-bug.json +2 -2
  13. package/examples/private/css-issue.json +314 -0
  14. package/examples/private/region-breaking.json +1639 -0
  15. package/examples/private/test1.json +27247 -0
  16. package/package.json +4 -4
  17. package/src/CdcMapComponent.tsx +107 -14
  18. package/src/_stories/CdcMap.AltText.stories.tsx +122 -0
  19. package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +600 -0
  20. package/src/_stories/CdcMap.Editor.DataTableSectionTests.stories.tsx +404 -0
  21. package/src/_stories/CdcMap.Editor.FiltersSectionTests.stories.tsx +229 -0
  22. package/src/_stories/CdcMap.Editor.GeneralSectionTests.stories.tsx +262 -0
  23. package/src/_stories/CdcMap.Editor.LegendSectionTests.stories.tsx +541 -0
  24. package/src/_stories/CdcMap.Editor.MultiCountryWorldMapTests.stories.tsx +359 -0
  25. package/src/_stories/CdcMap.Editor.PatternSettingsSectionTests.stories.tsx +516 -0
  26. package/src/_stories/CdcMap.Editor.SmallMultiplesSectionTests.stories.tsx +165 -0
  27. package/src/_stories/CdcMap.Editor.TextAnnotationsSectionTests.stories.tsx +145 -0
  28. package/src/_stories/CdcMap.Editor.TypeSectionTests.stories.tsx +312 -0
  29. package/src/_stories/CdcMap.Editor.VisualSectionTests.stories.tsx +359 -0
  30. package/src/_stories/CdcMap.Editor.ZoomControlsTests.stories.tsx +88 -0
  31. package/src/_stories/CdcMap.FocusVisibility.stories.tsx +87 -0
  32. package/src/_stories/CdcMap.HiddenMount.stories.tsx +69 -0
  33. package/src/_stories/CdcMap.ResetBehavior.stories.tsx +32 -0
  34. package/src/_stories/CdcMap.Zoom.stories.tsx +111 -0
  35. package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +60 -0
  36. package/src/_stories/_mock/alt_text_metadata.json +65 -0
  37. package/src/_stories/_mock/legends/legend-tests.json +3 -3
  38. package/src/_stories/_mock/world-bubble-reset.json +138 -0
  39. package/src/_stories/_mock/world-data-zoom-filters.json +166 -0
  40. package/src/components/Annotation/AnnotationList.tsx +1 -1
  41. package/src/components/BubbleList.tsx +13 -0
  42. package/src/components/EditorPanel/components/EditorPanel.tsx +637 -382
  43. package/src/components/EditorPanel/components/HexShapeSettings.tsx +1 -1
  44. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +112 -117
  45. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +26 -13
  46. package/src/components/EditorPanel/components/editorPanel.styles.css +22 -2
  47. package/src/components/FilterControls.tsx +21 -0
  48. package/src/components/Legend/components/Legend.tsx +3 -3
  49. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
  50. package/src/components/SmallMultiples/SmallMultiples.tsx +2 -2
  51. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
  52. package/src/components/UsaMap/components/UsaMap.County.tsx +309 -108
  53. package/src/components/UsaMap/components/UsaMap.Region.tsx +5 -2
  54. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +33 -10
  55. package/src/components/UsaMap/components/UsaMap.State.tsx +10 -3
  56. package/src/components/UsaMap/data/cb_2019_us_county_20m.json +75817 -1
  57. package/src/components/UsaMap/data/hsa_fips_mapping.json +3144 -0
  58. package/src/components/WorldMap/WorldMap.tsx +37 -4
  59. package/src/components/WorldMap/data/world-topo.json +1 -1
  60. package/src/components/ZoomableGroup.tsx +23 -3
  61. package/src/components/filterControls.styles.css +6 -0
  62. package/src/data/initial-state.js +3 -0
  63. package/src/data/supported-counties.json +1 -1
  64. package/src/helpers/countyTerritories.ts +38 -0
  65. package/src/helpers/dataTableHelpers.ts +35 -6
  66. package/src/helpers/generateRuntimeFilters.ts +2 -1
  67. package/src/helpers/handleMapAriaLabels.ts +45 -30
  68. package/src/helpers/shouldAutoResetSingleStateZoom.ts +22 -0
  69. package/src/helpers/tests/countyTerritories.test.ts +87 -0
  70. package/src/helpers/tests/handleMapAriaLabels.test.ts +71 -0
  71. package/src/helpers/tests/shouldAutoResetSingleStateZoom.test.ts +71 -0
  72. package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
  73. package/src/hooks/useGeoClickHandler.ts +13 -1
  74. package/src/hooks/useMapLayers.tsx +1 -1
  75. package/src/hooks/useStateZoom.tsx +39 -20
  76. package/src/hooks/useTooltip.test.tsx +2 -16
  77. package/src/hooks/useTooltip.ts +18 -7
  78. package/src/index.jsx +5 -2
  79. package/src/scss/main.scss +6 -21
  80. package/src/scss/map.scss +20 -0
  81. package/src/store/map.actions.ts +5 -2
  82. package/src/store/map.reducer.ts +12 -3
  83. package/src/test/CdcMap.test.jsx +24 -0
  84. package/src/types/MapConfig.ts +11 -0
  85. package/src/types/MapContext.ts +6 -1
  86. package/topojson-updater/README.txt +1 -1
  87. package/dist/cdcmap-vr9HZwRt.es.js +0 -6
  88. package/examples/__data__/city-state-data.json +0 -668
  89. package/examples/city-state.json +0 -434
  90. package/examples/default-world-data.json +0 -1450
  91. package/examples/new-cities.json +0 -656
  92. package/src/_stories/CdcMap.Editor.stories.tsx +0 -3648
  93. package/topojson-updater/package-lock.json +0 -223
  94. /package/src/_stories/{CdcMap.ColumnWrap.stories.tsx → CdcMap.ColumnWrap.smoke.stories.tsx} +0 -0
  95. /package/src/_stories/{CdcMap.Defaults.stories.tsx → CdcMap.Defaults.smoke.stories.tsx} +0 -0
  96. /package/src/_stories/{CdcMap.DistrictOfColumbia.stories.tsx → CdcMap.DistrictOfColumbia.smoke.stories.tsx} +0 -0
  97. /package/src/_stories/{CdcMap.Filters.stories.tsx → CdcMap.Filters.smoke.stories.tsx} +0 -0
  98. /package/src/_stories/{CdcMap.Legend.Gradient.stories.tsx → CdcMap.Legend.Gradient.smoke.stories.tsx} +0 -0
  99. /package/src/_stories/{CdcMap.Legend.stories.tsx → CdcMap.Legend.smoke.stories.tsx} +0 -0
  100. /package/src/_stories/{CdcMap.Patterns.stories.tsx → CdcMap.Patterns.smoke.stories.tsx} +0 -0
  101. /package/src/_stories/{CdcMap.SmallMultiples.stories.tsx → CdcMap.SmallMultiples.smoke.stories.tsx} +0 -0
  102. /package/src/_stories/{CdcMap.Table.stories.tsx → CdcMap.Table.smoke.stories.tsx} +0 -0
  103. /package/src/_stories/{CdcMap.ZeroColor.stories.tsx → CdcMap.ZeroColor.smoke.stories.tsx} +0 -0
  104. /package/src/_stories/{GoogleMap.stories.tsx → GoogleMap.smoke.stories.tsx} +0 -0
  105. /package/src/_stories/{UsaMap.NoData.stories.tsx → UsaMap.NoData.smoke.stories.tsx} +0 -0
@@ -0,0 +1,145 @@
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 TextAnnotationsSectionTests: 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
+ // Open the Text Annotations accordion
38
+ await openAccordion(canvas, 'Text Annotations')
39
+
40
+ // ==========================================================================
41
+ // TEST: Add Annotation
42
+ // ==========================================================================
43
+ const addAnnotationButton = Array.from(canvasElement.querySelectorAll('button')).find(btn => {
44
+ return btn.textContent?.includes('Add Annotation')
45
+ }) as HTMLButtonElement
46
+
47
+ expect(addAnnotationButton).toBeTruthy()
48
+
49
+ await performAndAssert(
50
+ 'Add Annotation → Click to add default annotation',
51
+ () => {
52
+ // Check the actual visualization - annotations appear as divs with aria-label
53
+ // Default annotation text is "New Annotation"
54
+ const annotationElements = canvasElement.querySelectorAll('div[aria-label*="Annotation text"]')
55
+ return {
56
+ annotationCount: annotationElements.length
57
+ }
58
+ },
59
+ async () => {
60
+ await userEvent.click(addAnnotationButton)
61
+ },
62
+ (before, after) => {
63
+ // After clicking, a new annotation should appear in the visualization
64
+ return after.annotationCount > before.annotationCount
65
+ }
66
+ )
67
+
68
+ // ==========================================================================
69
+ // TEST: Change Annotation Text
70
+ // ==========================================================================
71
+ // Wait for the annotation sub-accordion to appear and find the button with "New Annotation" or "Select Column"
72
+ await waitForPresence('.accordion__button', canvasElement)
73
+ const annotationAccordionButtons = canvasElement.querySelectorAll('.accordion__button')
74
+ const annotationAccordionButton = Array.from(annotationAccordionButtons).find(
75
+ btn => btn.textContent?.includes('New annotation') || btn.textContent?.includes('Select Column')
76
+ ) as HTMLElement
77
+
78
+ expect(annotationAccordionButton).toBeTruthy()
79
+
80
+ // Open the annotation's sub-accordion
81
+ await userEvent.click(annotationAccordionButton)
82
+
83
+ // Find the annotation text textarea
84
+ const annotationTextarea = Array.from(canvasElement.querySelectorAll('textarea')).find(textarea => {
85
+ const label = textarea.closest('label')
86
+ return label?.textContent?.includes('Annotation Text')
87
+ }) as HTMLTextAreaElement
88
+
89
+ expect(annotationTextarea).toBeTruthy()
90
+
91
+ await performAndAssert(
92
+ 'Annotation Text → Change to "Important Note"',
93
+ () => {
94
+ // Check the actual visualization - the annotation div should contain the text
95
+ const annotationDivs = canvasElement.querySelectorAll('div[aria-label*="Annotation text"]')
96
+ const texts = Array.from(annotationDivs).map(div => div.innerHTML)
97
+ return {
98
+ annotationTexts: texts.join('|')
99
+ }
100
+ },
101
+ async () => {
102
+ // Select all text and replace it
103
+ await userEvent.click(annotationTextarea)
104
+ await userEvent.keyboard('{Control>}a{/Control}')
105
+ await userEvent.type(annotationTextarea, 'Important Note')
106
+ await userEvent.tab() // Trigger blur to commit the change
107
+ },
108
+ (before, after) => {
109
+ // After changing the text, the annotation should display the new text
110
+ return after.annotationTexts.includes('Important Note')
111
+ }
112
+ )
113
+
114
+ // ==========================================================================
115
+ // TEST: Delete Annotation
116
+ // ==========================================================================
117
+ const deleteButton = Array.from(canvasElement.querySelectorAll('button')).find(btn => {
118
+ return btn.textContent?.includes('Delete Annotation')
119
+ }) as HTMLButtonElement
120
+
121
+ expect(deleteButton).toBeTruthy()
122
+
123
+ await performAndAssert(
124
+ 'Delete Annotation → Remove annotation from map',
125
+ () => {
126
+ // Check the visualization - count the annotations
127
+ const annotationDivs = canvasElement.querySelectorAll('div[aria-label*="Annotation text"]')
128
+ return {
129
+ annotationCount: annotationDivs.length
130
+ }
131
+ },
132
+ async () => {
133
+ await userEvent.click(deleteButton)
134
+ },
135
+ (before, after) => {
136
+ // After deleting, annotation count should decrease
137
+ return after.annotationCount < before.annotationCount
138
+ }
139
+ )
140
+ }
141
+ }
142
+
143
+ // ============================================================================
144
+ // SMALL MULTIPLES SECTION TESTS
145
+ // ============================================================================
@@ -0,0 +1,312 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, userEvent, expect } from 'storybook/test'
3
+ import CdcMap from '../CdcMap'
4
+ import usaStateGradientConfig from './_mock/usa-state-gradient.json'
5
+ import multiCountryConfig from './_mock/multi-country.json'
6
+ import wastewaterMapSmallMultiplesConfig from './_mock/small_multiples/wastewater-map-small-multiples.json'
7
+ import { performAndAssert, waitForEditor, waitForPresence, openAccordion } from '@cdc/core/helpers/testing'
8
+
9
+ type Story = StoryObj<typeof CdcMap>
10
+
11
+ const mapMeta: Meta<typeof CdcMap> = {
12
+ title: 'Components/Templates/Map/Editor Tests',
13
+ component: CdcMap,
14
+ parameters: {
15
+ layout: 'fullscreen'
16
+ }
17
+ }
18
+
19
+ export default mapMeta
20
+
21
+ const DEFAULT_ARGS = {
22
+ isEditor: true,
23
+ config: usaStateGradientConfig
24
+ }
25
+
26
+ export const TypeSectionTests: Story = {
27
+ args: {
28
+ ...DEFAULT_ARGS
29
+ },
30
+ play: async ({ canvasElement }) => {
31
+ const canvas = within(canvasElement)
32
+
33
+ await waitForEditor(canvas)
34
+ await waitForPresence('.map-container', canvasElement)
35
+
36
+ await openAccordion(canvas, 'Type')
37
+
38
+ const getMapContainerState = () => {
39
+ const container = canvasElement.querySelector('.map-container') as HTMLElement | null
40
+ const svg = canvasElement.querySelector('svg') as SVGElement | null
41
+
42
+ return {
43
+ classes: container ? Array.from(container.classList) : [],
44
+ hasSvg: Boolean(svg),
45
+ isBubble: container?.classList.contains('bubble') ?? false,
46
+ geoType: container?.classList.contains('us-county')
47
+ ? 'us-county'
48
+ : container?.classList.contains('us')
49
+ ? 'us'
50
+ : container?.classList.contains('single-state')
51
+ ? 'single-state'
52
+ : container?.classList.contains('world')
53
+ ? 'world'
54
+ : container?.classList.contains('us-region')
55
+ ? 'us-region'
56
+ : 'unknown'
57
+ }
58
+ }
59
+
60
+ // ==========================================================================
61
+ // TEST: Geography buttons change the geoType class on the map container
62
+ // ==========================================================================
63
+ const geoButtons = Array.from(canvasElement.querySelectorAll('.geo-buttons button')) as HTMLButtonElement[]
64
+ expect(geoButtons.length).toBeGreaterThanOrEqual(3)
65
+
66
+ const geographyButtonMap = geoButtons.reduce<Record<string, HTMLButtonElement>>((acc, button) => {
67
+ const label = button.textContent?.trim() ?? ''
68
+ acc[label.toLowerCase()] = button
69
+ return acc
70
+ }, {})
71
+
72
+ const targetButtons = [
73
+ { label: 'world', targetClasses: ['world'] },
74
+ { label: 'united states', targetClasses: ['us'] }
75
+ ]
76
+
77
+ for (const { label, targetClasses } of targetButtons) {
78
+ const button = geographyButtonMap[label]
79
+ expect(button).toBeTruthy()
80
+
81
+ await performAndAssert(
82
+ `GeoType → ${label}`,
83
+ getMapContainerState,
84
+ async () => {
85
+ await userEvent.click(button)
86
+ },
87
+ (before, after) =>
88
+ targetClasses.some(
89
+ targetClass => !before.classes.includes(targetClass) && after.classes.includes(targetClass)
90
+ )
91
+ )
92
+ }
93
+
94
+ // ==========================================================================
95
+ // TEST: Geography subtype select toggles county/state classes
96
+ // ==========================================================================
97
+ const subtypeSelect = canvas.getByLabelText(/Geography Subtype/i) as HTMLSelectElement
98
+ const subtypeValues = Array.from(subtypeSelect.options).map(option => option.value)
99
+ expect(subtypeValues).toContain('us-county')
100
+
101
+ await performAndAssert(
102
+ 'GeoType Subtype → US County',
103
+ getMapContainerState,
104
+ async () => {
105
+ await userEvent.selectOptions(subtypeSelect, 'us-county')
106
+ },
107
+ (before, after) => !before.classes.includes('us-county') && after.classes.includes('us-county'),
108
+ () => {
109
+ const territoriesCheckbox = canvas.getByLabelText(/Show Available Territories/i) as HTMLInputElement
110
+ expect(territoriesCheckbox).toBeTruthy()
111
+ expect(territoriesCheckbox.disabled).toBe(false)
112
+ }
113
+ )
114
+
115
+ await performAndAssert(
116
+ 'GeoType Subtype → Reset',
117
+ getMapContainerState,
118
+ async () => {
119
+ await userEvent.selectOptions(subtypeSelect, 'us')
120
+ },
121
+ (before, after) => before.classes.includes('us-county') && after.classes.includes('us')
122
+ )
123
+
124
+ // ==========================================================================
125
+ // TEST: Map Type select toggles classes/data representation
126
+ // ==========================================================================
127
+ // Use getAllByLabelText to avoid multiple elements error
128
+ const typeSelects = canvas.getAllByLabelText(/Map Type/i, { selector: 'select' }) as HTMLSelectElement[]
129
+ // Assume the first select is the correct one for the Type section
130
+ const typeSelect = typeSelects[0]
131
+ const initialType = typeSelect.value
132
+ const mapTypeOptions = Array.from(typeSelect.options).map(option => option.value)
133
+ expect(mapTypeOptions).toContain('navigation')
134
+
135
+ await performAndAssert(
136
+ 'Map Type → Navigation',
137
+ getMapContainerState,
138
+ async () => {
139
+ await userEvent.selectOptions(typeSelect, 'navigation')
140
+ },
141
+ (before, after) => !before.classes.includes('navigation') && after.classes.includes('navigation')
142
+ )
143
+
144
+ await performAndAssert(
145
+ 'Map Type → Reset',
146
+ getMapContainerState,
147
+ async () => {
148
+ await userEvent.selectOptions(typeSelect, initialType)
149
+ },
150
+ (before, after) =>
151
+ before.classes.includes('navigation') &&
152
+ !after.classes.includes('navigation') &&
153
+ after.classes.includes(initialType)
154
+ )
155
+
156
+ // ==========================================================================
157
+ // TEST: Data Classification Type radio buttons
158
+ // Verifies: Legend structure changes between numeric/quantitative and categorical
159
+ // ==========================================================================
160
+ const numericRadio = canvasElement.querySelector('input[type="radio"][value="equalnumber"]') as HTMLInputElement
161
+ const categoryRadio = canvasElement.querySelector('input[type="radio"][value="category"]') as HTMLInputElement
162
+ expect(numericRadio).toBeTruthy()
163
+ expect(categoryRadio).toBeTruthy()
164
+
165
+ const getLegendStructure = () => {
166
+ const legend = canvasElement.querySelector('.map-legend, .legend-container') as HTMLElement | null
167
+ const legendItems = canvasElement.querySelectorAll('.legend-item, .legend-container > div, .legend li')
168
+ const legendRects = legend?.querySelectorAll('rect')
169
+ const legendText = legend?.querySelectorAll('text')
170
+ return {
171
+ legendExists: Boolean(legend),
172
+ legendItemCount: legendItems.length,
173
+ legendRectCount: legendRects?.length || 0,
174
+ legendTextCount: legendText?.length || 0,
175
+ legendFullHTML: legend?.innerHTML || ''
176
+ }
177
+ }
178
+
179
+ await performAndAssert(
180
+ 'Classification Type → Category',
181
+ getLegendStructure,
182
+ async () => {
183
+ await userEvent.click(categoryRadio)
184
+ },
185
+ (before, after) =>
186
+ before.legendRectCount !== after.legendRectCount &&
187
+ before.legendTextCount !== after.legendTextCount &&
188
+ before.legendFullHTML !== after.legendFullHTML
189
+ )
190
+
191
+ await performAndAssert(
192
+ 'Classification Type → Numeric',
193
+ getLegendStructure,
194
+ async () => {
195
+ await userEvent.click(numericRadio)
196
+ },
197
+ (before, after) =>
198
+ before.legendRectCount !== after.legendRectCount &&
199
+ before.legendTextCount !== after.legendTextCount &&
200
+ before.legendFullHTML !== after.legendFullHTML
201
+ )
202
+
203
+ // ==========================================================================
204
+ // TEST: Display As Hex Map checkbox
205
+ // Verifies: Hexagon SVG polygons appear/disappear in map visualization
206
+ // ==========================================================================
207
+ const hexLabel = Array.from(canvasElement.querySelectorAll('label')).find(label =>
208
+ label.textContent?.includes('Display As Hex Map')
209
+ )
210
+ const actualHexCheckbox = hexLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
211
+ expect(actualHexCheckbox).toBeTruthy()
212
+
213
+ const getHexVisualization = () => {
214
+ const hexElements = canvasElement.querySelectorAll('.territory-wrapper--hex, polygon[points*="22 0 44 12.702"]')
215
+ return {
216
+ hexElementCount: hexElements.length
217
+ }
218
+ }
219
+
220
+ await performAndAssert(
221
+ 'Display As Hex Map → Enable',
222
+ getHexVisualization,
223
+ async () => {
224
+ await userEvent.click(actualHexCheckbox)
225
+ },
226
+ (before, after) => before.hexElementCount === 0 && after.hexElementCount > 0
227
+ )
228
+
229
+ await performAndAssert(
230
+ 'Display As Hex Map → Disable',
231
+ getHexVisualization,
232
+ async () => {
233
+ await userEvent.click(actualHexCheckbox)
234
+ },
235
+ (before, after) => before.hexElementCount > 0 && after.hexElementCount === 0
236
+ )
237
+
238
+ // ==========================================================================
239
+ // TEST: Show state labels checkbox
240
+ // Verifies: State abbreviation text elements appear/disappear on map SVG
241
+ // ==========================================================================
242
+ const stateLabelsCheckbox = canvas.getByLabelText(/Show state labels/i) as HTMLInputElement
243
+ expect(stateLabelsCheckbox).toBeTruthy()
244
+
245
+ const getStateLabelsVisual = () => {
246
+ const mapSvg = canvasElement.querySelector('svg[role="img"]')
247
+ const textElements = mapSvg?.querySelectorAll('text')
248
+ // State labels are text elements with short state abbreviations (2 chars)
249
+ const stateLabelTexts = Array.from(textElements || []).filter(text => {
250
+ const content = text.textContent?.trim()
251
+ return content && content.length === 2 && /^[A-Z]{2}$/.test(content)
252
+ })
253
+ return {
254
+ stateLabelCount: stateLabelTexts.length
255
+ }
256
+ }
257
+
258
+ await performAndAssert(
259
+ 'Show State Labels → Enable',
260
+ getStateLabelsVisual,
261
+ async () => {
262
+ await userEvent.click(stateLabelsCheckbox)
263
+ },
264
+ (before, after) => before.stateLabelCount === 0 && after.stateLabelCount > 0
265
+ )
266
+ await performAndAssert(
267
+ 'Show State Labels → Disable',
268
+ getStateLabelsVisual,
269
+ async () => {
270
+ await userEvent.click(stateLabelsCheckbox)
271
+ },
272
+ (before, after) => before.stateLabelCount > 0 && after.stateLabelCount === 0
273
+ )
274
+
275
+ // ==========================================================================
276
+ // TEST: Show Available Territories checkbox
277
+ // Verifies: Territory SVG elements appear/disappear in visualization
278
+ // ==========================================================================
279
+ const territoriesLabel = Array.from(canvasElement.querySelectorAll('label')).find(label =>
280
+ label.textContent?.includes('Show Available Territories')
281
+ )
282
+ const territoriesCheckbox = territoriesLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement
283
+ expect(territoriesCheckbox).toBeTruthy()
284
+
285
+ const getTerritoriesVisual = () => {
286
+ const territorySection = canvasElement.querySelector('.territories')
287
+ const territorySvgs = territorySection?.querySelectorAll('svg')
288
+ return {
289
+ territorySvgCount: territorySvgs?.length || 0,
290
+ hasTerritorySection: Boolean(territorySection)
291
+ }
292
+ }
293
+
294
+ await performAndAssert(
295
+ 'Show Available Territories → Enable',
296
+ getTerritoriesVisual,
297
+ async () => {
298
+ await userEvent.click(territoriesCheckbox)
299
+ },
300
+ (before, after) => before.territorySvgCount !== after.territorySvgCount
301
+ )
302
+
303
+ await performAndAssert(
304
+ 'Show Available Territories → Disable',
305
+ getTerritoriesVisual,
306
+ async () => {
307
+ await userEvent.click(territoriesCheckbox)
308
+ },
309
+ (before, after) => before.territorySvgCount !== after.territorySvgCount
310
+ )
311
+ }
312
+ }