@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,404 @@
|
|
|
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 DataTableSectionTests: 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, 'Data Table')
|
|
37
|
+
|
|
38
|
+
// ==========================================================================
|
|
39
|
+
// TEST: Data Table Title
|
|
40
|
+
// ==========================================================================
|
|
41
|
+
const dataTableTitleInput = canvasElement.querySelector('#dataTableTitle') as HTMLInputElement
|
|
42
|
+
|
|
43
|
+
await performAndAssert(
|
|
44
|
+
'Data Table Title → Set custom title',
|
|
45
|
+
() => {
|
|
46
|
+
const dataTableHeading = canvasElement.querySelector('.data-table-heading')
|
|
47
|
+
return {
|
|
48
|
+
titleText: dataTableHeading?.textContent?.trim() || ''
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async () => {
|
|
52
|
+
await userEvent.clear(dataTableTitleInput)
|
|
53
|
+
await userEvent.type(dataTableTitleInput, 'State Population Data')
|
|
54
|
+
},
|
|
55
|
+
(before, after) => {
|
|
56
|
+
return after.titleText.includes('State Population Data')
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// ==========================================================================
|
|
61
|
+
// TEST: Wrap Data Table Columns
|
|
62
|
+
// ==========================================================================
|
|
63
|
+
// First expand the data table if it's collapsed
|
|
64
|
+
const dataTableHeadingButton = canvasElement.querySelector('.data-table-heading') as HTMLElement
|
|
65
|
+
const dataTableContainer = canvasElement.querySelector('.data-table-container')
|
|
66
|
+
const isCollapsed = dataTableHeadingButton?.classList.contains('collapsed')
|
|
67
|
+
if (isCollapsed) {
|
|
68
|
+
await userEvent.click(dataTableHeadingButton)
|
|
69
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const wrapColumnsCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(checkbox => {
|
|
73
|
+
const label = checkbox.closest('label')
|
|
74
|
+
return label?.textContent?.includes('WRAP DATA TABLE COLUMNS')
|
|
75
|
+
}) as HTMLInputElement
|
|
76
|
+
|
|
77
|
+
await performAndAssert(
|
|
78
|
+
'Wrap Columns → Toggle wrapping',
|
|
79
|
+
() => {
|
|
80
|
+
const dataTable = canvasElement.querySelector('.data-table')
|
|
81
|
+
const firstCell = dataTable?.querySelector('tbody td')
|
|
82
|
+
const whiteSpace = firstCell ? window.getComputedStyle(firstCell).whiteSpace : ''
|
|
83
|
+
return {
|
|
84
|
+
whiteSpace,
|
|
85
|
+
hasCells: Boolean(firstCell)
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async () => {
|
|
89
|
+
await userEvent.click(wrapColumnsCheckbox)
|
|
90
|
+
},
|
|
91
|
+
(before, after) => {
|
|
92
|
+
// When wrap is enabled, white-space should be 'normal' or 'unset', not 'nowrap'
|
|
93
|
+
return after.hasCells && before.whiteSpace === 'nowrap' && after.whiteSpace !== 'nowrap'
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
// ==========================================================================
|
|
98
|
+
// TEST: Show Data Table
|
|
99
|
+
// ==========================================================================
|
|
100
|
+
const showDataTableCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(
|
|
101
|
+
checkbox => {
|
|
102
|
+
const label = checkbox.closest('label')
|
|
103
|
+
return label?.textContent?.includes('Show Data Table') && !label?.textContent?.includes('Non Geographic')
|
|
104
|
+
}
|
|
105
|
+
) as HTMLInputElement
|
|
106
|
+
|
|
107
|
+
await performAndAssert(
|
|
108
|
+
'Show Data Table → Hide data table',
|
|
109
|
+
() => {
|
|
110
|
+
const dataTableContainer = canvasElement.querySelector('.data-table-container')
|
|
111
|
+
const isVisible = dataTableContainer && window.getComputedStyle(dataTableContainer).display !== 'none'
|
|
112
|
+
return {
|
|
113
|
+
isVisible: Boolean(isVisible)
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
async () => {
|
|
117
|
+
await userEvent.click(showDataTableCheckbox)
|
|
118
|
+
},
|
|
119
|
+
(before, after) => {
|
|
120
|
+
// Should hide the data table
|
|
121
|
+
return before.isVisible && !after.isVisible
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// Toggle it back on for the next test
|
|
126
|
+
await userEvent.click(showDataTableCheckbox)
|
|
127
|
+
|
|
128
|
+
// ==========================================================================
|
|
129
|
+
// TEST: Show Non Geographic Data
|
|
130
|
+
// ==========================================================================
|
|
131
|
+
const showNonGeoDataCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(
|
|
132
|
+
checkbox => {
|
|
133
|
+
const label = checkbox.closest('label')
|
|
134
|
+
return label?.textContent?.includes('Show Non Geographic Data')
|
|
135
|
+
}
|
|
136
|
+
) as HTMLInputElement
|
|
137
|
+
|
|
138
|
+
await performAndAssert(
|
|
139
|
+
'Show Non Geographic Data → Toggle visibility',
|
|
140
|
+
() => {
|
|
141
|
+
const dataTable = canvasElement.querySelector('.data-table')
|
|
142
|
+
const rows = Array.from(dataTable?.querySelectorAll('tbody tr') || [])
|
|
143
|
+
const rowCount = rows.length
|
|
144
|
+
return {
|
|
145
|
+
rowCount
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
async () => {
|
|
149
|
+
await userEvent.click(showNonGeoDataCheckbox)
|
|
150
|
+
},
|
|
151
|
+
(before, after) => {
|
|
152
|
+
// Toggling non-geographic data should add rows to the table (overall data object)
|
|
153
|
+
return after.rowCount > before.rowCount
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
// ==========================================================================
|
|
158
|
+
// TEST: Index Column Header
|
|
159
|
+
// ==========================================================================
|
|
160
|
+
const indexColumnHeaderInput = Array.from(canvasElement.querySelectorAll('input[type="text"]')).find(input => {
|
|
161
|
+
const label = input.closest('label')
|
|
162
|
+
return label?.textContent?.includes('Index Column Header')
|
|
163
|
+
}) as HTMLInputElement
|
|
164
|
+
|
|
165
|
+
await performAndAssert(
|
|
166
|
+
'Index Column Header → Set custom header',
|
|
167
|
+
() => {
|
|
168
|
+
const dataTable = canvasElement.querySelector('.data-table')
|
|
169
|
+
const firstHeader = dataTable?.querySelector('thead th')
|
|
170
|
+
return {
|
|
171
|
+
headerText: firstHeader?.textContent?.trim() || ''
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
async () => {
|
|
175
|
+
await userEvent.clear(indexColumnHeaderInput)
|
|
176
|
+
await userEvent.type(indexColumnHeaderInput, 'State/Territory')
|
|
177
|
+
},
|
|
178
|
+
(before, after) => {
|
|
179
|
+
return after.headerText.includes('State/Territory')
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
// ==========================================================================
|
|
184
|
+
// TEST: Screen Reader Description
|
|
185
|
+
// ==========================================================================
|
|
186
|
+
const screenReaderDescTextarea = Array.from(canvasElement.querySelectorAll('textarea')).find(textarea => {
|
|
187
|
+
const label = textarea.closest('label')
|
|
188
|
+
return label?.textContent?.includes('Screen Reader Description')
|
|
189
|
+
}) as HTMLTextAreaElement
|
|
190
|
+
|
|
191
|
+
await performAndAssert(
|
|
192
|
+
'Screen Reader Description → Set custom description',
|
|
193
|
+
() => {
|
|
194
|
+
const dataTable = canvasElement.querySelector('.data-table')
|
|
195
|
+
const caption = dataTable?.querySelector('caption')
|
|
196
|
+
return {
|
|
197
|
+
captionText: caption?.textContent?.trim() || ''
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
async () => {
|
|
201
|
+
await userEvent.clear(screenReaderDescTextarea)
|
|
202
|
+
await userEvent.type(screenReaderDescTextarea, 'Table showing state population data by region')
|
|
203
|
+
},
|
|
204
|
+
(before, after) => {
|
|
205
|
+
return after.captionText.includes('Table showing state population data')
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
// ==========================================================================
|
|
210
|
+
// TEST: Limit Table Height
|
|
211
|
+
// ==========================================================================
|
|
212
|
+
const limitHeightCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(checkbox => {
|
|
213
|
+
const label = checkbox.closest('label')
|
|
214
|
+
return label?.textContent?.includes('Limit Table Height')
|
|
215
|
+
}) as HTMLInputElement
|
|
216
|
+
|
|
217
|
+
await performAndAssert(
|
|
218
|
+
'Limit Table Height → Enable height limit',
|
|
219
|
+
() => {
|
|
220
|
+
// Check if the "Data Table Height" input field appears (conditional rendering)
|
|
221
|
+
const heightInput = Array.from(canvasElement.querySelectorAll('input[type="number"]')).find(input => {
|
|
222
|
+
const label = input.closest('label')
|
|
223
|
+
return label?.textContent?.includes('Data Table Height')
|
|
224
|
+
})
|
|
225
|
+
return {
|
|
226
|
+
hasHeightInput: Boolean(heightInput)
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
async () => {
|
|
230
|
+
await userEvent.click(limitHeightCheckbox)
|
|
231
|
+
},
|
|
232
|
+
(before, after) => {
|
|
233
|
+
// After enabling, the "Data Table Height" input field should appear
|
|
234
|
+
return !before.hasHeightInput && after.hasHeightInput
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
// ==========================================================================
|
|
239
|
+
// TEST: Table Cell Min Width
|
|
240
|
+
// ==========================================================================
|
|
241
|
+
const cellMinWidthInput = Array.from(canvasElement.querySelectorAll('input[type="number"]')).find(input => {
|
|
242
|
+
const label = input.closest('label')
|
|
243
|
+
return label?.textContent?.includes('Table Cell Min Width')
|
|
244
|
+
}) as HTMLInputElement
|
|
245
|
+
|
|
246
|
+
await performAndAssert(
|
|
247
|
+
'Table Cell Min Width → Set minimum width',
|
|
248
|
+
() => {
|
|
249
|
+
const dataTable = canvasElement.querySelector('.data-table')
|
|
250
|
+
const firstCell = dataTable?.querySelector('thead th')
|
|
251
|
+
const minWidth = firstCell ? window.getComputedStyle(firstCell).minWidth : ''
|
|
252
|
+
return {
|
|
253
|
+
minWidth
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
async () => {
|
|
257
|
+
await userEvent.clear(cellMinWidthInput)
|
|
258
|
+
await userEvent.type(cellMinWidthInput, '150')
|
|
259
|
+
},
|
|
260
|
+
(before, after) => {
|
|
261
|
+
// After setting min width to 150px, cells should have minWidth of 150px
|
|
262
|
+
return before.minWidth !== after.minWidth && after.minWidth === '150px'
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
// ==========================================================================
|
|
267
|
+
// TEST: Show Download CSV Link
|
|
268
|
+
// ==========================================================================
|
|
269
|
+
const showDownloadCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(checkbox => {
|
|
270
|
+
const label = checkbox.closest('label')
|
|
271
|
+
return label?.textContent?.includes('Show Download CSV Link')
|
|
272
|
+
}) as HTMLInputElement
|
|
273
|
+
|
|
274
|
+
// First toggle it off, then back on
|
|
275
|
+
await performAndAssert(
|
|
276
|
+
'Show Download CSV Link → Toggle off',
|
|
277
|
+
() => {
|
|
278
|
+
const downloadLink = Array.from(canvasElement.querySelectorAll('a')).find(link =>
|
|
279
|
+
link.textContent?.includes('Download Data')
|
|
280
|
+
)
|
|
281
|
+
return {
|
|
282
|
+
hasDownloadLink: Boolean(downloadLink)
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
async () => {
|
|
286
|
+
await userEvent.click(showDownloadCheckbox)
|
|
287
|
+
},
|
|
288
|
+
(before, after) => {
|
|
289
|
+
// After disabling, the download link should disappear
|
|
290
|
+
return before.hasDownloadLink && !after.hasDownloadLink
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
await performAndAssert(
|
|
295
|
+
'Show Download CSV Link → Toggle back on',
|
|
296
|
+
() => {
|
|
297
|
+
const downloadLink = Array.from(canvasElement.querySelectorAll('a')).find(link =>
|
|
298
|
+
link.textContent?.includes('Download Data')
|
|
299
|
+
)
|
|
300
|
+
return {
|
|
301
|
+
hasDownloadLink: Boolean(downloadLink)
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
async () => {
|
|
305
|
+
await userEvent.click(showDownloadCheckbox)
|
|
306
|
+
},
|
|
307
|
+
(before, after) => {
|
|
308
|
+
// After re-enabling, the download link should reappear
|
|
309
|
+
return !before.hasDownloadLink && after.hasDownloadLink
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
// ==========================================================================
|
|
314
|
+
// TEST: Show Link Below Table
|
|
315
|
+
// ==========================================================================
|
|
316
|
+
const showLinkBelowCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(
|
|
317
|
+
checkbox => {
|
|
318
|
+
const label = checkbox.closest('label')
|
|
319
|
+
return label?.textContent?.includes('Show Link Below Table')
|
|
320
|
+
}
|
|
321
|
+
) as HTMLInputElement
|
|
322
|
+
|
|
323
|
+
// First toggle it off (move link above), then back on (move link below)
|
|
324
|
+
await performAndAssert(
|
|
325
|
+
'Show Link Below Table → Toggle off (move above)',
|
|
326
|
+
() => {
|
|
327
|
+
const dataTableContainer = canvasElement.querySelector('.data-table-container')
|
|
328
|
+
const downloadSection = canvasElement.querySelector('.download-links')
|
|
329
|
+
// Check if download link is positioned after the data table
|
|
330
|
+
const containerRect = dataTableContainer?.getBoundingClientRect()
|
|
331
|
+
const downloadRect = downloadSection?.getBoundingClientRect()
|
|
332
|
+
const isBelow = downloadRect && containerRect && downloadRect.top > containerRect.bottom
|
|
333
|
+
return {
|
|
334
|
+
isBelow: Boolean(isBelow)
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
async () => {
|
|
338
|
+
await userEvent.click(showLinkBelowCheckbox)
|
|
339
|
+
},
|
|
340
|
+
(before, after) => {
|
|
341
|
+
// After disabling, download link should move above the table
|
|
342
|
+
return before.isBelow && !after.isBelow
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
await performAndAssert(
|
|
347
|
+
'Show Link Below Table → Toggle back on (move below)',
|
|
348
|
+
() => {
|
|
349
|
+
const dataTableContainer = canvasElement.querySelector('.data-table-container')
|
|
350
|
+
const downloadSection = canvasElement.querySelector('.download-links')
|
|
351
|
+
const containerRect = dataTableContainer?.getBoundingClientRect()
|
|
352
|
+
const downloadRect = downloadSection?.getBoundingClientRect()
|
|
353
|
+
const isBelow = downloadRect && containerRect && downloadRect.top > containerRect.bottom
|
|
354
|
+
return {
|
|
355
|
+
isBelow: Boolean(isBelow)
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
async () => {
|
|
359
|
+
await userEvent.click(showLinkBelowCheckbox)
|
|
360
|
+
},
|
|
361
|
+
(before, after) => {
|
|
362
|
+
// After re-enabling, download link should move back below the table
|
|
363
|
+
return !before.isBelow && after.isBelow
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
// ==========================================================================
|
|
368
|
+
// TEST: Enable Image Download
|
|
369
|
+
// ==========================================================================
|
|
370
|
+
const enableImageDownloadCheckbox = Array.from(canvasElement.querySelectorAll('input[type="checkbox"]')).find(
|
|
371
|
+
checkbox => {
|
|
372
|
+
const label = checkbox.closest('label')
|
|
373
|
+
return label?.textContent?.includes('Enable Image Download')
|
|
374
|
+
}
|
|
375
|
+
) as HTMLInputElement
|
|
376
|
+
|
|
377
|
+
await performAndAssert(
|
|
378
|
+
'Enable Image Download → Enable button',
|
|
379
|
+
() => {
|
|
380
|
+
const downloadImgButton =
|
|
381
|
+
Array.from(canvasElement.querySelectorAll('button')).find(
|
|
382
|
+
btn => btn.textContent?.includes('Download Image') || btn.classList.contains('download-image')
|
|
383
|
+
) ||
|
|
384
|
+
Array.from(canvasElement.querySelectorAll('a[role="button"]')).find(
|
|
385
|
+
link => link.textContent?.includes('Download Map') && link.textContent?.includes('PNG')
|
|
386
|
+
)
|
|
387
|
+
return {
|
|
388
|
+
hasDownloadImgButton: Boolean(downloadImgButton)
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
async () => {
|
|
392
|
+
await userEvent.click(enableImageDownloadCheckbox)
|
|
393
|
+
},
|
|
394
|
+
(before, after) => {
|
|
395
|
+
// After enabling, the download image button should appear
|
|
396
|
+
return !before.hasDownloadImgButton && after.hasDownloadImgButton
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// =================================================================================================
|
|
403
|
+
// Visual Section Tests
|
|
404
|
+
// =================================================================================================
|
|
@@ -0,0 +1,229 @@
|
|
|
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 FiltersSectionTests: 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, 'Filters')
|
|
37
|
+
|
|
38
|
+
// ==========================================================================
|
|
39
|
+
// TEST: Add Filter and configure it to filter by STATE
|
|
40
|
+
// ==========================================================================
|
|
41
|
+
const filtersAccordionButton = canvas
|
|
42
|
+
.getAllByRole('button', { name: /^Filters$/i })
|
|
43
|
+
.find(button => button.closest('[data-accordion-component="AccordionItem"], .accordion__item'))
|
|
44
|
+
const filtersAccordion = filtersAccordionButton?.closest(
|
|
45
|
+
'[data-accordion-component="AccordionItem"], .accordion__item'
|
|
46
|
+
)
|
|
47
|
+
expect(filtersAccordion).toBeTruthy()
|
|
48
|
+
|
|
49
|
+
const addFilterButton = within(filtersAccordion as HTMLElement).getByRole('button', { name: /Add Filter/i })
|
|
50
|
+
|
|
51
|
+
await performAndAssert(
|
|
52
|
+
'Add Filter → Click button',
|
|
53
|
+
() => {
|
|
54
|
+
const filtersList = filtersAccordion?.querySelector('.grouped-list__items')
|
|
55
|
+
const collapsedFilters = filtersList?.querySelectorAll('.series-item--chart') || []
|
|
56
|
+
return {
|
|
57
|
+
hasFiltersList: Boolean(filtersList),
|
|
58
|
+
hasCollapsedFilter: collapsedFilters.length > 0
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
async () => {
|
|
62
|
+
await userEvent.click(addFilterButton)
|
|
63
|
+
},
|
|
64
|
+
(before, after) => {
|
|
65
|
+
// Should create filters list and add a collapsed filter
|
|
66
|
+
return after.hasFiltersList && after.hasCollapsedFilter
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// Filter item starts expanded (preExpanded); find the visible filter panel content
|
|
71
|
+
const filtersList = filtersAccordion?.querySelector('.grouped-list__items')
|
|
72
|
+
|
|
73
|
+
const getFilterBlock = () => {
|
|
74
|
+
return Array.from(filtersAccordion?.querySelectorAll('select') || [])
|
|
75
|
+
.find(select => {
|
|
76
|
+
const label = select.closest('label')
|
|
77
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
78
|
+
return labelSpan?.textContent?.includes('Filter Style')
|
|
79
|
+
})
|
|
80
|
+
?.closest(
|
|
81
|
+
'[data-accordion-component="AccordionPanel"], .series-item, .editor-field-item__content'
|
|
82
|
+
) as HTMLElement
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await performAndAssert(
|
|
86
|
+
'Wait for added filter panel to render',
|
|
87
|
+
() => Boolean(getFilterBlock()),
|
|
88
|
+
async () => {},
|
|
89
|
+
(_before, after) => after
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// Find the newly added filter section content
|
|
93
|
+
const filterBlock = getFilterBlock()
|
|
94
|
+
expect(filterBlock).toBeTruthy()
|
|
95
|
+
|
|
96
|
+
// ==========================================================================
|
|
97
|
+
// TEST: Select STATE as the filter column
|
|
98
|
+
// ==========================================================================
|
|
99
|
+
const filterColumnSelect = Array.from(filterBlock?.querySelectorAll('select') || []).find(select => {
|
|
100
|
+
const label = select.closest('label')
|
|
101
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
102
|
+
return labelSpan?.textContent?.includes('Filter') && !labelSpan?.textContent?.includes('Style')
|
|
103
|
+
}) as HTMLSelectElement
|
|
104
|
+
|
|
105
|
+
const getDefaultValueState = () => {
|
|
106
|
+
const updatedFilterBlock = getFilterBlock()
|
|
107
|
+
const defaultValueSelect = Array.from(updatedFilterBlock?.querySelectorAll('select') || []).find(select => {
|
|
108
|
+
const label = select.closest('label')
|
|
109
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
110
|
+
return labelSpan?.textContent?.includes('Filter Default Value')
|
|
111
|
+
}) as HTMLSelectElement
|
|
112
|
+
return {
|
|
113
|
+
hasDefaultValueSelect: Boolean(defaultValueSelect),
|
|
114
|
+
optionCount: defaultValueSelect?.options.length || 0
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await performAndAssert(
|
|
119
|
+
'Filter Column → Select STATE',
|
|
120
|
+
getDefaultValueState,
|
|
121
|
+
async () => {
|
|
122
|
+
await userEvent.selectOptions(filterColumnSelect, 'STATE')
|
|
123
|
+
},
|
|
124
|
+
(before, after) => {
|
|
125
|
+
// Should populate the default value select with state options
|
|
126
|
+
return after.hasDefaultValueSelect && after.optionCount > 0
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
// ==========================================================================
|
|
131
|
+
// TEST: Filter Intro Text
|
|
132
|
+
// ==========================================================================
|
|
133
|
+
const filterIntroTextarea = Array.from(canvasElement.querySelectorAll('textarea') || []).find(textarea => {
|
|
134
|
+
const label = textarea.closest('label')
|
|
135
|
+
return label?.textContent?.includes('Filter intro text')
|
|
136
|
+
}) as HTMLTextAreaElement
|
|
137
|
+
|
|
138
|
+
const getFilterIntroText = () => {
|
|
139
|
+
const filtersSection = canvasElement.querySelector('.filters-section')
|
|
140
|
+
const filterIntro = filtersSection?.querySelector('.filters-section__intro-text')
|
|
141
|
+
return {
|
|
142
|
+
introText: filterIntro?.textContent?.trim() || '',
|
|
143
|
+
hasIntro: Boolean(filterIntro)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await performAndAssert(
|
|
148
|
+
'Filter Intro Text → Set custom text',
|
|
149
|
+
getFilterIntroText,
|
|
150
|
+
async () => {
|
|
151
|
+
await userEvent.clear(filterIntroTextarea)
|
|
152
|
+
await userEvent.type(filterIntroTextarea, 'Select a state to filter the map data.')
|
|
153
|
+
},
|
|
154
|
+
(before, after) => {
|
|
155
|
+
return after.hasIntro && after.introText.includes('Select a state to filter the map data')
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
// ==========================================================================
|
|
160
|
+
// TEST: Select Alabama as the default filter value
|
|
161
|
+
// ==========================================================================
|
|
162
|
+
const updatedFilterBlock = getFilterBlock()
|
|
163
|
+
const defaultValueSelect = Array.from(updatedFilterBlock?.querySelectorAll('select') || []).find(select => {
|
|
164
|
+
const label = select.closest('label')
|
|
165
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
166
|
+
return labelSpan?.textContent?.includes('Filter Default Value')
|
|
167
|
+
}) as HTMLSelectElement
|
|
168
|
+
|
|
169
|
+
await performAndAssert(
|
|
170
|
+
'Filter Default Value → Select Alabama',
|
|
171
|
+
() => {
|
|
172
|
+
// Check the legend text labels - when filtered to one state, shows single value
|
|
173
|
+
const legendContainer = canvasElement.querySelector('.legend-container')
|
|
174
|
+
const textElements = Array.from(legendContainer?.querySelectorAll('text tspan') || [])
|
|
175
|
+
const labels = textElements.map(el => el.textContent?.trim()).filter(t => t && t !== '')
|
|
176
|
+
const labelText = labels.join(',')
|
|
177
|
+
|
|
178
|
+
// Verify filter dropdown is rendered
|
|
179
|
+
const vizContainer = canvasElement.querySelector('.cove-visualization')
|
|
180
|
+
const filterSelect = Array.from(vizContainer?.querySelectorAll('select') || []).find(select => {
|
|
181
|
+
const options = Array.from(select.options).map(opt => opt.value)
|
|
182
|
+
return options.includes('Alabama')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
labelCount: labels.length,
|
|
187
|
+
labelText,
|
|
188
|
+
hasFilterSelect: Boolean(filterSelect),
|
|
189
|
+
selectedValue: filterSelect?.value || ''
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
async () => {
|
|
193
|
+
await userEvent.selectOptions(defaultValueSelect, 'Alabama')
|
|
194
|
+
},
|
|
195
|
+
(before, after) => {
|
|
196
|
+
// After filtering to Alabama: should have filter dropdown, Alabama selected, and exactly 1 legend label
|
|
197
|
+
// The legend shows a single value when only one state's data is displayed
|
|
198
|
+
return after.hasFilterSelect && after.selectedValue === 'Alabama' && after.labelCount === 1
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// ==========================================================================
|
|
203
|
+
// TEST: Filter Label
|
|
204
|
+
// ==========================================================================
|
|
205
|
+
const labelInput = Array.from(updatedFilterBlock?.querySelectorAll('input[type="text"]') || []).find(input => {
|
|
206
|
+
const label = input.closest('label')
|
|
207
|
+
const labelSpan = label?.querySelector('.edit-label')
|
|
208
|
+
return labelSpan?.textContent?.includes('Label') && !labelSpan?.textContent?.includes('Query')
|
|
209
|
+
}) as HTMLInputElement
|
|
210
|
+
|
|
211
|
+
await performAndAssert(
|
|
212
|
+
'Filter Label → Set custom label',
|
|
213
|
+
() => {
|
|
214
|
+
const filtersSection = canvasElement.querySelector('.filters-section')
|
|
215
|
+
const filterLabel = filtersSection?.querySelector('.form-group label')
|
|
216
|
+
return {
|
|
217
|
+
labelText: filterLabel?.textContent?.trim() || ''
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
async () => {
|
|
221
|
+
await userEvent.clear(labelInput)
|
|
222
|
+
await userEvent.type(labelInput, 'Select State')
|
|
223
|
+
},
|
|
224
|
+
(before, after) => {
|
|
225
|
+
return after.labelText.includes('Select State')
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
}
|