@cdc/map 4.26.3 → 4.26.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIG.md +235 -0
- package/README.md +70 -24
- package/dist/cdcmap-CY9IcPSi.es.js +6 -0
- package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
- package/dist/cdcmap.js +27405 -26257
- package/examples/{testing-layer-2.json → __data__/testing-layer-2.json} +1 -1
- package/examples/{testing-layer.json → __data__/testing-layer.json} +1 -1
- package/examples/county-hsa-toggle.json +51993 -0
- package/examples/custom-map-layers.json +2 -2
- package/examples/default-county.json +3 -3
- package/examples/minimal-example.json +69 -0
- package/examples/private/annotation-bug.json +2 -2
- package/examples/private/css-issue.json +314 -0
- package/examples/private/region-breaking.json +1639 -0
- package/examples/private/test1.json +27247 -0
- package/package.json +4 -4
- package/src/CdcMapComponent.tsx +96 -13
- package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +601 -0
- package/src/_stories/CdcMap.Editor.DataTableSectionTests.stories.tsx +404 -0
- package/src/_stories/CdcMap.Editor.FiltersSectionTests.stories.tsx +229 -0
- package/src/_stories/CdcMap.Editor.GeneralSectionTests.stories.tsx +262 -0
- package/src/_stories/CdcMap.Editor.LegendSectionTests.stories.tsx +541 -0
- package/src/_stories/CdcMap.Editor.MultiCountryWorldMapTests.stories.tsx +359 -0
- package/src/_stories/CdcMap.Editor.PatternSettingsSectionTests.stories.tsx +516 -0
- package/src/_stories/CdcMap.Editor.SmallMultiplesSectionTests.stories.tsx +165 -0
- package/src/_stories/CdcMap.Editor.TextAnnotationsSectionTests.stories.tsx +145 -0
- package/src/_stories/CdcMap.Editor.TypeSectionTests.stories.tsx +312 -0
- package/src/_stories/CdcMap.Editor.VisualSectionTests.stories.tsx +359 -0
- package/src/_stories/CdcMap.Editor.ZoomControlsTests.stories.tsx +88 -0
- package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +12 -0
- package/src/_stories/_mock/legends/legend-tests.json +3 -3
- package/src/components/Annotation/AnnotationList.tsx +1 -1
- package/src/components/EditorPanel/components/EditorPanel.tsx +504 -383
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +112 -117
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +26 -13
- package/src/components/EditorPanel/components/editorPanel.styles.css +22 -2
- package/src/components/Legend/components/Legend.tsx +3 -3
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
- package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
- package/src/components/UsaMap/components/UsaMap.County.tsx +271 -100
- package/src/components/UsaMap/components/UsaMap.State.tsx +1 -1
- package/src/components/UsaMap/data/cb_2019_us_county_20m.json +75817 -1
- package/src/components/UsaMap/data/hsa_fips_mapping.json +3144 -0
- package/src/components/WorldMap/data/world-topo.json +1 -1
- package/src/data/initial-state.js +1 -0
- package/src/data/supported-counties.json +1 -1
- package/src/helpers/countyTerritories.ts +38 -0
- package/src/helpers/dataTableHelpers.ts +35 -6
- package/src/helpers/tests/countyTerritories.test.ts +87 -0
- package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
- package/src/hooks/useMapLayers.tsx +1 -1
- package/src/hooks/useTooltip.ts +18 -7
- package/src/store/map.actions.ts +5 -2
- package/src/store/map.reducer.ts +12 -3
- package/src/test/CdcMap.test.jsx +24 -0
- package/src/types/MapConfig.ts +6 -0
- package/src/types/MapContext.ts +3 -1
- package/topojson-updater/README.txt +1 -1
- package/LICENSE +0 -201
- package/dist/cdcmap-vr9HZwRt.es.js +0 -6
- package/examples/__data__/city-state-data.json +0 -668
- package/examples/city-state.json +0 -434
- package/examples/default-world-data.json +0 -1450
- package/examples/new-cities.json +0 -656
- package/src/_stories/CdcMap.Editor.stories.tsx +0 -3648
- package/topojson-updater/package-lock.json +0 -223
- /package/src/_stories/{CdcMap.ColumnWrap.stories.tsx → CdcMap.ColumnWrap.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Defaults.stories.tsx → CdcMap.Defaults.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.DistrictOfColumbia.stories.tsx → CdcMap.DistrictOfColumbia.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Filters.stories.tsx → CdcMap.Filters.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Legend.Gradient.stories.tsx → CdcMap.Legend.Gradient.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Legend.stories.tsx → CdcMap.Legend.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Patterns.stories.tsx → CdcMap.Patterns.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.SmallMultiples.stories.tsx → CdcMap.SmallMultiples.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Table.stories.tsx → CdcMap.Table.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.ZeroColor.stories.tsx → CdcMap.ZeroColor.smoke.stories.tsx} +0 -0
- /package/src/_stories/{GoogleMap.stories.tsx → GoogleMap.smoke.stories.tsx} +0 -0
- /package/src/_stories/{UsaMap.NoData.stories.tsx → UsaMap.NoData.smoke.stories.tsx} +0 -0
|
@@ -0,0 +1,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
|
+
}
|