@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.
Files changed (79) hide show
  1. package/CONFIG.md +235 -0
  2. package/README.md +70 -24
  3. package/dist/cdcmap-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcmap.js +27405 -26257
  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 +3 -3
  11. package/examples/minimal-example.json +69 -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 +96 -13
  18. package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +601 -0
  19. package/src/_stories/CdcMap.Editor.DataTableSectionTests.stories.tsx +404 -0
  20. package/src/_stories/CdcMap.Editor.FiltersSectionTests.stories.tsx +229 -0
  21. package/src/_stories/CdcMap.Editor.GeneralSectionTests.stories.tsx +262 -0
  22. package/src/_stories/CdcMap.Editor.LegendSectionTests.stories.tsx +541 -0
  23. package/src/_stories/CdcMap.Editor.MultiCountryWorldMapTests.stories.tsx +359 -0
  24. package/src/_stories/CdcMap.Editor.PatternSettingsSectionTests.stories.tsx +516 -0
  25. package/src/_stories/CdcMap.Editor.SmallMultiplesSectionTests.stories.tsx +165 -0
  26. package/src/_stories/CdcMap.Editor.TextAnnotationsSectionTests.stories.tsx +145 -0
  27. package/src/_stories/CdcMap.Editor.TypeSectionTests.stories.tsx +312 -0
  28. package/src/_stories/CdcMap.Editor.VisualSectionTests.stories.tsx +359 -0
  29. package/src/_stories/CdcMap.Editor.ZoomControlsTests.stories.tsx +88 -0
  30. package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +12 -0
  31. package/src/_stories/_mock/legends/legend-tests.json +3 -3
  32. package/src/components/Annotation/AnnotationList.tsx +1 -1
  33. package/src/components/EditorPanel/components/EditorPanel.tsx +504 -383
  34. package/src/components/EditorPanel/components/HexShapeSettings.tsx +1 -1
  35. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +112 -117
  36. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +26 -13
  37. package/src/components/EditorPanel/components/editorPanel.styles.css +22 -2
  38. package/src/components/Legend/components/Legend.tsx +3 -3
  39. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
  40. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
  41. package/src/components/UsaMap/components/UsaMap.County.tsx +271 -100
  42. package/src/components/UsaMap/components/UsaMap.State.tsx +1 -1
  43. package/src/components/UsaMap/data/cb_2019_us_county_20m.json +75817 -1
  44. package/src/components/UsaMap/data/hsa_fips_mapping.json +3144 -0
  45. package/src/components/WorldMap/data/world-topo.json +1 -1
  46. package/src/data/initial-state.js +1 -0
  47. package/src/data/supported-counties.json +1 -1
  48. package/src/helpers/countyTerritories.ts +38 -0
  49. package/src/helpers/dataTableHelpers.ts +35 -6
  50. package/src/helpers/tests/countyTerritories.test.ts +87 -0
  51. package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
  52. package/src/hooks/useMapLayers.tsx +1 -1
  53. package/src/hooks/useTooltip.ts +18 -7
  54. package/src/store/map.actions.ts +5 -2
  55. package/src/store/map.reducer.ts +12 -3
  56. package/src/test/CdcMap.test.jsx +24 -0
  57. package/src/types/MapConfig.ts +6 -0
  58. package/src/types/MapContext.ts +3 -1
  59. package/topojson-updater/README.txt +1 -1
  60. package/LICENSE +0 -201
  61. package/dist/cdcmap-vr9HZwRt.es.js +0 -6
  62. package/examples/__data__/city-state-data.json +0 -668
  63. package/examples/city-state.json +0 -434
  64. package/examples/default-world-data.json +0 -1450
  65. package/examples/new-cities.json +0 -656
  66. package/src/_stories/CdcMap.Editor.stories.tsx +0 -3648
  67. package/topojson-updater/package-lock.json +0 -223
  68. /package/src/_stories/{CdcMap.ColumnWrap.stories.tsx → CdcMap.ColumnWrap.smoke.stories.tsx} +0 -0
  69. /package/src/_stories/{CdcMap.Defaults.stories.tsx → CdcMap.Defaults.smoke.stories.tsx} +0 -0
  70. /package/src/_stories/{CdcMap.DistrictOfColumbia.stories.tsx → CdcMap.DistrictOfColumbia.smoke.stories.tsx} +0 -0
  71. /package/src/_stories/{CdcMap.Filters.stories.tsx → CdcMap.Filters.smoke.stories.tsx} +0 -0
  72. /package/src/_stories/{CdcMap.Legend.Gradient.stories.tsx → CdcMap.Legend.Gradient.smoke.stories.tsx} +0 -0
  73. /package/src/_stories/{CdcMap.Legend.stories.tsx → CdcMap.Legend.smoke.stories.tsx} +0 -0
  74. /package/src/_stories/{CdcMap.Patterns.stories.tsx → CdcMap.Patterns.smoke.stories.tsx} +0 -0
  75. /package/src/_stories/{CdcMap.SmallMultiples.stories.tsx → CdcMap.SmallMultiples.smoke.stories.tsx} +0 -0
  76. /package/src/_stories/{CdcMap.Table.stories.tsx → CdcMap.Table.smoke.stories.tsx} +0 -0
  77. /package/src/_stories/{CdcMap.ZeroColor.stories.tsx → CdcMap.ZeroColor.smoke.stories.tsx} +0 -0
  78. /package/src/_stories/{GoogleMap.stories.tsx → GoogleMap.smoke.stories.tsx} +0 -0
  79. /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
+ }